Ava3v.Abstractions (0.3.0)
Installation
dotnet nuget add source --name Administrator --username your_username --password your_token https://git.ava3v.ru/api/packages/Administrator/nuget/index.jsondotnet add package --source Administrator --version 0.3.0 Ava3v.AbstractionsAbout this package
Базовые контракты ava3v (ноль зависимостей): интерфейсы сущностей (IHasId/IHasParentId/IHasChildrenInfo и др.), EntityBase, DTO, перечисления (Enumeration/UserKindEnum), контексты, права (Security), страничный ответ PagedResponse, паджинг-запросы (IPagedQuery/IHasSearch/ITreePagedQuery), ошибки бизнес-логики. Извлечено из WareHub.
Ava3vLibs
Монорепозиторий общих .NET-библиотек ava3v.
- Общая версия пакетов —
<Version>вDirectory.Build.props(одна на всю монорепу). - Версии внешних зависимостей — в
Directory.Packages.props(Central Package Management), меняются в одном месте. - Приватный NuGet-фид:
https://git.ava3v.com/api/packages/Administrator/nuget.
Пакеты (зависимость строго вниз)
| Пакет | Зависит от | Назначение |
|---|---|---|
| Ava3v.Abstractions | — | Базовые контракты (ноль зависимостей): интерфейсы сущностей (IHasId/IHasParentId/IHasChildrenInfo и др.), EntityBase, DTO, Enumeration/UserKindEnum, контексты, Security, PagedResponse, паджинг-запросы (IPagedQuery/IHasSearch/ITreePagedQuery), ошибки бизнес-логики. |
| Ava3v.Components.Contract | Abstractions (+EFCore) | Грид-лоадер (фильтры/сортировка/поиск/уникальные значения по IQueryable) + построитель древовидного реестра. |
| Ava3v.WebApi.Core | Abstractions (+MediatR/AspNetCore/…) | minimal-API → MediatR (CQRS): билдеры (GET/POST/PUT/DELETE/file/form-data), middleware, локализация, хендлер-базы, JWT. |
| Ava3v.DataAccess | Abstractions (+EFCore.Relational) | Базовый DbContext (аудит/soft-delete/период действия), snake_case-конфиг сущностей, query-фильтры. |
Публикация
Токен Gitea (personal access token с правом write:package) держи у себя — в репо не коммитим.
Перед публикацией новой версии подними <Version> в Directory.Build.props (одну и ту же версию Gitea второй раз не примет; --skip-duplicate это проглотит).
Всё сразу (обычный путь)
cd ~/Ava3vLibs
rm -rf artifacts
dotnet pack Ava3vLibs.slnx -c Release -o artifacts
dotnet nuget push "artifacts/*.nupkg" \
--source https://git.ava3v.com/api/packages/Administrator/nuget \
--api-key <твой-gitea-токен> \
--skip-duplicate
По отдельности
Порядок важен, только если фид пустой:
Ava3v.Abstractionsпубликуй первым — остальные от него зависят.dotnet packлюбого проекта складывает.nupkgв общийartifacts/.
Общий шаблон:
cd ~/Ava3vLibs
PROJ=Ava3v.Abstractions # или Ava3v.Components.Contract / Ava3v.WebApi.Core / Ava3v.DataAccess
dotnet pack src/$PROJ/$PROJ.csproj -c Release -o artifacts
dotnet nuget push "artifacts/$PROJ.*.nupkg" \
--source https://git.ava3v.com/api/packages/Administrator/nuget \
--api-key <твой-gitea-токен> \
--skip-duplicate
Конкретно по каждому:
1. Ava3v.Abstractions (сначала)
dotnet pack src/Ava3v.Abstractions/Ava3v.Abstractions.csproj -c Release -o artifacts
dotnet nuget push "artifacts/Ava3v.Abstractions.*.nupkg" --source https://git.ava3v.com/api/packages/Administrator/nuget --api-key <токен> --skip-duplicate
2. Ava3v.Components.Contract
dotnet pack src/Ava3v.Components.Contract/Ava3v.Components.Contract.csproj -c Release -o artifacts
dotnet nuget push "artifacts/Ava3v.Components.Contract.*.nupkg" --source https://git.ava3v.com/api/packages/Administrator/nuget --api-key <токен> --skip-duplicate
3. Ava3v.WebApi.Core
dotnet pack src/Ava3v.WebApi.Core/Ava3v.WebApi.Core.csproj -c Release -o artifacts
dotnet nuget push "artifacts/Ava3v.WebApi.Core.*.nupkg" --source https://git.ava3v.com/api/packages/Administrator/nuget --api-key <токен> --skip-duplicate
4. Ava3v.DataAccess
dotnet pack src/Ava3v.DataAccess/Ava3v.DataAccess.csproj -c Release -o artifacts
dotnet nuget push "artifacts/Ava3v.DataAccess.*.nupkg" --source https://git.ava3v.com/api/packages/Administrator/nuget --api-key <токен> --skip-duplicate
Закоммитить исходники
git add -A
git commit -m "Ava3vLibs <version> — <что изменилось>"
git push
Как это потребляют другие проекты
В nuget.config потребителя добавить источник (один раз):
<packageSources>
<add key="ava3v" value="https://git.ava3v.com/api/packages/Administrator/nuget/index.json" />
</packageSources>
Затем нужные пакеты (транзитивно тянут Ava3v.Abstractions):
<PackageReference Include="Ava3v.WebApi.Core" Version="0.2.0" />
<PackageReference Include="Ava3v.Components.Contract" Version="0.2.0" />
<PackageReference Include="Ava3v.DataAccess" Version="0.2.0" />
Массовые операции по выделению грида (Ava3v.Components.Contract 0.2.33+)
Грид с 0.1.158 умеет «выделить всё по фильтру»: на сервер приходит не список ключей, а
GridSelectionRequest — флаг AllMatchingSelected, ExcludedKeys и Filter (тело
getBackendRequest() грида) либо SelectedKeys для явного выделения. Разрешение в запрос
и предохранитель — GridSelectionResolver:
// Команда: { GridSelectionRequest Selection; ... patch ... }
var rows = await db.Items.ResolveAsync(command.Selection, x => x.Id, maxAffectedRows: 10_000, ct);
// > лимита — BusinessLogicException с Code = "BulkLimitExceeded" ДО изменения данных (COUNT до лимита + 1)
await rows.ExecuteUpdateAsync(s => s.SetProperty(x => x.Status, command.Status), ct);
ApplySelection — только запрос без проверки, EnsureWithinLimitAsync — только проверка.
Лимит держите согласованным с фронтовым selectAllMatchingLimit (по умолчанию 10 000).
Заметки к выпуску
0.3.0 — ЛОМАЮЩЕЕ: авторство при добавлении
Ava3vDbContextBase:CreatedByпри добавлении ВСЕГДА берётся из текущего пользователя, заданное значение (в том числе прилетевшее из DTO через маппер) игнорируется. Раньше заполненныйCreatedByне трогался — клиент мог записать запись от чужого имени.- Сохранить исходного автора можно только из кода, явным режимом с областью действия:
using (context.PreserveAuthorship()) { … await context.SaveChangesAsync(); }— для сидеров, генераторов демо-данных, переноса и синхронизации данных. В режимеCreatedBy = 0всё равно заменяется текущим пользователем;UpdatedBy/DeletedByрежим не затрагивает; автор записи журнала аудита — всегда тот, кто выполняет операцию.CreatedAtэто решение не трогает. - Что проверить у себя: все места, где
CreatedByзадаётся явно (сидеры, импорты, DevData, генераторы, вызываемые через API — там автором станет вызвавший), обернуть в режим. - Аудит: тесты на настоящем Postgres (
tests/Ava3v.Audit.PostgresTests, Testcontainerspostgres:14.1либоTEST_POSTGRES_CONNECTION); без Docker локально — Skipped, в CI (CI=true) — падение.
Аудит (Ava3v.Audit, 0.3.0+)
Журнал изменений по полям и снимки версий в транзакции самих изменений. Контракты — в
Ava3v.Abstractions.Audit, реализация — в пакете Ava3v.Audit. Подключение:
// DI
services.AddAva3vAudit<AppDbContext>(o => { o.Schema = "audit"; o.WriteSnapshots = true; });
services.AddDbContext<AppDbContext>((sp, o) => o.UseNpgsql(cs).UseAva3vAudit(sp));
// Своё: описания полей, резолверы ссылок, форматтеры, скоуп тенанта, политика доступа, имена авторов
services.AddScoped<IAuditFieldDescriptorProvider, IssueAuditFields>();
services.AddScoped<IAuditReferenceResolver, UserReferenceResolver>(); // читать через IgnoreQueryFilters!
services.AddScoped<IAuditScopeProvider, WorkspaceAuditScope>(); // TenantId/ScopeId + фильтр чтения
services.AddScoped<IAuditAccessPolicy, EntityAuditAccessPolicy>(); // без неё — любому аутентифицированному
services.AddScoped<IAuditUserNameResolver, UserNameResolver>();
// Контекст: наследник Ava3vDbContextBase (штамп метаданных и мягкое удаление — до аудита)
protected override void OnModelCreating(ModelBuilder b) { base.OnModelCreating(b); b.ApplyAva3vAudit(auditOptions); }
public bool SuppressAudit { get; set; } // IAuditSuppressible — миграции и массовая загрузка
// Эндпоинт истории: GET api/audit/getShort?entityType=Issue&entityId=5&skip=0&take=20
app.MapAva3vAudit();
// Программно: IAuditLogService.GetShortAsync(...), IEntitySnapshotService.GetAsOfAsync<T>(id, moment)
Правила:
- журнал пишется вторым SaveChanges того же контекста в той же транзакции; без явной транзакции интерсептор открывает свою и коммитит после журнала; откат/ошибка/отмена журнал не оставляют;
EnableRetryOnFailureнесовместим с собственной транзакцией: оборачивайте сохранения вCreateExecutionStrategy().ExecuteAsync(...)(интерсептор бросает понятную ошибку, если этого не сделать), либоUseOwnTransaction = false;ExecuteUpdate/ExecuteDeleteобходят и метаданные, и мягкое удаление, и журнал — в проекте нужен архитектурный тест с разрешённым списком таких вызовов;- мягкое удаление пишется как
Deleted; lifecycle-поля,[AuditIgnore],INotAuditableне пишутся; - автор записи — текущий пользователь (не
CreatedByсущности); ключ сущности —long; - таблицы
{schema}.audit_logи{schema}.entity_snapshot— миграция на стороне потребителя.