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.

Datos de origen

Repositorio
microsoft/Agents
Última actividad en el origen
4 de mayo de 2026 a las 21:08
Idioma detectado de SKILL.md
inglés
Estrellas
1085
Forks
348

Opciones de instalación

De forma predeterminada está seleccionado el prompt que primero revisa el origen. Puedes cambiar a un comando directo o descargar una copia local.

Revisa los archivos de origen

Lee SKILL.md y los archivos complementarios que muestra SkillsMP antes de decidir si quieres instalarlo.

Mostrando SKILL.md

SKILL.md
Instrucciones de origen · Vista previa de solo lectura
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
Ver en GitHub
Este SKILL.md es muy grande, por eso SkillsMP muestra aqui solo la primera seccion. Ver en GitHub