microsoft/teams.net
Publicmirrored fromhttps://github.com/microsoft/teams.netAvailable
devtools-plan/02-core-architecture.md
299lines · modecode
| 1 | # Core Branch Architecture (`upstream/next/core`) |
| 2 | |
| 3 | How the target `upstream/next/core` branch works — the architecture DevTools must integrate with. |
| 4 | |
| 5 | ## Overview |
| 6 | |
| 7 | The core branch is a complete rewrite of the .NET SDK. Key differences from `main`: |
| 8 | |
| 9 | - **No plugin system** — `IPlugin`, `IAspNetCorePlugin`, `ISenderPlugin`, `[Plugin]`, `[Dependency]` are all removed |
| 10 | - **3 layers**: `Microsoft.Teams.Bot.Core` → `Microsoft.Teams.Bot.Apps` → `Microsoft.Teams.Bot.Compat` |
| 11 | - **Middleware-based pipeline** instead of plugin event callbacks |
| 12 | - **Virtual methods on `ConversationClient`** for extensibility |
| 13 | - **Minimal API hosting** (`MapPost`) instead of MVC controllers |
| 14 | |
| 15 | --- |
| 16 | |
| 17 | ## Layer 1: `Microsoft.Teams.Bot.Core` |
| 18 | |
| 19 | The protocol-level foundation. No Teams-specific concepts. |
| 20 | |
| 21 | ### `BotApplication.cs` |
| 22 | |
| 23 | The central class that processes incoming activities. |
| 24 | |
| 25 | ```csharp |
| 26 | public class BotApplication |
| 27 | { |
| 28 | private readonly ConversationClient? _conversationClient; |
| 29 | private readonly UserTokenClient? _userTokenClient; |
| 30 | internal TurnMiddleware MiddleWare { get; } |
| 31 | |
| 32 | public BotApplication(ConversationClient conversationClient, UserTokenClient userTokenClient, |
| 33 | ILogger<BotApplication> logger, BotApplicationOptions? options = null) |
| 34 | { |
| 35 | MiddleWare = new TurnMiddleware(); |
| 36 | _conversationClient = conversationClient; |
| 37 | _userTokenClient = userTokenClient; |
| 38 | // ... |
| 39 | } |
| 40 | |
| 41 | public ConversationClient ConversationClient => _conversationClient ?? throw ...; |
| 42 | public UserTokenClient UserTokenClient => _userTokenClient ?? throw ...; |
| 43 | |
| 44 | // Terminal handler — invoked after all middleware runs |
| 45 | public virtual Func<CoreActivity, CancellationToken, Task>? OnActivity { get; set; } |
| 46 | |
| 47 | // Entry point for incoming HTTP requests |
| 48 | public virtual async Task ProcessAsync(HttpContext httpContext, CancellationToken cancellationToken = default) |
| 49 | { |
| 50 | CoreActivity activity = await CoreActivity.FromJsonStreamAsync(httpContext.Request.Body, cancellationToken) |
| 51 | ?? throw new InvalidOperationException("Invalid Activity"); |
| 52 | |
| 53 | try |
| 54 | { |
| 55 | CancellationToken token = Debugger.IsAttached ? CancellationToken.None : cancellationToken; |
| 56 | await MiddleWare.RunPipelineAsync(this, activity, this.OnActivity, 0, token); |
| 57 | } |
| 58 | catch (Exception ex) |
| 59 | { |
| 60 | throw new BotHandlerException("Error processing activity", ex, activity); |
| 61 | } |
| 62 | } |
| 63 | |
| 64 | // Sends activity via ConversationClient |
| 65 | public async Task<SendActivityResponse?> SendActivityAsync(CoreActivity activity, CancellationToken cancellationToken = default) |
| 66 | { |
| 67 | return await _conversationClient.SendActivityAsync(activity, cancellationToken: cancellationToken); |
| 68 | } |
| 69 | |
| 70 | // Register middleware |
| 71 | public ITurnMiddleware UseMiddleware(ITurnMiddleware middleware) |
| 72 | { |
| 73 | MiddleWare.Use(middleware); |
| 74 | return MiddleWare; |
| 75 | } |
| 76 | } |
| 77 | ``` |
| 78 | |
| 79 | **Key integration points for DevTools:** |
| 80 | - `ProcessAsync` — where incoming activities enter the system |
| 81 | - `SendActivityAsync` — delegates to `ConversationClient` (interceptable via virtual override) |
| 82 | - `UseMiddleware` — how to register middleware that sees every incoming activity |
| 83 | - `OnActivity` — terminal callback, runs after all middleware |
| 84 | |
| 85 | ### `ITurnMiddleware.cs` |
| 86 | |
| 87 | ```csharp |
| 88 | public delegate Task NextTurn(CancellationToken cancellationToken); |
| 89 | |
| 90 | public interface ITurnMiddleware |
| 91 | { |
| 92 | Task OnTurnAsync(BotApplication botApplication, CoreActivity activity, |
| 93 | NextTurn nextTurn, CancellationToken cancellationToken = default); |
| 94 | } |
| 95 | ``` |
| 96 | |
| 97 | Middleware can: |
| 98 | - Run code **before** `nextTurn()` (pre-processing) |
| 99 | - Run code **after** `nextTurn()` (post-processing) |
| 100 | - Wrap `nextTurn()` in try/catch (error handling) |
| 101 | - Short-circuit by not calling `nextTurn()` at all |
| 102 | |
| 103 | ### `TurnMiddleware.cs` (internal) |
| 104 | |
| 105 | Chain-of-responsibility pipeline executor: |
| 106 | |
| 107 | ```csharp |
| 108 | internal sealed class TurnMiddleware : ITurnMiddleware, IEnumerable<ITurnMiddleware> |
| 109 | { |
| 110 | private readonly IList<ITurnMiddleware> _middlewares = []; |
| 111 | |
| 112 | internal TurnMiddleware Use(ITurnMiddleware middleware) { _middlewares.Add(middleware); return this; } |
| 113 | |
| 114 | public Task RunPipelineAsync(BotApplication botApplication, CoreActivity activity, |
| 115 | Func<CoreActivity, CancellationToken, Task>? callback, int nextMiddlewareIndex, CancellationToken ct) |
| 116 | { |
| 117 | if (nextMiddlewareIndex == _middlewares.Count) |
| 118 | return callback?.Invoke(activity, ct) ?? Task.CompletedTask; |
| 119 | |
| 120 | ITurnMiddleware nextMiddleware = _middlewares[nextMiddlewareIndex]; |
| 121 | return nextMiddleware.OnTurnAsync( |
| 122 | botApplication, activity, |
| 123 | (ct) => RunPipelineAsync(botApplication, activity, callback, nextMiddlewareIndex + 1, ct), |
| 124 | ct); |
| 125 | } |
| 126 | } |
| 127 | ``` |
| 128 | |
| 129 | ### `ConversationClient.cs` |
| 130 | |
| 131 | All methods are `virtual` — this is how DevTools can intercept outgoing activities: |
| 132 | |
| 133 | ```csharp |
| 134 | public class ConversationClient(HttpClient httpClient, ILogger<ConversationClient> logger = default!) |
| 135 | { |
| 136 | internal const string ConversationHttpClientName = "BotConversationClient"; |
| 137 | |
| 138 | public CustomHeaders DefaultCustomHeaders { get; } = []; |
| 139 | |
| 140 | public virtual async Task<SendActivityResponse> SendActivityAsync(CoreActivity activity, |
| 141 | CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default) |
| 142 | { |
| 143 | // Builds URL from activity.ServiceUrl + conversation ID |
| 144 | // Serializes activity to JSON, sends via HTTP POST |
| 145 | // Returns SendActivityResponse with activity ID |
| 146 | } |
| 147 | |
| 148 | public virtual async Task<SendActivityResponse> UpdateActivityAsync(...) { ... } |
| 149 | public virtual async Task DeleteActivityAsync(...) { ... } |
| 150 | public virtual async Task<IList<ConversationAccount>> GetConversationMembersAsync(...) { ... } |
| 151 | public virtual async Task<T> GetConversationMemberAsync<T>(...) { ... } |
| 152 | // ... all methods are virtual |
| 153 | } |
| 154 | ``` |
| 155 | |
| 156 | ### `CoreActivity.cs` (Schema) |
| 157 | |
| 158 | The activity DTO — replaces `Activity`/`IActivity` from main: |
| 159 | |
| 160 | ```csharp |
| 161 | public class CoreActivity |
| 162 | { |
| 163 | [JsonPropertyName("type")] public string Type { get; set; } |
| 164 | [JsonPropertyName("channelId")] public string? ChannelId { get; set; } |
| 165 | [JsonPropertyName("id")] public string? Id { get; set; } |
| 166 | [JsonPropertyName("serviceUrl")] public Uri? ServiceUrl { get; set; } |
| 167 | [JsonPropertyName("channelData")] public ChannelData? ChannelData { get; set; } |
| 168 | [JsonPropertyName("from")] public ConversationAccount? From { get; set; } |
| 169 | [JsonPropertyName("recipient")] public ConversationAccount? Recipient { get; set; } |
| 170 | [JsonPropertyName("conversation")] public Conversation? Conversation { get; set; } |
| 171 | [JsonPropertyName("entities")] public JsonArray? Entities { get; set; } |
| 172 | [JsonPropertyName("attachments")] public JsonArray? Attachments { get; set; } |
| 173 | [JsonPropertyName("value")] public JsonNode? Value { get; set; } |
| 174 | [JsonPropertyName("replyToId")] public string? ReplyToId { get; set; } |
| 175 | [JsonExtensionData] public ExtendedPropertiesDictionary Properties { get; set; } = []; |
| 176 | |
| 177 | // AOT-compatible serialization |
| 178 | public virtual string ToJson() => JsonSerializer.Serialize(this, CoreActivityJsonContext.Default.CoreActivity); |
| 179 | public static CoreActivity FromJsonString(string json) => ...; |
| 180 | public static ValueTask<CoreActivity?> FromJsonStreamAsync(Stream stream, CancellationToken ct) => ...; |
| 181 | } |
| 182 | ``` |
| 183 | |
| 184 | ### `ConversationAccount.cs` (Schema) |
| 185 | |
| 186 | ```csharp |
| 187 | public class ConversationAccount |
| 188 | { |
| 189 | [JsonPropertyName("id")] public string? Id { get; set; } |
| 190 | [JsonPropertyName("name")] public string? Name { get; set; } |
| 191 | [JsonExtensionData] public ExtendedPropertiesDictionary Properties { get; set; } = []; |
| 192 | } |
| 193 | ``` |
| 194 | |
| 195 | ### `Conversation.cs` (Schema) |
| 196 | |
| 197 | ```csharp |
| 198 | public class Conversation |
| 199 | { |
| 200 | [JsonPropertyName("id")] public string Id { get; set; } = string.Empty; |
| 201 | [JsonExtensionData] public ExtendedPropertiesDictionary Properties { get; set; } = []; |
| 202 | } |
| 203 | ``` |
| 204 | |
| 205 | > **Note:** Core's `Conversation` has only `Id` + extension data. Main's `Conversation` has `Id`, `Type`, `Name`. This affects the `chat` wire format — see porting design doc. |
| 206 | |
| 207 | --- |
| 208 | |
| 209 | ## Layer 2: `Microsoft.Teams.Bot.Apps` |
| 210 | |
| 211 | Teams-specific application layer built on Core. |
| 212 | |
| 213 | ### `TeamsBotApplication.cs` |
| 214 | |
| 215 | Extends `BotApplication` with Teams routing: |
| 216 | |
| 217 | ```csharp |
| 218 | public class TeamsBotApplication : BotApplication |
| 219 | { |
| 220 | private readonly Router _router = new(); |
| 221 | |
| 222 | // Handler registration |
| 223 | public TeamsBotApplication OnMessage(Func<Context, CancellationToken, Task> handler) { ... } |
| 224 | public TeamsBotApplication OnInvoke(string name, Func<Context, CancellationToken, Task> handler) { ... } |
| 225 | // ... other handler types |
| 226 | } |
| 227 | ``` |
| 228 | |
| 229 | ### Hosting Extensions |
| 230 | |
| 231 | **Service registration:** |
| 232 | ```csharp |
| 233 | // AddTeamsBotApplication registers: |
| 234 | // - TeamsApiClient (with auth handler) |
| 235 | // - Then calls AddBotApplication<TeamsBotApplication>() which registers: |
| 236 | // - BotApplicationOptions |
| 237 | // - HttpContextAccessor |
| 238 | // - JWT auth + authorization |
| 239 | // - ConversationClient (with named HttpClient "BotConversationClient" + auth handler) |
| 240 | // - UserTokenClient |
| 241 | // - TeamsBotApplication as singleton |
| 242 | ``` |
| 243 | |
| 244 | **Endpoint mapping:** |
| 245 | ```csharp |
| 246 | public static TApp UseBotApplication<TApp>(this IEndpointRouteBuilder endpoints, string routePath = "api/messages") |
| 247 | where TApp : BotApplication |
| 248 | { |
| 249 | // Adds auth/authz middleware |
| 250 | if (endpoints is IApplicationBuilder app) |
| 251 | { |
| 252 | app.UseAuthentication(); |
| 253 | app.UseAuthorization(); |
| 254 | } |
| 255 | |
| 256 | TApp botApp = endpoints.ServiceProvider.GetService<TApp>() |
| 257 | ?? throw new InvalidOperationException("Application not registered"); |
| 258 | |
| 259 | // Maps POST endpoint that calls ProcessAsync |
| 260 | endpoints.MapPost(routePath, (HttpContext httpContext, CancellationToken cancellationToken) |
| 261 | => botApp.ProcessAsync(httpContext, cancellationToken) |
| 262 | ).RequireAuthorization(); |
| 263 | |
| 264 | return botApp; |
| 265 | } |
| 266 | ``` |
| 267 | |
| 268 | --- |
| 269 | |
| 270 | ## Typical Usage |
| 271 | |
| 272 | ```csharp |
| 273 | var builder = WebApplication.CreateSlimBuilder(args); |
| 274 | builder.Services.AddTeamsBotApplication(); |
| 275 | |
| 276 | var app = builder.Build(); |
| 277 | var teamsApp = app.UseTeamsBotApplication(); |
| 278 | |
| 279 | teamsApp.OnMessage(async (context, ct) => |
| 280 | { |
| 281 | await context.SendActivityAsync(new CoreActivity("message") { ... }, ct); |
| 282 | }); |
| 283 | |
| 284 | app.Run(); |
| 285 | ``` |
| 286 | |
| 287 | --- |
| 288 | |
| 289 | ## Summary: What DevTools Needs to Hook Into |
| 290 | |
| 291 | | Concern | Core mechanism | |
| 292 | |---------|---------------| |
| 293 | | Intercept incoming activities | `ITurnMiddleware` — registered via `botApp.UseMiddleware()` | |
| 294 | | Intercept outgoing activities | Subclass `ConversationClient` — override `virtual SendActivityAsync()` | |
| 295 | | Intercept errors | Middleware wraps `nextTurn()` in try/catch | |
| 296 | | Serve static files | `IApplicationBuilder` middleware / `IEndpointRouteBuilder` endpoints | |
| 297 | | WebSocket connections | `IApplicationBuilder.UseWebSockets()` + endpoint mapping | |
| 298 | | DI registration | `IServiceCollection` extensions | |
| 299 | | Configuration | `IConfiguration` binding | |
| 300 | |