Skip to main content

agents-sdk-dotnet-activityhandler-migration

Use when migrating a Microsoft 365 Agents SDK agent that uses ActivityHandler or TeamsActivityHandler to AgentApplication. This is ONLY for DotNet projects that uses `Microsoft.Agents.*` packages. Triggered by Agents SDK bots that subclass ActivityHandler or TeamsActivityHandler that need to be modernized to AgentApplication routing.

Informations de source

Dépôt
microsoft/Agents
Dernière activité de la source
4 mai 2026 à 21:08
Langue détectée de SKILL.md
anglais
Étoiles
1 092
Forks
346

Options d'installation

Le prompt qui vérifie d'abord la source est sélectionné par défaut. Vous pouvez passer à une commande directe ou télécharger une copie locale.

Vérifiez les fichiers source

Lisez SKILL.md et les fichiers associés affichés par SkillsMP avant de décider de l'installer.

Affichage de SKILL.md

SKILL.md
Instructions source · Aperçu en lecture seule
name
agents-sdk-dotnet-activityhandler-migration
description
Use when migrating a Microsoft 365 Agents SDK agent that uses ActivityHandler or TeamsActivityHandler to AgentApplication. This is ONLY for DotNet projects that uses `Microsoft.Agents.*` packages. Triggered by Agents SDK bots that subclass ActivityHandler or TeamsActivityHandler that need to be modernized to AgentApplication routing.
# Agents SDK ActivityHandler → AgentApplication Migration (.NET) ## Overview Upgrades a bot from the ActivityHandler compat layer to the modern `AgentApplication` routing pattern. This is ONLY for DotNet projects that already use `Microsoft.Agents.*` packages. If the project is still using `Microsoft.Bot.*` packages the `bf-to-agents-sdk-dotnet-migration` skill should be used first. **Two entry points:** - **Standalone:** The bot already uses `Microsoft.Agents.*` packages, namespaces, and appsettings — focus is on the bot class, Program.cs, and endpoint mapping. - **Second step after `bf-to-agents-sdk-dotnet-migration`:** Packages, namespaces, and appsettings were converted in that step — this skill handles the AgentApplication conversion. --- ## What Does NOT Need to Change Verify each of these is already correct — do not modify unless broken: | Item | Expected State | |------|---------------| | Namespaces in bot class | Already `Microsoft.Agents.*` — only remove compat namespaces | | `appsettings.json` | Already has `Connections` + `TokenValidation` sections | | `builder.Services.AddAgentAspNetAuthentication(...)` | Already present — keep | | `AspNetExtensions.cs` (if present) | Sample-provided auth helper — keep as-is | | Custom DI in Program.cs | Preserve dialogs, adapters, custom services — **but remove `ConversationState` and `UserState`** (see Step 2) | > **⚠️ If beta packages are required:** Tell the user explicitly: *"This migration uses pre-release (`-beta`) Agents SDK packages, which are nightly builds and not suitable for production. Upgrade to a stable release before deploying."* --- ## Handler Mapping: ActivityHandler → AgentApplication | ActivityHandler Override | AgentApplication Registration | |--------------------------|-------------------------------| | `OnMessageActivityAsync(ITurnContext<IMessageActivity>, CT)` | `OnActivity(ActivityTypes.Message, handler, rank: RouteRank.Last)` | | `OnMembersAddedAsync(IList<ChannelAccount>, ITurnContext<IConversationUpdateActivity>, CT)` | `OnConversationUpdate(ConversationUpdateEvents.MembersAdded, handler)` | | `OnMembersRemovedAsync(...)` | `OnConversationUpdate(ConversationUpdateEvents.MembersRemoved, handler)` | | `OnMessageUpdateActivityAsync(...)` | `OnActivity(ActivityTypes.MessageUpdate, handler)` | | `OnMessageDeleteActivityAsync(...)` | `OnActivity(ActivityTypes.MessageDelete, handler)` | | `OnReactionsAddedAsync(IList<MessageReaction>, ...)` | `OnMessageReactionsAdded(handler)` | | `OnReactionsRemovedAsync(IList<MessageReaction>, ...)` | `OnMessageReactionsRemoved(handler)` | | `OnEventAsync(ITurnContext<IEventActivity>, CT)` | `OnEvent(eventName, handler)` or `OnActivity(ActivityTypes.Event, handler)` | | `OnInvokeActivityAsync(...)` with `adaptiveCard/action` + verb switch | `AdaptiveCards.OnActionExecute(verb, handler)` — one registration per verb (preferred) | | `OnInvokeActivityAsync(...)` with `application/search` | `AdaptiveCards.OnSearch(dataset, handler)` — one registration per `choices.data.dataset` value | | `OnInvokeActivityAsync(...)` (other invoke types) | `OnActivity(ActivityTypes.Invoke, handler)` | | `OnTurnAsync(...)` override (cross-cutting) | `OnBeforeTurn(handler)` / `OnAfterTurn(handler)` | | `OnInstallationUpdateActivityAsync(...)` | `OnActivity(ActivityTypes.InstallationUpdate, handler)` | | `OnEndOfConversationActivityAsync(...)` | `OnActivity(ActivityTypes.EndOfConversation, handler)` | **Key difference:** Handler signature changes from `(ITurnContext<T>, CT)` to `(ITurnContext, ITurnState, CT)`. `membersAdded`, `membersRemoved`, `messageReactions` are no longer separate parameters — access via `turnContext.Activity.*`. ### `adaptiveCard/action` invokes → `AdaptiveCards.OnActionExecute` If the bot overrides `OnInvokeActivityAsync` and switches on a verb from `adaptiveCard/action` activities, replace it with per-verb `AdaptiveCards.OnActionExecute` registrations. `AdaptiveCards` is a property on `AgentApplication` (no `RegisterExtension` needed): ```csharp // Before: one OnInvokeActivityAsync with switch(verb) protected override async Task<InvokeResponse> OnInvokeActivityAsync(ITurnContext<IInvokeActivity> ctx, CancellationToken ct) { if (ctx.Activity.Name == "adaptiveCard/action") { var wrapper = JsonSerializer.Deserialize<AdaptiveCardInvokeValue>(ctx.Activity.Value.ToString()); switch (wrapper.Action.Verb) { case "approve": return CreateInvokeResponse(GetApproveCard(wrapper.Action.Data)); case "reject": return CreateInvokeResponse(GetRejectCard(wrapper.Action.Data)); } } return null; } // After: one registration per verb — framework filters and extracts action.data public MyBot(AgentApplicationOptions options) : base(options) { AdaptiveCards.OnActionExecute("approve", OnApproveAsync); AdaptiveCards.OnActionExecute("reject", OnRejectedAsync); } // Handler receives action.data directly — NOT the full AdaptiveCardInvokeValue // Return AdaptiveCardInvokeResponse directly — no CreateInvokeResponse() wrapper private Task<AdaptiveCardInvokeResponse> OnApproveAsync( ITurnContext turnContext, ITurnState turnState, object data, CancellationToken ct) { var actionData = ProtocolJsonSerializer.ToObject<MyDataModel>(data); // data == action.data return Task.FromResult(new AdaptiveCardInvokeResponse { StatusCode = 200, Type = "application/vnd.microsoft.card.adaptive", Value = BuildCard(actionData) }); } ``` **Key points:** - `data` parameter = `invokeValue.Action.Data` — deserialize as your data model, not as `AdaptiveCardInvokeValue` - Return `AdaptiveCardInvokeResponse` directly — the framework wraps it in an `InvokeResponse` automatically - `CreateInvokeResponse()` is a compat-layer helper that does not exist on `AgentApplication` - Namespace: `using Microsoft.Agents.Builder.App.AdaptiveCards;` --- ## Step 1-a: Convert Bot Class to AgentApplication (not using Dialogs) - Remove `using Microsoft.Agents.Builder.Compat;` (ActivityHandler namespace) - Remove `using Microsoft.Agents.Extensions.Teams.Compat;` (TeamsActivityHandler namespace) - Keep all other `using Microsoft.Agents.*` statements — they are already correct ```csharp // Before: ActivityHandler compat layer using Microsoft.Agents.Builder.Compat; // REMOVE using Microsoft.Agents.Extensions.Teams.Compat; // REMOVE if Teams using Microsoft.Agents.Builder; using Microsoft.Agents.Core.Models; public class EchoBot : ActivityHandler // or TeamsActivityHandler { protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken ct) { /* business logic */ } } // After: AgentApplication using Microsoft.Agents.Builder; using Microsoft.Agents.Builder.App; using Microsoft.Agents.Core.Models; public class EchoBot : AgentApplication { public EchoBot(AgentApplicationOptions options) : base(options) { OnConversationUpdate(ConversationUpdateEvents.MembersAdded, OnMembersAddedAsync); OnActivity(ActivityTypes.Message, OnMessageAsync, rank: RouteRank.Last); } // Signature: (ITurnContext, ITurnState, CancellationToken) private async Task OnMessageAsync(ITurnContext ctx, ITurnState state, CancellationToken ct) { /* same business logic */ } } ``` For Teams bots (`TeamsActivityHandler`) with Teams-specific handlers (messaging extensions, task modules, etc.), see **Step 1-d** below. ## Step 1-b: Convert Bot Class to AgentApplication (using Dialogs) - Remove `using Microsoft.Agents.Builder.Compat;` (ActivityHandler namespace) - Remove `using Microsoft.Agents.Extensions.Teams.Compat;` (TeamsActivityHandler namespace) - Keep all other `using Microsoft.Agents.*` statements — they are already correct ```csharp // Before: ActivityHandler compat layer using Microsoft.Agents.Builder.Compat; // REMOVE using Microsoft.Agents.Builder; using Microsoft.Agents.Builder.State; using Microsoft.Agents.Core.Models; public class EchoBot : ActivityHandler { protected override Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken ct) => dialog.RunAsync(turnContext, conversationState, ct); } // After: AgentApplication using Microsoft.Agents.Builder; using Microsoft.Agents.Builder.App; using Microsoft.Agents.Builder.State; using Microsoft.Agents.Core.Models; public class EchoBot : AgentApplication { public EchoBot(AgentApplicationOptions options, MainDialog dialog) : base(options) { _dialog = dialog; OnActivity(ActivityTypes.Message, OnMessageAsync, rank: RouteRank.Last); // signin/verifyState and signin/tokenExchange are both matched by "signin/.*" OnActivity( (ctx, ct) => Task.FromResult(ctx.Activity.IsType(ActivityTypes.Invoke) && ctx.Activity.Name != null && Regex.IsMatch(ctx.Activity.Name, "signin/.*")), OnSigninInvokeStateAsync); } // Signature: (ITurnContext, ITurnState, CancellationToken) — NOT the ActivityHandler signature private Task OnMessageAsync(ITurnContext turnContext, ITurnState turnState, CancellationToken ct) => _dialog.RunAsync(turnContext, turnState.Conversation, ct); private Task OnSigninInvokeStateAsync(ITurnContext turnContext, ITurnState turnState, CancellationToken ct) => _dialog.RunAsync(turnContext, turnState.Conversation, ct); } ``` **Key differences from non-dialog migration:** - Do **not** inject `ConversationState` into the bot — pass `turnState.Conversation` to `Dialog.RunAsync` instead - Use `InvokeRouteBuilder` (not `OnActivity(ActivityTypes.Invoke, ...)`) to route signin invokes — the name pattern matches both `signin/verifyState` and `signin/tokenExchange` - Remove `ConversationState` and `UserState` from DI entirely — see Step 2 > ⚠️ **Generic `DialogBot<T>` base class pattern:** Some projects use a two-level class hierarchy where a generic base class (e.g. `DialogBot<T> : ActivityHandler`) holds the dialog field, state injection, and `OnTurnAsync` state-saving override, and the concrete bot class (e.g. `MyBot : DialogBot<MainDialog>`) only adds routing. Collapse both classes into a single non-generic `AgentApplication` subclass — inject the concrete dialog type directly in the constructor. The generic type parameter is not needed. Remove `OnTurnAsync` entirely (AgentApplication manages state automatically). --- ## Step 1-c: Remove State Management Boilerplate ### Remove OnTurnBeginAsync / OnTurnEndAsync Overrides `ActivityHandler` and `TeamsActivityHandler` bots sometimes manually load and save state in turn lifecycle overrides. Remove these entirely — `AgentApplication` handles state loading and saving automatically: ```csharp // REMOVE these overrides completely — AgentApplication does this automatically protected override async Task OnTurnBeginAsync(ITurnContext turnContext, CancellationToken cancellationToken = default) { await ConversationState.LoadAsync(turnContext, false, cancellationToken); await UserState.LoadAsync(turnContext, false, cancellationToken); } protected override async Task OnTurnEndAsync(ITurnContext turnContext, CancellationToken cancellationToken = default) { await ConversationState.SaveChangesAsync(turnContext, false, cancellationToken); await UserState.SaveChangesAsync(turnContext, false, cancellationToken); } ``` Also remove any `ConversationState` or `UserState` properties/fields that were injected for this purpose. ### Migrate CloudAdapter OnTurnError to AgentApplication If a custom `CloudAdapter` subclass injects `ConversationState` only to call `DeleteStateAsync` on error, move that logic to an `AgentApplication.OnTurnError` handler instead. **Before — in the custom adapter:** ```csharp public MyAdapter( IChannelServiceClientFactory channelServiceClientFactory, IActivityTaskQueue activityTaskQueue, ILogger<CloudAdapter> logger, ConversationState conversationState) // REMOVE this parameter : base(channelServiceClientFactory, activityTaskQueue, logger: logger) { OnTurnError = async (turnContext, exception) => { if (conversationState != null) { await conversationState.DeleteStateAsync(turnContext); // REMOVE } }; } ``` **After — remove ConversationState from the adapter:** ```csharp public MyAdapter( IChannelServiceClientFactory channelServiceClientFactory, IActivityTaskQueue activityTaskQueue, ILogger<CloudAdapter> logger) : base(channelServiceClientFactory, activityTaskQueue, logger: logger) { // OnTurnError can remain for logging; state deletion moves to AgentApplication } ``` **Add to the AgentApplication bot constructor:** ```csharp public MyBot(AgentApplicationOptions options, ...) : base(options) { OnTurnError(OnAgentTurnErrorAsync); // ... other route registrations } private async Task OnAgentTurnErrorAsync(ITurnContext turnContext, ITurnState turnState, Exception exception, CancellationToken cancellationToken) { try { await turnState.Conversation.DeleteStateAsync(turnContext, cancellationToken); } catch(Exception ex) { Logger.LogError(ex, "Exception deleting conversation state after a turn error."); } } ``` Customer-written classes that accept `ConversationState` or `UserState` as constructor parameters can remain unchanged for now. However, they will ultimately need to source their state from `ITurnState.Conversation` or `ITurnState.User` — flag this to the customer as a follow-up item. ### `application/search` invokes → `AdaptiveCards.OnSearch` If the bot overrides `OnInvokeActivityAsync` and handles `activity.Name == "application/search"` (adaptive card typeahead/dynamic search), replace it with per-dataset `AdaptiveCards.OnSearch` registrations. The dataset name comes from `choices.data.dataset` in the card JSON. The handler returns `IList<AdaptiveCardsSearchResult>` — the framework automatically wraps results in the `application/vnd.microsoft.search.searchResponse` format and sends the invoke response. ```csharp // Before: one OnInvokeActivityAsync checking activity.Name protected override async Task<InvokeResponse> OnInvokeActivityAsync(ITurnContext<IInvokeActivity> turnContext, CancellationToken ct) { if (turnContext.Activity.Name == "application/search") { var searchData = JsonSerializer.Deserialize<DynamicSearchCard>(turnContext.Activity.Value.ToString()); // ... build results ... return new InvokeResponse { Status = 200, Body = searchResponseData }; } return null; } // After: per-dataset OnSearch registrations public ActivityBot(AgentApplicationOptions options) : base(options) { AdaptiveCards.OnSearch("npmpackages", OnNpmSearchAsync); AdaptiveCards.OnSearch("cities", OnCitiesSearchAsync); } // Handler signature: (ITurnContext, ITurnState, Query<AdaptiveCardsSearchParams>, CT) → Task<IList<AdaptiveCardsSearchResult>> private async Task<IList<AdaptiveCardsSearchResult>> OnNpmSearchAsync( ITurnContext turnContext, ITurnState turnState, Query<AdaptiveCardsSearchParams> query, CancellationToken ct) { // query.Parameters.QueryText = search text typed by user // query.Parameters.Dataset = "npmpackages" // ... fetch results ... return results.Select(r => new AdaptiveCardsSearchResult(r.Title, r.Value)).ToList(); } ``` **Key differences:** - Return `IList<AdaptiveCardsSearchResult>` directly — no `InvokeResponse` wrapper - `AdaptiveCardsSearchResult(string title, string value)` — constructor takes title and value - `query.Parameters.QueryText` replaces `searchData.queryText` - `query.Parameters.Dataset` contains the dataset name (same as the registration key) > ⚠️ **Dependent dropdown — associated inputs not in `query`:** When a card uses `"associatedInputs": "auto"`, the selected values of other inputs are sent in the `data` field of the raw invoke payload — NOT in `query.Parameters`. Access them directly: `ProtocolJsonSerializer.ToObject<MyModel>(turnContext.Activity.Value)` where `MyModel` maps the top-level `data` field. > > ```csharp
Voir sur GitHub
Ce SKILL.md est tres volumineux, SkillsMP affiche donc ici seulement la premiere section. Voir sur GitHub