Forge
markdowne8ad0934
1# Wissance WebApiToolkit — полный стек слоёв (fundamental → operational)
2
3Источник: [Wissance/WebApiToolkit](https://github.com/Wissance/WebApiToolkit), README и Wiki.
4
5## Назначение KB
6Эта KB-страница описывает **все слои**, которые нужны агентам, чтобы **проектировать, собирать, разворачивать и сопровождать** REST backend на базе WebApiToolkit.
7
8**База идеи** (из README): библиотека позволяет поднять CRUD REST API «почти одной строкой» за счёт:
9- автогенерации контроллеров (assembly on-the-fly)
10- вынесения логики в **Manager** слой
11- стандартизации ответов/ошибок и query-поведения
12
13README: https://raw.githubusercontent.com/Wissance/WebApiToolkit/master/README.md
14
15---
16
17## L0 — Fundamental (концептуальные основы)
18
19### L0.1 Architectural principle: Controller ≈ thin, Manager ≈ core
20- Контроллеры (REST/gRPC/SignalR) должны быть **тонкими адаптерами транспорта**.
21- Вся прикладная логика и работа с хранилищем концентрируется в **Manager**.
22
23README заявляет общий контракт через `IModelManager` и возможность переиспользовать один Manager для разных транспортов.
24
25### L0.2 Standardization principle
26Цель — **одинаковое поведение** у всех CRUD эндпоинтов:
27- единый формат ответа/ошибок
28- единые paging/sorting/filter правила
29- единые bulk правила
30
31(в README: “Output of all REST methods is standardize”, “Unified error format out of the box”, paging/sorting/filter, bulk)
32
33---
34
35## L1 — Library/Package layer (что подключать)
36
37### L1.1 Основные пакеты
38По README (NuGet бейджи):
39- `Wissance.WebApiToolkit.Core` — базовые контракты/контроллеры/менеджеры/утилиты
40- `Wissance.WebApiToolkit.Ef` — EF-ориентированная реализация и DI entrypoints
41- `Wissance.WebApiToolkit.AWS.S3` — S3 utilities (опционально)
42
43### L1.2 Target frameworks
44`Wissance.WebApiToolkit.Core` и `Wissance.WebApiToolkit.Ef` таргетят `net6.0;net8.0;net9.0`.
45
46---
47
48## L2 — Code-generation & Composition layer (как появляется контроллер)
49
50### L2.1 On-the-fly generation
51Контроллер генерируется в runtime (assembly) и подключается к MVC как application part.
52
53### L2.2 DI entrypoints (точки входа)
54Ключевые entrypoints (EF пакет):
55- `Wissance.WebApiToolkit.Ef.Extensions.ServiceCollectionExtensions.AddSimplifiedAutoController(...)`
56- `Wissance.WebApiToolkit.Ef.Extensions.ServiceCollectionExtensions.AddFullyConfiguredAutoController(...)`
57
58Обе функции делают 3 вещи:
59- **регистрируют scoped manager** (через `EfBasedManagerFactory`)
60- **генерируют controller assembly** (через `OnTheFlyServicesGenerator.GenerateController(...)`)
61- **возвращают Assembly**
62
63Источник кода: https://raw.githubusercontent.com/Wissance/WebApiToolkit/a1016f6631d3a7bcc76eb986ff6abcd1b0e5e91f/Wissance.WebApiToolkit/Wissance.WebApiToolkit.Ef/Extensions/ServiceCollectionExtensions.cs
64
65### L2.3 Подключение в ASP.NET Core
66Рекомендуемый шаблон (README):
671) вызвать `services.AddSimplifiedAutoController<...>(...)` → получить `Assembly`
682) `services.AddControllers().AddApplicationPart(assembly).AddControllersAsServices();`
69
70README: https://raw.githubusercontent.com/Wissance/WebApiToolkit/master/README.md
71
72---
73
74## L3 — Domain & Contract layer (что должен предоставить проект)
75
76### L3.1 Entity identity contract
77Для автоконтроллера есть generic constraints:
78- `TObj : class, IModelIdentifiable`
79- `TId : IComparable`
80
81Т.е. сущность должна реализовать контракт идентификации, а ключ быть сравнимым.
82
83Источник: `ServiceCollectionExtensions.cs` (см. L2.2).
84
85### L3.2 Resource naming contract
86Тебе нужен **стабильный `resourceName`** (строка), из которого строится имя сборки/контроллера и маршрутизация ресурса.
87
88---
89
90## L4 — Query semantics layer (paging/sorting/filter)
91
92### L4.1 Read filter contract
93Generic constraint:
94- `TFilter : class, IReadFilterable`
95
96---
97
98## L5 — Application/Manager layer (ядро приложения)
99
100### L5.1 Stable boundary
101Менеджер — это boundary между транспортом и persistence/business.
102
103README: “support to work with any persistent storage (`IModelManager` interface)”
104
105### L5.2 EF implementation path
106EF путь подразумевает:
107- `DbContext`
108- EF-based manager factory:
109 - `EfBasedManagerFactory.CreateSimplifiedManager(...)`
110 - `EfBasedManagerFactory.CreateFullyDefinedManager(...)`
111
112Источник: `ServiceCollectionExtensions.cs` (см. L2.2).
113
114### L5.3 Extensibility path (как агент расширяет поведение)
115Чтобы агент мог “строить приложения”, в KB фиксируем стандартные варианты расширения:
116- **Custom manager**: своя реализация `IModelManager` (или composition поверх EF менеджера)
117- **ManagerConfiguration**: использовать `AddFullyConfiguredAutoController(...)` и `ManagerConfiguration` для тонкой настройки поведения менеджера
118- **FilterFunc**: в simplified-варианте есть `filterFunc` для запрета/разрешения элементов (вход: объект; выход: bool)
119
120---
121
122## L6 — Persistence & Infrastructure layer
123
124### L6.1 EF DbContext
125Проект должен владеть:
126- конфигурацией `DbContext` (connection string, migrations, provider)
127- жизненным циклом/миграциями
128
129### L6.2 Storage portability
130Если нужна БД не EF — реализовать `IModelManager` для другого storage.
131
132---
133
134## L7 — API Surface layer (CRUD + Bulk)
135
136### L7.1 CRUD surface
137Контроллеры предоставляют `GET/POST/PUT/DELETE` в едином формате.
138
139### L7.2 Bulk surface
140README: bulk `Create/Update/Delete` на уровне контроллера и интерфейса.
141
142---
143
144## L8 — Cross-cutting layer (responses, errors, logging)
145
146### L8.1 Unified response & error model
147README заявляет унификацию ответов и ошибок.
148
149---
150
151## L9 — Security layer (обязательное поверх автогенерации)
152Автогенерация CRUD **не** закрывает требования безопасности.
153
154---
155
156## L10 — Testing & Quality layer
157
158---
159
160## L11 — Delivery layer (build/release)
161
162---
163
164## L12 — Operational layer (deploy, run, support)
165
166---
167
168## L13 — Agent-facing “recipe” (что агент должен уметь сделать автоматически)
169
170---
171
172## API Contract (exact)
173Это точная спецификация, выведенная из базовых контроллеров WebApiToolkit.
174
175### Base routes
176- **CRUD/read** контроллеры: `api/[controller]`
177 - задаётся атрибутом `[Route("api/[controller]")]`
178 - источник: `Wissance.WebApiToolkit.Core.Controllers.BasicReadController` и наследники
179
180Источник: https://raw.githubusercontent.com/Wissance/WebApiToolkit/a1016f6631d3a7bcc76eb986ff6abcd1b0e5e91f/Wissance.WebApiToolkit/Wissance.WebApiToolkit.Core/Controllers/BasicReadController.cs
181
182- **Bulk** контроллеры: `api/bulk/[controller]`
183 - задаётся атрибутом `[Route("api/bulk/[controller]")]`
184 - источник: `Wissance.WebApiToolkit.Core.Controllers.BasicBulkCrudController`
185
186Источник: https://raw.githubusercontent.com/Wissance/WebApiToolkit/a1016f6631d3a7bcc76eb986ff6abcd1b0e5e91f/Wissance.WebApiToolkit/Wissance.WebApiToolkit.Core/Controllers/BasicBulkCrudController.cs
187
188---
189
190### Endpoints: Read (list)
191`GET /api/{ControllerName}`
192
193**Query parameters**:
194- `page` (int?, default = 1, min = 1)
195- `size` (int?, default = 25, min = 1)
196- `sort` (string) — имя поля/свойства для сортировки
197- `order` (string) — `asc` или `desc` (любой другой ввод → трактуется как `asc`)
198- `additionalFilters` — параметры фильтрации, собираемые через `TFilter : IReadFilterable` → `IDictionary`
199
200Факты:
201- значения по умолчанию: `page=1`, `size=25` (см. `PagingUtils`)
202- порядок сортировки: если `order` пустой/неизвестный → `Ascending` (см. `SortOption`)
203
204Источники:
205- `BasicReadController.ReadAsync(...)`: https://raw.githubusercontent.com/Wissance/WebApiToolkit/a1016f6631d3a7bcc76eb986ff6abcd1b0e5e91f/Wissance.WebApiToolkit/Wissance.WebApiToolkit.Core/Controllers/BasicReadController.cs
206- `PagingUtils`: https://raw.githubusercontent.com/Wissance/WebApiToolkit/a1016f6631d3a7bcc76eb986ff6abcd1b0e5e91f/Wissance.WebApiToolkit/Wissance.WebApiToolkit.Core/Utils/PagingUtils.cs
207- `SortOption`: https://raw.githubusercontent.com/Wissance/WebApiToolkit/a1016f6631d3a7bcc76eb986ff6abcd1b0e5e91f/Wissance.WebApiToolkit/Wissance.WebApiToolkit.Core/Data/SortOption.cs
208- `IReadFilterable`: https://raw.githubusercontent.com/Wissance/WebApiToolkit/a1016f6631d3a7bcc76eb986ff6abcd1b0e5e91f/Wissance.WebApiToolkit/Wissance.WebApiToolkit.Core/Data/IReadFilterable.cs
209- `EmptyAdditionalFilters`: https://raw.githubusercontent.com/Wissance/WebApiToolkit/a1016f6631d3a7bcc76eb986ff6abcd1b0e5e91f/Wissance.WebApiToolkit/Wissance.WebApiToolkit.Core/Data/EmptyAdditionalFilters.cs
210
211**Response body envelope**: `OperationResultDto<PagedDataDto<TRes>>`
212- `OperationResultDto<T>` JSON содержит:
213 - `Success: bool`
214 - `Message: string`
215 - `Data: T`
216- поле `Status: int` в JSON **не сериализуется** (помечено `[JsonIgnore]`), но HTTP status код выставляется в `HttpContext.Response.StatusCode = result.Status`
217
218Источники:
219- `BasicReadController` (выставление status + возвращаемое тело): https://raw.githubusercontent.com/Wissance/WebApiToolkit/a1016f6631d3a7bcc76eb986ff6abcd1b0e5e91f/Wissance.WebApiToolkit/Wissance.WebApiToolkit.Core/Controllers/BasicReadController.cs
220- `OperationResultDto`: https://raw.githubusercontent.com/Wissance/WebApiToolkit/a1016f6631d3a7bcc76eb986ff6abcd1b0e5e91f/Wissance.WebApiToolkit/Wissance.WebApiToolkit.Dto/OperationResultDto.cs
221- `PagedDataDto`: https://raw.githubusercontent.com/Wissance/WebApiToolkit/a1016f6631d3a7bcc76eb986ff6abcd1b0e5e91f/Wissance.WebApiToolkit/Wissance.WebApiToolkit.Dto/PagedDataDto.cs
222
223**`PagedDataDto<T>` поля**:
224- `Page: long`
225- `Total: long`
226- `Pages: long`
227- `Data: IList<T>`
228
229---
230
231### Endpoints: Read (by id)
232`GET /api/{ControllerName}/{id}`
233
234- `{id}` берётся из route (`[HttpGet("{id}")]`)
235- Response: `OperationResultDto<TRes>`
236- HTTP status выставляется по `result.Status`
237
238Источник: `BasicReadController.ReadByIdAsync(...)`: https://raw.githubusercontent.com/Wissance/WebApiToolkit/a1016f6631d3a7bcc76eb986ff6abcd1b0e5e91f/Wissance.WebApiToolkit/Wissance.WebApiToolkit.Core/Controllers/BasicReadController.cs
239
240---
241
242### Endpoints: CRUD
243Базовые mutating-операции реализованы в `BasicCrudController` (на базе `api/[controller]`).
244
245- `POST /api/{ControllerName}`
246 - body: `TRes`
247 - response: `OperationResultDto<TRes>`
248
249- `PUT /api/{ControllerName}/{id}`
250 - body: `TRes`
251 - response: `OperationResultDto<TRes>`
252
253- `DELETE /api/{ControllerName}/{id}`
254 - response body: отсутствует (метод возвращает `Task`, но выставляет `HttpContext.Response.StatusCode`)
255
256Источник: `BasicCrudController`: https://raw.githubusercontent.com/Wissance/WebApiToolkit/a1016f6631d3a7bcc76eb986ff6abcd1b0e5e91f/Wissance.WebApiToolkit/Wissance.WebApiToolkit.Core/Controllers/BasicCrudController.cs
257
258---
259
260### Endpoints: Bulk
261Базовые bulk-операции реализованы в `BasicBulkCrudController` (на базе `api/bulk/[controller]`).
262
263- `POST /api/bulk/{ControllerName}`
264 - body: `TRes[]`
265 - response: `OperationResultDto<TRes[]>`
266
267- `PUT /api/bulk/{ControllerName}`
268 - body: `TRes[]`
269 - response: `OperationResultDto<TRes[]>`
270
271- `DELETE /api/bulk/{ControllerName}?id={id1}&id={id2}...`
272 - query: `id: TId[]`
273 - response body: отсутствует (метод возвращает `Task`, но выставляет status)
274
275Источник: `BasicBulkCrudController`: https://raw.githubusercontent.com/Wissance/WebApiToolkit/a1016f6631d3a7bcc76eb986ff6abcd1b0e5e91f/Wissance.WebApiToolkit/Wissance.WebApiToolkit.Core/Controllers/BasicBulkCrudController.cs
276
277---
278
279### Manager contract (exact)
280Контроллеры вызывают `IModelManager<TRes, TObj, TId>`.
281
282Методы:
283- `CreateAsync(TRes)` → `OperationResultDto<TRes>`
284- `BulkCreateAsync(TRes[])` → `OperationResultDto<TRes[]>`
285- `UpdateAsync(TId, TRes)` → `OperationResultDto<TRes>`
286- `BulkUpdateAsync(TRes[])` → `OperationResultDto<TRes[]>`
287- `DeleteAsync(TId)` → `OperationResultDto<bool>`
288- `BulkDeleteAsync(TId[])` → `OperationResultDto<bool>`
289- `GetAsync(page,size,sorting,parameters)` → `OperationResultDto<Tuple<IList<TRes>, long>>`
290- `GetByIdAsync(TId)` → `OperationResultDto<TRes>`
291
292Источник: `IModelManager`: https://raw.githubusercontent.com/Wissance/WebApiToolkit/a1016f6631d3a7bcc76eb986ff6abcd1b0e5e91f/Wissance.WebApiToolkit/Wissance.WebApiToolkit.Core/Managers/IModelManager.cs
293
294---
295
296### Operational notes (важно для агентского генератора)
297- **HTTP status** берётся из `OperationResultDto.Status`, но **в JSON его нет** → клиентам нельзя полагаться на наличие `status` в теле.
298- Для `DELETE` (и обычного, и bulk) контроллер возвращает `Task`/пустой body → клиенты должны смотреть на HTTP status.
299- `sort` поддерживает **одну колонку** (как минимум на уровне контракта `SortOption`).
300- `order` принимает только `asc|desc` (case-insensitive) → иначе `asc`.
301- `page/size` не могут выключить пагинацию: “no option to receive all data without paging” (комментарий в `BasicReadController`).
302
303---
304
305## Пример wiring (из TestApp)
306В TestApp показан вариант с `AddFullyConfiguredAutoController(...)` + `ManagerConfiguration`.
307
308Источник: https://raw.githubusercontent.com/Wissance/WebApiToolkit/a1016f6631d3a7bcc76eb986ff6abcd1b0e5e91f/Wissance.WebApiToolkit/Wissance.WebApiToolkit.TestApp/Startup.cs
309
310---
311
312## Ссылки
313- Репо: https://github.com/Wissance/WebApiToolkit
314- README: https://raw.githubusercontent.com/Wissance/WebApiToolkit/master/README.md
315- Wiki: https://github.com/Wissance/WebApiToolkit/wiki
316- DI entrypoints (EF): https://raw.githubusercontent.com/Wissance/WebApiToolkit/a1016f6631d3a7bcc76eb986ff6abcd1b0e5e91f/Wissance.WebApiToolkit/Wissance.WebApiToolkit.Ef/Extensions/ServiceCollectionExtensions.cs
317## TFilter template (exact pattern for agents)
318Этот раздел нужен, чтобы агент мог **автоматически генерировать фильтры** для `GET /api/{Controller}`.
319
320### Факт из WebApiToolkit
321`BasicReadController.ReadAsync(...)` получает `TFilter additionalFilters` и делает:
322- `additionalFilters.SelectFilters()` → `IDictionary additionalQueryParams`
323- передаёт это в `Manager.GetAsync(..., parameters: additionalQueryParams)`
324
325Источник: `BasicReadController.ReadAsync(...)`: https://raw.githubusercontent.com/Wissance/WebApiToolkit/a1016f6631d3a7bcc76eb986ff6abcd1b0e5e91f/Wissance.WebApiToolkit/Wissance.WebApiToolkit.Core/Controllers/BasicReadController.cs
326
327Контракт фильтра:
328- `TFilter : IReadFilterable`
329- `IReadFilterable.SelectFilters(): IDictionary`
330
331Источник: `IReadFilterable`: https://raw.githubusercontent.com/Wissance/WebApiToolkit/a1016f6631d3a7bcc76eb986ff6abcd1b0e5e91f/Wissance.WebApiToolkit/Wissance.WebApiToolkit.Core/Data/IReadFilterable.cs
332
333---
334
335### Правила для агентов (обязательные)
336- **Правило 1 — ключи словаря**: ключи в `IDictionary` должны быть **стабильными** и совпадать с тем, что ожидает реализация `IModelManager.GetAsync(..., parameters)`.
337 - По умолчанию удобно использовать те же имена, что и в query (`from`, `to`, `status`, …).
338- **Правило 2 — не класть null/пустое**: `SelectFilters()` должен исключать `null`, пустые строки, пустые коллекции.
339- **Правило 3 — типы значений**: клади в словарь значения в типах, которые менеджер реально умеет обработать (обычно `string`, `int`, `bool`, `DateTime`, arrays).
340- **Правило 4 — имена query-параметров**: если нужно фиксированное имя в URL, используй `[FromQuery(Name = "...")]` на свойстве.
341
342---
343
344### Шаблон `TFilter` (рекомендуемый)
345Пример фильтра, который агент может генерировать “по умолчанию”:
346
347```csharp
348using System;
349using System.Collections;
350using System.Collections.Generic;
351using Microsoft.AspNetCore.Mvc;
352using Wissance.WebApiToolkit.Core.Data;
353
354public sealed class MyResourceFilters : IReadFilterable
355{
356 // Пример диапазона дат: ?from=2022-01-01&to=2024-12-31
357 [FromQuery(Name = "from")]
358 public DateTime? From { get; init; }
359
360 [FromQuery(Name = "to")]
361 public DateTime? To { get; init; }
362
363 // Пример точного совпадения: ?status=Active
364 [FromQuery(Name = "status")]
365 public string? Status { get; init; }
366
367 // Пример числового фильтра: ?minScore=10
368 [FromQuery(Name = "minScore")]
369 public int? MinScore { get; init; }
370
371 // Пример мульти-значения: ?id=1&id=2&id=3
372 [FromQuery(Name = "id")]
373 public int[]? Id { get; init; }
374
375 public IDictionary SelectFilters()
376 {
377 var dict = new Dictionary<string, object>();
378
379 if (From is not null) dict["from"] = From.Value;
380 if (To is not null) dict["to"] = To.Value;
381
382 if (!string.IsNullOrWhiteSpace(Status)) dict["status"] = Status!;
383 if (MinScore is not null) dict["minScore"] = MinScore.Value;
384
385 if (Id is { Length: > 0 }) dict["id"] = Id;
386
387 return dict;
388 }
389}
390```
391
392### Как агенту связать `TFilter` с контроллером
393- Для автогенерации через `AddSimplifiedAutoController<..., TFilter>` / `AddFullyConfiguredAutoController<..., TFilter>` — передай этот тип `TFilter` как generic параметр.
394- Для ручных контроллеров на базе `BasicReadController<TRes, TObj, TId, TFilter>` — укажи `TFilter` в generic параметрах.
395
396### Пустой фильтр
397Если фильтров нет — используй `EmptyAdditionalFilters`.
398
399Источник: https://raw.githubusercontent.com/Wissance/WebApiToolkit/a1016f6631d3a7bcc76eb986ff6abcd1b0e5e91f/Wissance.WebApiToolkit/Wissance.WebApiToolkit.Core/Data/EmptyAdditionalFilters.cs
400
View only · write via MCP/CIDE