From agents-for-dotnet
Use when any code imports Microsoft.Agents.Hosting.AspNetCore, Microsoft.Agents.Builder, or related Agents SDK packages, or when the user is building, configuring, or asking questions about a Microsoft 365 Agents SDK agent in C# / .NET. Trigger on questions about appsettings.json, connection configuration, AgentApplication patterns, OAuth sign-in flows, storage backends, cards, streaming, or local testing with the Agents Playground — even if no code exists yet and the user is planning or asking how to get started.
How this skill is triggered — by the user, by Claude, or both
Slash command
/agents-for-dotnet:agents-sdk-dotnetThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
The Microsoft 365 Agents SDK builds multichannel agents for Teams, Copilot Studio, and web chat.
The Microsoft 365 Agents SDK builds multichannel agents for Teams, Copilot Studio, and web chat.
| Package | Purpose |
|---|---|
Microsoft.Agents.Hosting.AspNetCore | ASP.NET Core hosting, auth, endpoint mapping |
Microsoft.Agents.Authentication.Msal | MSAL-based auth (client credentials, OBO) |
Microsoft.Agents.Builder | Core: AgentApplication, ITurnContext, ITurnState |
Microsoft.Agents.Core | Models, activity types, channel accounts |
Microsoft.Agents.Storage | IStorage, MemoryStorage |
Microsoft.Agents.Extensions.Teams | Teams-specific models and extensions |
Microsoft.Agents.Builder.Dialogs | Dialog system (waterfall, prompts) |
Microsoft.Agents.AI | AI-specific features |
Requires .NET 8+.
Microsoft Entra App Registration
ClientId — Application (client) IDClientSecret — Certificates & secretsTenantId — Directory (tenant) IDAzure Bot Resource
https://<your-host>/api/messagesClientIdLocal dev: Set TokenValidation:Enabled to false. No Azure Bot needed until deployment.
{
"Connections": {
"ServiceConnection": {
"Settings": {
"AuthType": "ClientSecret",
"AuthorityEndpoint": "https://login.microsoftonline.com/<tenantId>",
"ClientId": "<appId>",
"ClientSecret": "<secret>",
"Scopes": ["https://api.botframework.com/.default"]
}
}
},
"ConnectionsMap": [
{
"ServiceUrl": "*",
"Connection": "ServiceConnection"
}
],
"TokenValidation": {
"Enabled": true,
"Audiences": ["<appId>"],
"TenantId": "<tenantId>"
}
}
Each entry maps a ServiceUrl pattern to a named connection. The first matching entry wins.
ServiceUrl: "*" — matches any service URL (use as the default/fallback){
"Connections": {
"MainConn": {
"Settings": {
"AuthType": "ClientSecret",
"AuthorityEndpoint": "https://login.microsoftonline.com/<tenantId>",
"ClientId": "<app-id-1>",
"ClientSecret": "<secret-1>",
"Scopes": ["https://api.botframework.com/.default"]
}
},
"TeamsConn": {
"Settings": {
"AuthType": "ClientSecret",
"AuthorityEndpoint": "https://login.microsoftonline.com/<tenantId>",
"ClientId": "<app-id-2>",
"ClientSecret": "<secret-2>",
"Scopes": ["https://api.botframework.com/.default"]
}
}
},
"ConnectionsMap": [
{
"ServiceUrl": "https://smba.trafficmanager.net/.*",
"Connection": "TeamsConn"
},
{
"ServiceUrl": "*",
"Connection": "MainConn"
}
]
}
UserManagedIdentity (no secret, Azure-hosted only):
{
"Connections": {
"ServiceConnection": {
"Settings": {
"AuthType": "UserManagedIdentity",
"ClientId": "<msi-clientId>",
"Scopes": ["https://api.botframework.com/.default"]
}
}
}
}
FederatedCredentials (no secret, App Registration + MSI):
{
"Connections": {
"ServiceConnection": {
"Settings": {
"AuthType": "FederatedCredentials",
"AuthorityEndpoint": "https://login.microsoftonline.com/<tenantId>",
"ClientId": "<appId>",
"FederatedClientId": "<msi-clientId>",
"Scopes": ["https://api.botframework.com/.default"]
}
}
}
}
AuthType, AuthorityEndpoint, ClientId, ClientSecret, TenantId, FederatedClientId, Scopes, CertificateSubject, CertificateThumbprint
{
"TokenValidation": {
"Enabled": false
}
}
using Microsoft.Agents.Builder;
using Microsoft.Agents.Builder.App;
using Microsoft.Agents.Core.Models;
using Microsoft.Agents.Hosting.AspNetCore;
using Microsoft.Agents.Storage;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpClient();
builder.AddAgent<MyAgent>();
builder.Services.AddSingleton<IStorage, MemoryStorage>();
builder.Services.AddAgentAspNetAuthentication(builder.Configuration);
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapAgentRootEndpoint();
app.MapAgentApplicationEndpoints(requireAuth: !app.Environment.IsDevelopment());
app.Run();
public class MyAgent : AgentApplication
{
public MyAgent(AgentApplicationOptions options) : base(options)
{
OnConversationUpdate(ConversationUpdateEvents.MembersAdded, WelcomeAsync);
OnActivity(ActivityTypes.Message, OnMessageAsync, rank: RouteRank.Last);
}
private async Task WelcomeAsync(ITurnContext ctx, ITurnState state, CancellationToken ct)
{
foreach (var member in ctx.Activity.MembersAdded)
{
if (member.Id != ctx.Activity.Recipient.Id)
{
await ctx.SendActivityAsync("Hello! Send me a message.", cancellationToken: ct);
}
}
}
private async Task OnMessageAsync(ITurnContext ctx, ITurnState state, CancellationToken ct)
{
int counter = state.GetValue("conversation.counter", () => 0);
await ctx.SendActivityAsync($"[{counter++}] You said: {ctx.Activity.Text}", cancellationToken: ct);
state.SetValue("conversation.counter", counter);
}
}
Run: dotnet run
using Microsoft.Agents.Hosting.AspNetCore;
using Microsoft.Agents.Storage;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpClient();
builder.AddAgent<MyAgent>();
builder.Services.AddSingleton<IStorage, MemoryStorage>();
builder.Services.AddAgentAspNetAuthentication(builder.Configuration);
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapAgentRootEndpoint();
app.MapAgentApplicationEndpoints(requireAuth: !app.Environment.IsDevelopment());
app.Run();
app.MapAgentProactiveEndpoints<MyAgent>(requireAuth: !app.Environment.IsDevelopment());
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapAgentRootEndpoint();
app.MapAgentApplicationEndpoints(requireAuth: !app.Environment.IsDevelopment());
app.MapGet("/health", () => Results.Json(new { ok = true }));
app.Run();
[Agent(name: "MyAgent", description: "A helpful agent", version: "1.0")]
[AgentInterface(protocol: AgentTransportProtocol.ActivityProtocol, path: "/api/messages")]
public class MyAgent : AgentApplication
{
// ...
}
builder.AddAgent<T>() — registers the agent class. Always required.AddAgentApplicationOptions() separately unless you have a specific reason — AddAgent handles it.MapAgentApplicationEndpoints — for AgentApplication subclasses (modern pattern)MapAgentEndpoints — for ActivityHandler / TeamsActivityHandler compat layer botsUsing the wrong one causes runtime routing failures with no clear error.
curl -s -X POST \
"https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token" \
-d "grant_type=client_credentials\
&client_id=$clientId\
&client_secret=$clientSecret\
&scope=https://api.botframework.com/.default" \
| jq '{token_type, expires_in, error, error_description}'
Common errors:
AADSTS700016 — ClientId not found in tenant (wrong ID or wrong tenant)AADSTS7000215 — invalid ClientSecret (expired or incorrect)AADSTS90002 — TenantId not foundcurl -s -o /dev/null -w "%{http_code}" \
-X POST http://localhost:3978/api/messages \
-H "Content-Type: application/json" \
-d '{}'
401 — agent is running; JWT auth rejected the empty request (expected)000 or connection refused — agent is not running or wrong port200 — agent is running with auth disabled (local dev with TokenValidation:Enabled = false)OAuth connection names can only be tested end-to-end via:
Azure Portal → Your Bot Resource → Settings → OAuth Connection Settings → [your connection] → Test Connection
The Agents Playground lets you test locally without deploying to Azure.
Install:
npm install -g agentsplayground
Run against an anonymous agent:
agentsplayground -c emulator
Then start your agent separately with dotnet run.
With authentication:
agentsplayground -c msteams \
--client-id <your-app-id> \
--client-secret <your-secret> \
--tenant-id <your-tenant-id>
Channel options (-c): msteams, webchat, directline, emulator, agents
If the playground connects but messages don't get responses, add a fallback handler to confirm:
OnActivity(ActivityTypes.Message, async (ctx, state, ct) =>
{
await ctx.SendActivityAsync($"Echo: {ctx.Activity.Text}", cancellationToken: ct);
}, rank: RouteRank.Last);
Routing
OnMessage("/cmd", handler); // exact command
OnActivity(ActivityTypes.Message, handler, rank: RouteRank.Last); // all messages (fallback)
OnConversationUpdate(ConversationUpdateEvents.MembersAdded, handler);
OnActivity(ActivityTypes.Invoke, handler);
Per-route auth:
OnMessage("-me", OnMe, autoSignInHandlers: ["me"]);
TurnState — dot-notation keys scoped to conversation/user/temp:
int count = state.GetValue("conversation.counter", () => 0);
state.SetValue("conversation.counter", count + 1);
state.DeleteValue("conversation.counter");
Storage backends
| Backend | Use case |
|---|---|
MemoryStorage | Local dev only — not persistent |
Cosmos DB (via IStorage impl) | Production |
Blob Storage (via IStorage impl) | Production |
{
"AgentApplication": {
"UserAuthorization": {
"DefaultHandlerName": "graph",
"AutoSignin": true,
"Handlers": {
"graph": {
"Settings": {
"AzureBotOAuthConnectionName": "GraphOAuthConnection",
"Title": "Sign in with Microsoft",
"Text": "Please sign in to continue"
}
}
}
}
}
}
public class MyAgent : AgentApplication
{
public MyAgent(AgentApplicationOptions options) : base(options)
{
// Handle sign-in failure
UserAuthorization.OnUserSignInFailure(
async (ctx, state, handler, response, activity, ct) =>
{
await ctx.SendActivityAsync($"Sign-in failed: {response.Error?.Message}", cancellationToken: ct);
});
// Protected route — requires sign-in via "graph" handler
OnMessage("-profile", OnProfileAsync, autoSignInHandlers: ["graph"]);
// Fallback message handler
OnActivity(ActivityTypes.Message, OnMessageAsync, rank: RouteRank.Last);
// Sign out
OnMessage("/logout", OnLogoutAsync);
}
private async Task OnProfileAsync(ITurnContext ctx, ITurnState state, CancellationToken ct)
{
// Token is guaranteed here — route won't run until user is signed in
string token = await UserAuthorization.GetTurnTokenAsync(ctx, "graph");
// Use token to call Graph API
}
private async Task OnLogoutAsync(ITurnContext ctx, ITurnState state, CancellationToken ct)
{
await UserAuthorization.SignOutUserAsync(ctx, state, cancellationToken: ct);
await ctx.SendActivityAsync("Signed out.", cancellationToken: ct);
}
}
OBO (on-behalf-of) — exchange user token for a downstream service token:
string newToken = await UserAuthorization.ExchangeTurnTokenAsync(
ctx,
"graph",
exchangeScopes: ["https://graph.microsoft.com/.default"]
);
{
"AgentApplication": {
"UserAuthorization": {
"DefaultHandlerName": "auto",
"AutoSignin": true,
"Handlers": {
"auto": {
"Settings": {
"AzureBotOAuthConnectionName": "AutoConnection"
}
},
"me": {
"Settings": {
"AzureBotOAuthConnectionName": "MeConnection"
}
}
}
}
}
}
using Microsoft.Agents.Core.Models;
using System.Text.Json;
Adaptive Card (from JSON):
string cardJson = File.ReadAllText("Resources/myCard.json");
var card = new Attachment
{
ContentType = ContentTypes.AdaptiveCard,
Content = JsonSerializer.Deserialize<object>(cardJson)
};
await ctx.SendActivityAsync(MessageFactory.Attachment(card), cancellationToken: ct);
Hero Card:
var card = new HeroCard
{
Title = "Card Title",
Images = new List<CardImage> { new CardImage("https://example.com/image.jpg") },
Buttons = new List<CardAction>
{
new CardAction(ActionTypes.OpenUrl, "Learn more", value: "https://example.com")
}
};
await ctx.SendActivityAsync(MessageFactory.Attachment(card.ToAttachment()), cancellationToken: ct);
Adaptive Card Action Execute:
public MyAgent(AgentApplicationOptions options) : base(options)
{
AdaptiveCards.OnActionExecute("approve", OnApproveAsync);
AdaptiveCards.OnActionExecute("reject", OnRejectAsync);
}
private Task<AdaptiveCardInvokeResponse> OnApproveAsync(
ITurnContext ctx, ITurnState state, object data, CancellationToken ct)
{
var actionData = ProtocolJsonSerializer.ToObject<MyDataModel>(data);
return Task.FromResult(new AdaptiveCardInvokeResponse
{
StatusCode = 200,
Type = "application/vnd.microsoft.card.adaptive",
Value = BuildCard(actionData)
});
}
Adaptive Card Search (typeahead):
AdaptiveCards.OnSearch("myDataset", OnSearchAsync);
private Task<IList<AdaptiveCardsSearchResult>> OnSearchAsync(
ITurnContext ctx, ITurnState state, Query<AdaptiveCardsSearchParams> query, CancellationToken ct)
{
var results = new List<AdaptiveCardsSearchResult>
{
new AdaptiveCardsSearchResult("Result 1", "value1")
};
return Task.FromResult<IList<AdaptiveCardsSearchResult>>(results);
}
private async Task OnMessageAsync(ITurnContext ctx, ITurnState state, CancellationToken ct)
{
try
{
ctx.StreamingResponse.SetFeedbackLoop(true);
ctx.StreamingResponse.SetGeneratedByAILabel(true);
await ctx.StreamingResponse.QueueInformativeUpdateAsync("Working on it...", ct);
ctx.StreamingResponse.QueueTextChunk("Part 1 ");
ctx.StreamingResponse.QueueTextChunk("Part 2");
}
finally
{
await ctx.StreamingResponse.EndStreamAsync(ct);
}
}
Streaming with Azure OpenAI:
private async Task OnMessageAsync(ITurnContext ctx, ITurnState state, CancellationToken ct)
{
try
{
await ctx.StreamingResponse.QueueInformativeUpdateAsync("Thinking...", ct);
await foreach (var update in chatClient.CompleteChatStreamingAsync(messages, cancellationToken: ct))
{
if (update.ContentUpdate.Count > 0 && !string.IsNullOrEmpty(update.ContentUpdate[0]?.Text))
{
ctx.StreamingResponse.QueueTextChunk(update.ContentUpdate[0].Text);
}
}
}
finally
{
await ctx.StreamingResponse.EndStreamAsync(ct);
}
}
Streaming with a final card:
ctx.StreamingResponse.FinalMessage = MessageFactory.Attachment(new Attachment
{
ContentType = ContentTypes.AdaptiveCard,
Content = cardJson
});
await ctx.StreamingResponse.EndStreamAsync(ct);
public class MyAgent : AgentApplication
{
public MyAgent(AgentApplicationOptions options) : base(options)
{
OnActivity(ActivityTypes.Message, OnMessageAsync, rank: RouteRank.Last);
}
private async Task OnMessageAsync(ITurnContext ctx, ITurnState state, CancellationToken ct)
{
// Store the conversation reference for later
string convId = await Proactive.StoreConversationAsync(ctx, ct);
await ctx.SendActivityAsync($"Stored conversation: {convId}", cancellationToken: ct);
}
[ContinueConversation]
public async Task OnProactiveAsync(ITurnContext ctx, ITurnState state, CancellationToken ct)
{
await ctx.SendActivityAsync("Proactive message!", cancellationToken: ct);
}
}
In Program.cs:
app.MapAgentProactiveEndpoints<MyAgent>(requireAuth: !app.Environment.IsDevelopment());
using OpenTelemetry;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
builder.Services.AddOpenTelemetry()
.ConfigureResource(r => r.AddService("MyAgent", serviceVersion: "1.0"))
.WithTracing(tracing => tracing
.AddSource("Microsoft.AspNetCore", "System.Net.Http")
.SetSampler(new AlwaysOnSampler())
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddOtlpExporter());
Required packages:
<PackageReference Include="OpenTelemetry" Version="1.*" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.*" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.*" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.*" />
1. Wrong endpoint mapping method
// WRONG — for ActivityHandler compat layer only
app.MapAgentEndpoints(requireAuth: false);
// CORRECT — for AgentApplication subclasses
app.MapAgentApplicationEndpoints(requireAuth: false);
2. Missing IStorage registration
// WRONG — no storage registered, runtime error
builder.AddAgent<MyAgent>();
// CORRECT — always register IStorage
builder.AddAgent<MyAgent>();
builder.Services.AddSingleton<IStorage, MemoryStorage>();
3. Calling AddAgentApplicationOptions separately
// WRONG — double registration
builder.AddAgent<MyAgent>();
builder.Services.AddAgentApplicationOptions(builder.Configuration);
// CORRECT — AddAgent handles everything
builder.AddAgent<MyAgent>();
4. Wrong ConversationReference field name
// WRONG
var reference = new ConversationReference { Bot = new ChannelAccount(appId) };
// CORRECT
var reference = new ConversationReference { Agent = new ChannelAccount(appId) };
5. Forgetting UseAuthentication/UseAuthorization
// WRONG — auth middleware missing
var app = builder.Build();
app.MapAgentApplicationEndpoints(requireAuth: true);
// CORRECT
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapAgentApplicationEndpoints(requireAuth: true);
6. TokenValidation:Enabled left true for local dev
If TokenValidation:Enabled is true with no valid credentials configured, every incoming request will be rejected with 401. Set to false for local anonymous development.
If you hit a problem this skill couldn't solve, found a workaround, or noticed something wrong or outdated, that's valuable — please help improve this skill for everyone.
Draft a suggested issue title and body based on the conversation, then ask the user to open it at: https://github.com/microsoft/agents/issues/new
A good issue includes:
npx claudepluginhub nature-magic-tech/agents --plugin agents-for-netGuides collaborative design exploration before implementation: explores context, asks clarifying questions, proposes approaches, and writes a design doc for user approval.
Creates structured, bite-sized implementation plans from specs or requirements before writing code. Useful for breaking down multi-step tasks into testable steps with file structure and task boundaries.
Synthesizes the current conversation into a structured spec (PRD) and publishes it to the project issue tracker with a ready-for-agent label, without interviewing the user.
3plugins reuse this skill
First indexed Jun 29, 2026