microsoft/teams.net

Public

mirrored fromhttps://github.com/microsoft/teams.netAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
devtools-port-no-auth

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

devtools-plan/01-devtools-architecture.md

375lines · modecode

1# DevTools Architecture on `main`
2
3How the DevTools plugin works in the current `main` branch of the .NET SDK.
4
5## Project
6
7```
8Libraries/Microsoft.Teams.Plugins/Microsoft.Teams.Plugins.AspNetCore.DevTools/
9```
10
11Package: `Microsoft.Teams.Plugins.AspNetCore.DevTools`
12Target: `net8.0`
13Embedded UI: React/TypeScript app in `web/` folder, served via `ManifestEmbeddedFileProvider`
14
15### Dependencies
16
17- `Microsoft.Teams.Plugins.AspNetCore` (plugin host)
18- `Microsoft.Teams.Apps` (app model, plugin interfaces)
19- `Microsoft.Teams.Api` (activity types)
20- `Microsoft.Teams.Common` (logging, JSON utilities)
21- `Microsoft.Teams.Extensions.Hosting`
22- `Microsoft.Extensions.FileProviders.Embedded` (9.0.0)
23- `System.IdentityModel.Tokens.Jwt` (8.8.0)
24
25---
26
27## Plugin Class — `DevToolsPlugin.cs`
28
29Implements `IAspNetCorePlugin`, decorated with `[Plugin]`.
30
31```csharp
32[Plugin]
33public class DevToolsPlugin : IAspNetCorePlugin
34{
35 [Dependency] public ILogger Logger { get; set; }
36 [Dependency("AppId", optional: true)] public string? AppId { get; set; }
37 [Dependency("AppName", optional: true)] public string? AppName { get; set; }
38
39 public event EventFunction Events;
40
41 internal MetaData MetaData => new() { Id = AppId, Name = AppName, Pages = _pages };
42 internal readonly WebSocketCollection Sockets = [];
43
44 private readonly ISenderPlugin _sender;
45 private readonly IServiceProvider _services;
46 private readonly IList<Page> _pages = [];
47 private readonly TeamsDevToolsSettings _settings;
48
49 public DevToolsPlugin(AspNetCorePlugin sender, IServiceProvider provider) { ... }
50```
51
52### Lifecycle Methods
53
54| Method | What it does |
55|--------|-------------|
56| `Configure(IApplicationBuilder)` | Enables WebSockets (`AllowedOrigins = { "*" }`), serves embedded static files at `/devtools` path, adds error-logging middleware |
57| `OnInit(App)` | Loads custom pages from `TeamsDevToolsSettings`, logs security warning |
58| `OnStart(App)` | Resolves `IServer` addresses, logs `Available at {address}/devtools` for each |
59| `OnActivity(App, ISenderPlugin, ActivityEvent)` | Emits `ActivityEvent.Received(activity, conversation)` to all WebSocket clients |
60| `OnActivitySent(App, ISenderPlugin, ActivitySentEvent)` | Emits `ActivityEvent.Sent(activity, conversation)` to all WebSocket clients |
61| `OnActivityResponse(...)` | No-op (logs debug) |
62| `OnError(...)` | No-op (logs debug) |
63| `Do(ActivityEvent)` | Delegates to `AspNetCorePlugin` sender — used by `ActivityController` for test injection |
64
65---
66
67## Controllers
68
69### `DevToolsController.cs` — UI + WebSocket
70
71```csharp
72[ApiController]
73public class DevToolsController : ControllerBase
74{
75 private readonly DevToolsPlugin _plugin;
76 private readonly IFileProvider _files;
77 private readonly IHostApplicationLifetime _lifetime;
78
79 public DevToolsController(DevToolsPlugin plugin, IHostApplicationLifetime lifetime) { ... }
80```
81
82**Endpoints:**
83
84| Route | Method | Behavior |
85|-------|--------|----------|
86| `GET /devtools` | `Get(null)` | Serves `index.html` from embedded files |
87| `GET /devtools/{*path}` | `Get(path)` | Serves requested file; falls back to `index.html` (SPA routing) |
88| `GET /devtools/sockets` | `GetSocket()` | WebSocket upgrade → adds to `Sockets` collection → sends `MetaDataEvent` → loops until close |
89
90**WebSocket lifecycle:**
911. Accept WebSocket connection
922. Assign GUID id, add to `_plugin.Sockets`
933. Send `MetaDataEvent` with app id, name, and custom pages
944. Block on `socket.ReceiveAsync()` until socket closes
955. Remove from `_plugin.Sockets` on disconnect
96
97### `ActivityController.cs` — Test Activity Injection
98
99```csharp
100[ApiController]
101[Obsolete("Use Minimal APIs instead.")]
102public class ActivityController : ControllerBase
103{
104 private readonly DevToolsPlugin _plugin;
105 private readonly SecurityKey _securityKey;
106```
107
108**Endpoint:** `POST /v3/conversations/{conversationId}/activities`
109
110**Logic:**
1111. Check for `x-teams-devtools: true` header
1122. If **not** from DevTools client: return `201` with `{ id }` (passthrough for outgoing activities from `ConversationClient`)
1133. If **from** DevTools client:
114 - Set `from` to `{ id: "devtools", name: "devtools", role: "user" }`
115 - Set `conversation` to `{ id: conversationId, type: "personal", name: "default" }`
116 - Set `recipient` to `{ id: appId, name: appName, role: "bot" }`
117 - Deserialize to `Activity`
118 - Create fake JWT with `serviceurl` claim pointing at localhost
119 - Call `_plugin.Do(activityEvent)` — runs through the full sender pipeline
1204. Return `201` with `{ id }`
121
122---
123
124## Event System
125
126### `IEvent` Interface (`Event.cs`)
127
128```csharp
129[TrueTypeJson<IEvent>]
130public interface IEvent
131{
132 public Guid Id { get; }
133 public string Type { get; }
134 public object? Body { get; }
135 public DateTime SentAt { get; }
136}
137```
138
139The `[TrueTypeJson]` attribute enables polymorphic JSON serialization — the serializer writes the concrete type's properties, not just the interface.
140
141### `ActivityEvent.cs`
142
143```csharp
144public class ActivityEvent : IEvent
145{
146 [JsonPropertyName("id")] public Guid Id { get; }
147 [JsonPropertyName("type")] public string Type { get; }
148 [JsonPropertyName("body")] public object? Body { get; }
149 [JsonPropertyName("chat")] public Conversation Chat { get; set; }
150 [JsonPropertyName("error")] public object? Error { get; set; }
151 [JsonPropertyName("sentAt")] public DateTime SentAt { get; }
152
153 public ActivityEvent(string type, IActivity body, Conversation chat)
154 {
155 Id = Guid.NewGuid();
156 Type = $"activity.{type}"; // → "activity.received", "activity.sent", "activity.error"
157 Body = body;
158 Chat = chat;
159 SentAt = DateTime.Now;
160 }
161
162 public static ActivityEvent Received(IActivity body, Conversation chat) => new("received", body, chat);
163 public static ActivityEvent Sent(IActivity body, Conversation chat) => new("sent", body, chat);
164 public static ActivityEvent Err(IActivity body, Conversation chat, object error) => new("error", body, chat) { Error = error };
165}
166```
167
168### `MetaDataEvent.cs`
169
170```csharp
171public class MetaDataEvent : IEvent
172{
173 [JsonPropertyName("id")] public Guid Id { get; }
174 [JsonPropertyName("type")] public string Type { get; } // always "metadata"
175 [JsonPropertyName("body")] public object? Body { get; } // MetaData object
176 [JsonPropertyName("sentAt")] public DateTime SentAt { get; }
177
178 public MetaDataEvent(MetaData body) { ... }
179}
180```
181
182---
183
184## Wire Format (Critical for React UI)
185
186The embedded React app expects these exact JSON shapes over WebSocket:
187
188### Activity events
189
190```json
191{
192 "id": "d290f1ee-6c54-4b01-90e6-d701748f0851",
193 "type": "activity.received",
194 "body": { /* full Activity object */ },
195 "chat": {
196 "id": "conversation-id",
197 "type": "personal",
198 "name": "default"
199 },
200 "sentAt": "2026-03-18T10:30:00"
201}
202```
203
204`type` values: `"activity.received"`, `"activity.sent"`, `"activity.error"`
205
206Error events additionally include:
207```json
208{
209 "error": { /* error object */ }
210}
211```
212
213### Metadata events
214
215```json
216{
217 "id": "guid",
218 "type": "metadata",
219 "body": {
220 "id": "app-id",
221 "name": "app-name",
222 "pages": [
223 { "icon": "...", "name": "...", "displayName": "...", "url": "..." }
224 ]
225 },
226 "sentAt": "2026-03-18T10:30:00"
227}
228```
229
230### Key detail: `chat` property
231
232The `chat` property in `ActivityEvent` maps to `Microsoft.Teams.Api.Conversation` on main, which has properties `Id`, `Type` (enum: `Personal`, `Group`, `Channel`), and `Name`. The React UI reads `chat.id`, `chat.type`, and `chat.name`.
233
234---
235
236## WebSocket Management — `WebSocketCollection.cs`
237
238```csharp
239public class WebSocketCollection : IEnumerable<KeyValuePair<string, WebSocket>>
240{
241 protected IDictionary<string, WebSocket> _store = new Dictionary<string, WebSocket>();
242
243 public WebSocket? Get(string key) { ... }
244 public WebSocketCollection Add(string key, WebSocket value) { ... }
245 public WebSocketCollection Remove(params string[] keys) { ... }
246
247 // Broadcast to ALL connected clients
248 public async Task Emit(IEvent @event, CancellationToken ct)
249 {
250 var payload = JsonSerializer.SerializeToUtf8Bytes(@event, new JsonSerializerOptions()
251 {
252 DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
253 });
254 var buffer = new ArraySegment<byte>(payload, 0, payload.Length);
255 foreach (var socket in _store.Values)
256 await socket.SendAsync(buffer, WebSocketMessageType.Text, true, ct);
257 }
258
259 // Send to a SINGLE client by id
260 public async Task Emit(string key, IEvent @event, CancellationToken ct) { ... }
261}
262```
263
264---
265
266## Models
267
268### `MetaData.cs`
269
270```csharp
271public class MetaData
272{
273 [JsonPropertyName("id")] public string? Id { get; set; }
274 [JsonPropertyName("name")] public string? Name { get; set; }
275 [JsonPropertyName("pages")] public IList<Page> Pages { get; set; } = [];
276}
277```
278
279### `Page.cs`
280
281```csharp
282public class Page
283{
284 [JsonPropertyName("icon")] public string? Icon { get; set; }
285 [JsonPropertyName("name")] public required string Name { get; set; }
286 [JsonPropertyName("displayName")] public required string DisplayName { get; set; }
287 [JsonPropertyName("url")] public required string Url { get; set; }
288}
289```
290
291---
292
293## Registration — `HostApplicationBuilder.cs`
294
295```csharp
296public static class HostApplicationBuilderExtensions
297{
298 public static IHostApplicationBuilder AddTeamsDevTools(this IHostApplicationBuilder builder)
299 {
300 builder.Services.AddSingleton(builder.Configuration.GetTeamsDevTools());
301 builder.Services.AddTeamsPlugin<DevToolsPlugin>();
302 builder.Services.AddControllers().AddApplicationPart(Assembly.GetExecutingAssembly());
303 return builder;
304 }
305}
306```
307
308Configuration binding via `ConfigurationManager.cs`:
309```csharp
310public static TeamsDevToolsSettings GetTeamsDevTools(this IConfigurationManager manager)
311{
312 return manager.GetSection("Teams").GetSection("Plugins.DevTools").Get<TeamsDevToolsSettings>() ?? new();
313}
314```
315
316Settings POCO:
317```csharp
318public class TeamsDevToolsSettings
319{
320 public IList<Page> Pages { get; set; } = [];
321}
322```
323
324---
325
326## Extension Helpers
327
328### `WebSocket.cs`
329
330```csharp
331public static class WebSocketExtensions
332{
333 public static bool IsCloseable(this WebSocket socket)
334 {
335 return socket.State != WebSocketState.Closed &&
336 socket.State != WebSocketState.Aborted;
337 }
338}
339```
340
341---
342
343## Usage (main branch)
344
345```csharp
346var builder = WebApplication.CreateBuilder(args);
347builder.AddTeams();
348builder.AddTeamsDevTools(); // registers plugin + settings + MVC controllers
349
350var app = builder.Build();
351app.MapControllers(); // needed for DevTools MVC controllers
352// ... plugin lifecycle handled automatically by Teams app
353```
354
355---
356
357## File Inventory
358
359| File | Type | SDK Dependencies |
360|------|------|-----------------|
361| `DevToolsPlugin.cs` | Core plugin | `IAspNetCorePlugin`, `ISenderPlugin`, `[Plugin]`, `[Dependency]`, `App` |
362| `Controllers/DevToolsController.cs` | MVC controller | `DevToolsPlugin` (injected) |
363| `Controllers/ActivityController.cs` | MVC controller | `DevToolsPlugin`, `Activity`, `Conversation`, `Account`, `Role` |
364| `Events/ActivityEvent.cs` | Event DTO | `IActivity`, `Conversation` (from `Microsoft.Teams.Api`) |
365| `Events/MetaDataEvent.cs` | Event DTO | None (uses `MetaData` model) |
366| `Event.cs` (IEvent) | Interface | `[TrueTypeJson]` from `Microsoft.Teams.Common` |
367| `WebSocketCollection.cs` | Infrastructure | `IEvent` interface only |
368| `Models/Page.cs` | POCO | None |
369| `Models/MetaData.cs` | POCO | `Page` |
370| `Extensions/HostApplicationBuilder.cs` | DI registration | `AddTeamsPlugin<T>()` from `Microsoft.Teams.Apps.Extensions` |
371| `Extensions/ConfigurationManager.cs` | Config binding | None |
372| `Extensions/WebSocket.cs` | Helper | None |
373| `TeamsDevToolsSettings.cs` | Config POCO | `Page` |
374| `Microsoft.Teams.Plugins.AspNetCore.DevTools.csproj` | Project file | References 4 SDK projects |
375| `web/` | Embedded React UI | None (framework-agnostic) |
376