Changelog
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[Unreleased]
[10.10.0] - 2026-06-29
Release única que entrega as três fases de infraestrutura — Email/Cache, Microsoft Entra ID e Bus/Messaging. As subseções abaixo preservam o detalhe por fase. Primeiro consumidor:
ITLab.Template.Next(pina 10.10.0).
Added — Event bus and messaging harness
Abstrações de bus de eventos (domínio + notification) + harness genérico de transporte: bus in-process (
Channel<T>+BackgroundService) e harness receptor de Azure Service Bus com settlement (Complete/Abandon/DeadLetter), retry/backoff e impersonação de worker. Per D7=Var-A, o harness vive no DevPack como opt-in (AddDevPack*). O publisher ServiceBus (keyedExternal) e a convenção de roteamento (Args[0]) ficam no consumidor (canon). Ver ADR-011. Primeiro consumidor:ITLab.Template.Next(Fase 3 — Bus/Messaging).EMessageDisposition/MessageHandlingResult/MessagingErrorsjá existiam e são reusados.
Messaging/EventBusType—enum { InMemory, External }, seleciona o transporte de publicação.Messaging/IDomainEventsBus—Task PublishAsync<T>(T, CancellationToken) where T : class, IDomainEvent.Messaging/IDomainEventsBusFactory—IDomainEventsBus Create(EventBusType)(resolução keyed).Messaging/INotificationsBus—Task PublishAsync<T>(T, CancellationToken) where T : class, INotificationEvent.Messaging/IDomainEventMessageHandler<in T>—Task<MessageHandlingResult> HandleAsync(T, CancellationToken); o resultado dirige o settlement do receiver.Messaging/InMemoryDomainEventsQueue/InMemoryNotificationsQueue—Channel<T>unbounded (singleton).Messaging/InMemoryDomainEventsBus/InMemoryNotificationsBus— escrevem o evento na fila correspondente.Messaging/DomainEventsProcessor/NotificationEventsProcessor(BackgroundService) — drenam a fila e republicam via MediatorIPublisherem escopo novo; logam o event id real (GetEventId()), não umGuidaleatório (correção do bug do WG).Messaging/DomainEventsBusFactory—IDomainEventsBusFactoryviaGetKeyedService<IDomainEventsBus>(busType); lançaInvalidOperationExceptionclara quando o transporte (ex.:External) não está registrado.Messaging/RetryBackoffGate<T>—SemaphoreSlim+ pausa global para backoff transitório (singleton porT).Messaging/ServiceBusEventReceiver<T>(where T : class, IDomainEvent) —ServiceBusProcessor(AutoComplete=false, MaxConcurrentCalls=1, Prefetch=0); (de)serializaçãoSystem.Text.Json(sem Newtonsoft); valida payload+TenantId(DeadLetter se inválido); trocaICurrentUserparaservicebus-workercom oTenantIddo evento; resolveIDomainEventMessageHandler<T>; settlement com máx. 5 entregas. Classificação de transitório desacoplada do WG (sem tipos OpenAI): sóServiceBusException.IsTransient+TimeoutException, com hook opcional injetávelFunc<Exception, TimeSpan?>?. Lógica de decisão extraída em método puro estáticoDecideDisposition(MessageHandlingResult, int deliveryCount)(testável).Messaging/ServiceBusReceiverHostedService<T>+ServiceBusReceiverQueue<T>—IHostedServiceque chamaStartAsync(queue)/StopAsync.Messaging/DevPackMessagingServiceCollectionExtensions—AddDevPackInMemoryEventBus(filas + bus keyedInMemory+INotificationsBus+ processors + factory),AddDevPackServiceBusClient(connection stringConnectionStrings:ServiceBusouDefaultAzureCredential+ServiceBusOptions.Namespace),AddDevPackServiceBusEventReceiver<T>(queueName)(gate + receiver + hosted service).- Pacote:
Azure.Identity1.13.2 adicionado (managed identity viaDefaultAzureCredential).Azure.Messaging.ServiceBusjá presente. - Docs:
Messaging/README.md(atualizado); ADR-011. - Testes:
MessagingTests(RetryBackoffGate<T>enter/release/pause; in-memory bus publish→queue;ServiceBusEventReceiver<T>.DecideDisposition— invalid→DeadLetter, transient abandon<5→Abandon, ≥5→DeadLetter, Completed→Complete).
Added — Microsoft Entra ID connector
Conector Entra ID: terceiro scheme de autenticação, opt-in por tenant, que compõe com o dual scheme existente (cookie BFF + Bearer próprio) sem substituir o
DefaultProvider(ADR-010). O DevPack ship só a abstração de validação per-tenant + mapeamento de claims + a extensão de registro do scheme; o wiring, o sliceSigninWithEntrae o Graph sync ficam no consumidor (template). Primeiro consumidor:ITLab.Template.Next(Fase 2 — Entra ID).
Identity/Authentication/EntraId/IEntraTenantValidator+EntraTenantValidationResult(record (bool IsValid, int? AppTenantId, string? FailureReason)+ factoriesSuccess/Fail) — decide se um token Entra (tenant Entra extraído doiss) é aceito para o tenant da aplicação.Identity/Authentication/EntraId/PerTenantSettingsEntraTenantValidator— impl default lendoIPerTenantSettingsStoreporEntraSettingKeys: exige provider habilitado + diretório (isstid) casando o configurado + (recomendado) audience contra oWebApi.ClientIddo tenant, tolerante quando o client id ou oaudnão estão presentes. Resolve o tenant da aplicação viaICurrentUser.TenantId(resolvido pré-auth por domínio; ADR-007).Identity/Authentication/EntraId/IEntraClaimsMapper+EntraUserInfo(record (string ObjectId, string? UserName, string? Email, string[] Roles)) +DefaultEntraClaimsMapper— mapeiaoid/objectidentifier,name/preferred_username/email/upn,roles+ scopesscp(split por espaço).Identity/Authentication/EntraId/IEntraGraphClientFactory— interface-only (Task<object?> CreateForTenantAsync(int appTenantId, CancellationToken)); a impl concreta comMicrosoft.Graphfica no consumidor (DevPack não referenciaMicrosoft.Graph).Identity/Authentication/EntraId/EntraSettingKeys— constantes das chaves per-tenant:AuthProviderEntraIdEnabled(AuthProvider.EntraID, idêntica à chave canon do template) + famíliaEntraID.*(TenantId,Instance,WebApi.ClientId,WebApi.ClientSecret,WebApi.Scopes,Spa.ClientId,Spa.RedirectUri,Spa.Scopes) +Prefix.Identity/Authentication/EntraId/DevPackEntraIdServiceCollectionExtensions.AddDevPackEntraId(this AuthenticationBuilder, string scheme = IdentitySchemas.MicrosoftEntraID.Name, Action<MicrosoftIdentityOptions>? configure = null)— registraAddMicrosoftIdentityWebApipara o scheme; issuer validator aceitalogin.microsoftonline.com/{tid}/v2.0ests.windows.net/{tid}/;OnTokenValidatedresolveIEntraTenantValidatordoRequestServices, extrai o tenant Entra doissecontext.Fail(reason)se inválido (encadeia após o handler doMicrosoft.Identity.Web). RegistraDefaultEntraClaimsMapperviaTryAdd. Helper público estáticoExtractTenantIdFromIssuer(string? iss).- Pacote:
Microsoft.Identity.Web3.8.3 adicionado.Microsoft.IdentityModel.JsonWebTokensbumpado 8.0.1 → 8.8.0 para unificar o grafo de tokens (Identity.Web exige >= 8.8.0; evita NU1605). - Docs:
Identity/Authentication/EntraId/README.md(novo),Identity/README.md(§ Entra connector); ADR-010. - Testes:
ExtractTenantIdFromIssuerTests,PerTenantSettingsEntraTenantValidatorTests,DefaultEntraClaimsMapperTests.
Added — Email abstractions and options
Contratos universais de email (serviço, renderer de template, repositório de template) + options tipadas. O DevPack ship só os contratos + options; os concretos (SMTP/SendGrid, renderer HTML, repositório de arquivos, layouts) vivem no consumidor. Ver ADR-009. Primeiro consumidor: template
ITLab.Template.Next(Fase 1 — Email/Cache/Templates).
Notifications/Email/IEmailService— contrato de envio: três overloadsSendAsync(destinatário único, múltiplos, múltiplos +attachments) +SendFromTemplateAsync(templateKey, IDictionary<string, object> data, subject, string[] destination, int? tenantId = null, IEnumerable<EmailAttachment>? attachments = null, CancellationToken).Notifications/Email/IEmailTemplateRenderer—string Render(string templateContent, IDictionary<string, object?> data, string? layoutContent = null). Engine de renderização é do consumidor.Notifications/Email/IEmailTemplateRepository—Task<string?> GetTemplateAsync(string templateKey, int? tenantId = null, CancellationToken ct = default). Email é HTML-only; sem acoplamento a um enum de canal de entrega.Dtos/EmailResult—record (bool Success, string? MessageId = null, string? ErrorMessage = null), resultado de envio.AppSettings/EmailOptions(IAppOptions, seçãoEmailOptions) —Provider(Smtp|SendGrid),DefaultFromAddress,DefaultSenderName,Smtp,SendGrid; propsprivate init. NestedSmtpOptions(DeliveryMethod,Host,Port,EnableSsl,Username,Password,PickupDirectoryLocation) eSendGridOptions(ApiKey).- Docs:
Notifications/Email/README.md(novo),Notifications/README.md,Dtos/README.md,AppSettings/README.md; ADR-009.
Nota:
Dtos/EmailAttachment(record (Stream Content, string FileName, string? ContentType = null)) já existia no DevPack e é reusado por estes contratos — não é uma adição desta release.
[10.7.1] - 2026-06-22
Added — Per-tenant settings store + secret protection
Nota: a 10.7.0 foi publicada por engano sem este conteúdo (Fase 2 re-versionada). Esta entrega saiu como 10.7.1.
Entradas de versões ≤ 10.7.1 abaixo preservam a estrutura plana herdada (acumuladas sob
[Unreleased]em releases anteriores); não foram reorganizadas nesta mudança.
Substrato universal para configuração de auth por-tenant (Fase 3 do roadmap de multi-tenancy). Esta entrega ship só o substrato; o consumidor (template) habilita apenas o provider local — Entra/LDAP ficam para depois. Ver ADR-008. Primeiro consumidor: template
ITLab.Template.Next(ADR-012).
Abstractions/ISecretProtector—Protect/Unprotectsimétricos para cifrar segredos em repouso.Abstractions/DataProtectionSecretProtector— impl default sobre ASP.NET Core Data Protection (IDataProtectionProvider, shared framework — sem pacote novo); purpose fixo.Abstractions/SecretProtectorServiceCollectionExtensions.AddDevPackSecretProtector()— registraAddDataProtection()+ISecretProtector(viaTryAdd).Identity/Settings/IPerTenantSettingsStore— contrato string-valued para settings por-tenant:GetAsync,GetByPrefixAsync,SetAsync(..., mustProtect),InvalidateCache. Secrets cifrados em repouso e retornados decifrados. Interface no DevPack (universal; seam para ler config fora de um handler, ex.OnTokenValidatedpor-tenant); impl é do consumidor.- Recipe (docs): schemes de auth são estáticos (registrados na startup) → validação por-tenant nos eventos (
OnTokenValidated) lendo o store.IPerTenantAuthSchemeResolverdeliberadamente não promovido (recipe primeiro — ver ADR-008). - Decisão: cifragem no store (em
SetAsync/GetAsync), não numSaveChangesInterceptor— evita dupla cifragem e estado de entidade (ver ADR-008 §3). - Docs:
Abstractions/README.md(§ secret protection),Identity/Settings/README.md(novo), ADR-008. - Testes:
SecretProtectorTests.
Added (10.6.0 — domain tenant resolution + glue middleware)
Ativa a resolução de tenant por domínio/subdomínio como caminho de primeira classe, opt-in por config. Os contributors concretos + a glue middleware + as options eram o plumbing que faltava sobre as abstrações da 10.4.0. Ver ADR-007. Primeiro consumidor: template
ITLab.Template.Next(ADR-011).
Identity/MultiTenancy/HttpTenantResolveContributorBase— base abstrata para contributors que leem oHttpContext(resolvido viaITenantResolveContext.GetHttpContext()); captura/loga exceções para não quebrar o pipeline.Identity/MultiTenancy/DomainTenantResolveContributor— resolve o tenant do host na ordemOrigin→Referer→Request.Host, casando contraMultiTenancyOptions.AlternateDomains(overrides literais, primeiro) eDomainFormats(padrões{0}viaFormattedStringValueExtracter). Logger opcional.Identity/MultiTenancy/MultiTenancyOptionsExtensions.AddDomainTenantResolver(this TenantResolveOptions, string[] domainFormats, List<AlternateDomainOption> alternateDomains, IServiceProvider? serviceProvider = null)— insere o domain contributor no início da lista de resolvers.AppSettings/MultiTenancyOptions(IAppOptions, seçãoMultiTenancyOptions) —EnableDomainValidation(default false),DefaultDevelopmentTenant,DomainFormats,AlternateDomains,ExcludedPaths.AppSettings/AlternateDomainOption—Host+Tenant(override literal host→tenant).Middleware/TenantResolutionMiddleware+Extensions/ApplicationBuilderExtensions.UseDevPackTenantResolution()— glue genérica: gate emEnableDomainValidation, skip de anônimos/ExcludedPaths, resolve host →ITenantAccessValidator.GetTenantIdByDomainNameAsync→ValidateAccessAsync→ cross-check contraICurrentUser.TenantId. Status:401(sem tenant no token / host não resolve / divergência host-vs-token),404(host sem tenant),403(sem membership). No-op quando desabilitada. Deve rodar apósUseDevPackIdentityContext()e antes deUseAuthorization().- Docs:
Identity/MultiTenancy/README.md(§ resolução por domínio),AppSettings/README.md,Middleware/README.md; ADR-007. - Testes:
DomainTenantResolveContributorTests,TenantResolutionMiddlewareTests.
Added (10.5.0 — multi-tenant persistence: global query filter + stamping interceptor)
Lado de persistência da tenancy (a resolução/contributors já existiam na 10.4.0). Ver ADR-006. Primeiro consumidor: template
ITLab.Template.Next(ADR-009, canon claim-based row-level).
Infrastructure/EfCore/MultiTenantModelBuilderExtensions.ApplyMultiTenantQueryFilters(this ModelBuilder, IMultiTenantDbContext)— varre o model e aplica, por reflexão, uma única lambda de query filter por entidade combinando tenant + soft-delete (switch de 4 viasisMultiTenant × isSoftDeletable). Pula entidades com filtro já declarado (GetDeclaredQueryFilters().Count > 0) — preserva o filtro de soft-delete das convenções de Identity e dá escape hatch (filtro explícito numa entidadeIMultiTenanta remove do sweep).Infrastructure/EfCore/IMultiTenantDbContext—int? CurrentTenantId { get; }. O query filter referencia a instância do DbContext por meio desta interface: o EF Core só re-avalia o filtro por query quando o predicado referencia o contexto; umFunc<int?>/objeto capturado é funcletizado para constante e congela no modelo cacheado. Consumidor:public int? CurrentTenantId => _currentUser.TenantId;+modelBuilder.ApplyMultiTenantQueryFilters(this).Infrastructure/EfCore/Interceptors/MultiTenantEntityInterceptor— write-side: stampaIMultiTenant.TenantIdno insert (reflexivo, viaObjectHelper.TrySetProperty) e lançaInvalidOperationExceptionem modify/delete cross-tenant. No-op quando não há tenant corrente (null/0= host/global). ResolveICurrentUserde escopo por-call (Singleton).Infrastructure/EfCore/DevPackEfCoreInterceptorBuilder.WithMultiTenantInterceptor()— registra o interceptor. Ordem recomendada: apósWithSoftDeleteInterceptor(), antes deWithDispatchDomainEventsInterceptor().Testing/Authentication/TestAuthenticationHandler— passa a emitir um claimtenantquando o headerX-Test-Tenantestá presente (constsTenantHeader/TenantClaimType), permitindo testes de isolamento multi-tenant. Sem o header, comportamento inalterado.
Changed (10.5.0)
Middleware/DevPackIdentityContextMiddleware.DefaultMap— passa a derivarUserInfo.TenantIddo claimtenant(outid) do principal, em vez de hardcodar0. Retrocompatível: ausência/valor não-numérico →0. Mapper custom (Func<HttpContext, UserInfo?>) permanece como escape hatch.- Docs:
Infrastructure/EfCore/README.md,Infrastructure/EfCore/Interceptors/README.md,Identity/README.md,Identity/MultiTenancy/README.md; ADR-006.
Added (10.4.0 — Identity store, self-issued tokens, JWKS, dual scheme)
Blocos universais para apps com Identity local que emitem os próprios tokens. Ver ADR-005. Consumido pelo template
ITLab.Template.Next(ADR-006, dual-scheme BFF cookie + Bearer JWKS como novo default).
Infrastructure/EfCore/BaseIdentityDbContext<TUser,TRole>+ModelBuilderIdentityExtensions.ApplyDevPackIdentityConventions—IdentityDbContext<TUser,TRole,int>+ convenções DevPack: move as 7 tabelas de Identity para um schema dedicado (defaultidentity, configurável viaIdentitySchema) sem o prefixoAspNet(identity.Users,identity.Roles,identity.UserRoles,identity.UserClaims,identity.UserLogins,identity.UserTokens,identity.RoleClaims); mapeiaAuditTimestamp [Owned]; aplica query filter de soft-delete no user.Identity/Authentication/Bearer/ITokenService+DefaultJwtTokenService— emite access token JWT assimétrico (RSA/ECDSA) com headerkid, issuer/audience deIdentityTokenOptions,TimeProviderinjetado. RecordAccessToken(Value, ExpiresAt).Identity/Authentication/Bearer/ISigningKeyProvider+EphemeralRsaSigningKeyProvider(RSA em memória, dev) +FileSigningKeyProvider(PEM em disco, prod).kidestável derivado do módulo público;PublicKeysexpõe só material público.Identity/Authentication/Bearer/JwksDocumentBuilder— JWKS (RFC 7517) das chaves públicas (RSA/ECDSA,use=sig); nunca emite material privado.Identity/RefreshToken(abstract) +IRefreshTokenStore+RefreshTokenGenerator— refresh token persistido só como hash; estado de rotação (Revokeidempotente,ReplacedByTokenId) e contrato de store (add/find-active/rotate/revoke-all).Identity/ServiceClient(abstract) — cliente de máquina para client-credentials:ClientId, secret BCrypt (VerifySecret/HashSecret),GrantedRoles,ToClaimsIdentity.AppSettings/IdentityTokenOptions—Issuer/Audience/AccessTokenLifetimeMinutes/RefreshTokenLifetimeDays/SigningKeyPemPath(seçãoAuthentication:Token).Identity/DevPackIdentityServiceCollectionExtensions—AddDevPackSigningKey(pemPath, isDevelopment)(ephemeral em dev, PEM em prod, falha-cedo fora de dev sem chave),AddDevPackTokenService(configuration),AddDevPackDualScheme(selector)(só oPolicySchemeSmartcomForwardDefaultSelector;AddCookie/AddJwtBearerpermanecem explícitos no consumidor — ADR-001).Identity/IdentitySchemas.Bff+IdentitySchemas.Smart— constantes de scheme do cookie BFF e do scheme composto.- Pacote:
Microsoft.IdentityModel.JsonWebTokens8.0.1 (pinado na versão queMicrosoft.AspNetCore.Authentication.JwtBearer10.0.1 resolve transitivamente, para unificar o grafo do consumidor sem upgrade forçado). - Docs:
Identity/README.md§ Self-issued tokens & dual scheme.
Added (10.3.0 — code + namespace migration)
ITLab.Template.DevPack.Testing(novo pacote auxiliar publicável): ship como nupkg separado.WebApplicationFactoryBase<TProgram, TDbContext>— substituiDbContextOptions<TDbContext>por EF InMemory isolado, registraTestAuthenticationHandlercomo esquema default, expõeCreateClientWithRoles(...)eCreateAnonymousClient(), hookConfigureAdditionalServices(IServiceCollection).TestAuthenticationHandler— lê roles do headerX-Test-Rolese produzClaimsPrincipalautenticado.IsolatedInMemoryProviderExtensions.UseIsolatedInMemoryProvider(string)— InMemory comIServiceProviderinterno isolado, evita o erro "Services for database providers 'X', 'InMemory' have been registered".CurrentUserBuilder/DateTimeProviderBuilder/ObjectArrayExtensionsmigrados detests/ITLab.Template.DevPack.Testing(mesmo nome de assembly; o projeto interno foi promovido a pacote publishable).
Helpers/DocumentHelper— validação e normalização de CPF/CNPJ brasileiros:IsCpfValid,IsCnpjValid,NormalizeCpf,NormalizeCnpj. Cobre all-equal, formatação, null/empty, comprimento e mod 11. Pesos do CNPJ corretos (não copiados de CPF).Guards/DocumentGuards—Guard.Against.InvalidCpfeGuard.Against.InvalidCnpjretornam o valor digit-only normalizado em sucesso (compatível com inicialização de value object). Internamente delegam paraDocumentHelper.Middleware/DevPackIdentityContextMiddleware+Extensions/ApplicationBuilderExtensions.UseDevPackIdentityContext()— bridge entreHttpContext.UsereAsyncLocalCurrentUserAccessor. Sem isso,RoleBasedPermissionEvaluatorrecebeUserRoles = []em produção. Mapper default extrai claimsNameIdentifier/sub,Name/name/preferred_username, eRole/role. Override via overload comFunc<HttpContext, UserInfo?>para layouts JWT custom.Extensions/ResultHttpExtensions.ToHttpResult(...)— bridgeResult<T>/Result→Microsoft.AspNetCore.Http.IResultpara Minimal APIs. Mapping default: Validation→400, NotFound→404, Unauthorized→403, Conflict→409, Failure/Problem→500. AceitaerrorMapperopcional.Extensions/MinimalApiPaginatedExtensions.MapPaginatedGet<TQuery,TDto>— encapsula bind individual depageNumber/pageSize/orderBy/order/filter/paginated, dispatcha via Mediator e renderiza comToHttpResult. Resolve gotcha do[AsParameters]sobrePaginatedQueryBase<T>.Domain/AuditedSoftDeletableEntity<TKey>: ctorprotected AuditedSoftDeletableEntity(TKey id) : base(id) {}— paridade comAuditedEntity<TKey>. Permiteprivate Pedido(Guid id, ...) : base(id)sem precisar deId = id;no corpo.Domain/DomainEvent.GetKeys(): XML doc com<example>de override completo (OrderApprovedEvent : DomainEvent).
Changed (10.3.0 — BREAKING namespace migration)
Result,Result<T>,Error,ErrorType,ValidationErrormovidos do namespace raizITLab.Template.DevPackparaITLab.Template.DevPack.Core. Foldersrc/.../Results/renomeado parasrc/.../Core/. Update consumerusingdirectives. Marcado breaking por convenção; nenhum consumidor de produção existia no momento da release.DispatchDomainEventsInterceptorctor mudou de(IPublisher publisher)para(IServiceProvider serviceProvider). ResolveIPublisherper-save viaIServiceProvider.CreateAsyncScope(). Permite que o interceptor Singleton coexista com handlers Mediator Scoped (default DevPack) sem violar container validation. Regression test adicionado emtests/.../InterceptorsTests.cs.
Removed (10.3.0 — BREAKING)
ITLab.Template.DevPack.IResultremovido (sem uso real). Quem usavausing IResult = ITLab.Template.DevPack.IResult;para evitar colisão comMicrosoft.AspNetCore.Http.IResultpode remover o alias.
Added (10.2.4 — docs/CI)
- CLAUDE.md (raiz): prefácio "Escopo deste documento (princípio G-01)" estabelecendo onde mora cada regra (uso da biblioteca = aqui; convenções específicas do template =
ITLab.Template.Next/CLAUDE.md). Seção "What NOT to do" consolida os anti-padrões mais frequentes. Authorization/README.md: subseção "Permissions catalogue location" canonizandoApplication/Common/Authorization/{Project}Permissions.cs. Subseção "Permission convention: endpoint vs command" explicita queRequireAuthorizationé default e[RequiresPermission]é opt-in para regras contextuais.Behaviors/README.md: subseção "Validator placement convention" (validators ao lado do Command/Query +AddValidatorsFromAssembly).Cqrs/README.md: seção "Handler lifetime" (configuraropt.ServiceLifetime = ServiceLifetime.ScopedemAddMediatorquando handlers consomemIAppDbContext); seção "Domain events" cross-link com Domain README.Core/README.md: tabela "Error factory methods" com mapping HTTP default (Validation→400, NotFound→404, Unauthorized→403, Conflict→409, Failure/Problem→500).Domain/README.md: seção "Domain events policy" (quando criar evento + handler obrigatório MSG0005 + placeholder pattern comILogger). EsclareceIDomainEvent : Mediator.INotification.Identity/README.md: seção "Authentication scheme — production checklist" (JWT Bearer / Entra ID / Cookie / pipeline order incluindoapp.UseDevPackIdentityContext()da 10.3.0). Nota sobrePlaceholderAuthenticationHandlerser scaffold do template, não publicado no DevPack.Infrastructure/EfCore/README.md: seção "EF migrations CLI" com--project+--startup-projecte cross-ref ao CLAUDE.md do template.- CI: smoke-test-convention-check via Roslyn em
tools/DocsSmokeCheck/— extrai blocos```csharpdos READMEs de áreas e os compila contra a DLL recém-buildada do DevPack. Suporta fence```csharp-noverifypara snippets ilustrativos.- Baseline pragmática: snippets de "Como usar" / wiring / chamadas em corpo de método ficaram marcados como
csharp-noverifypor padrão, pois referenciam símbolos do consumidor (Order,services,MyAppPermissions). Blocos auto-suficientes (definições de tipo) ficam comocsharppara guardar regressões — atualmente cobrem o A-02 (ICommand<T>/IQuery<T>), A-03 (class : PaginatedQueryBase<T>) e C-01 (override GetKeys()). - CI gate é strict (
continueOnError: false).Mediatorfoi removido dos common usings da tool por colidir comITLab.Template.DevPack.Cqrs; blocos que precisam dele declaramusing Mediator;explicitamente. CONTRIBUTING.mddocumenta as convenções e o checklist do autor.
- Baseline pragmática: snippets de "Como usar" / wiring / chamadas em corpo de método ficaram marcados como
Changed
-
Cqrs/README.md: exemplo canônico mudou deICommand<Result<TResponse>>paraICommand<TResponse>— o wrappingResult<>é aplicado automaticamente pela assinatura do handler. "What NOT to do" no CLAUDE.md ratifica. -
Data/README.md: exemplo canônico mudou derecord ListOrdersQuery : PaginatedQueryBase<...>paraclass ListOrdersQuery : PaginatedQueryBase<...>— base é classe abstrata, records C# não herdam de classes não-record (CS8865/CS0115). Cross-ref aMapPaginatedGet(10.3.0). -
LLM-readable docs shipped inside the nupkg at
devpack-docs/. A pasta incluillms.txt(índice formato llmstxt.org),CLAUDE.md,DEVPACK.md,CHANGELOG.md, os 4 ADRs emadrs/, e os READMEs por área emareas/{Namespace}/README.md(preserva hierarquia — ex.:areas/Identity/MultiTenancy/README.md). Resolve no consumidor via{NuGetPackageRoot}/itlab.template.devpack/{version}/devpack-docs/. -
Empacotamento via
<None Pack="true" PackagePath="devpack-docs/...">no.csproj. Os arquivos MD reusam a fonte de verdade do repositório (sem duplicação) — editar o MD original atualiza fonte e o conteúdo do package na próximadotnet pack. -
Tamanho adicional do nupkg: ~30 KB compridos (39 arquivos MD totalizando ~135 KB descomprimidos). Insignificante perto dos 105 KB da DLL e 385 KB do XML doc.
Motivação: o
.xmldoc da DLL é referência de tipo (IntelliSense), mas falta conhecimento operacional ("quando uso X vs Y?", "qual é o padrão para Z?"). Odevpack-docs/cobre esse gap, version-pinned com a release exata do package consumido — elimina drift estrutural entre a doc e o binário.
[10.2.2] - 2026-05-02
Fixed
ITLab.Template.DevPack.csproj: adicionada<RestoreEnablePackagePruning>false</RestoreEnablePackagePruning>no<PropertyGroup>raiz. Sem essa property, o NuGet 10.x SDK prunava aPackageReferencedefensiva deSystem.Security.Cryptography.Xml 10.0.7(mesmo comNoWarn="NU1510") durantedotnet pack, fazendo com que o pin de segurança introduzido na 10.2.0 não chegasse ao nuspec publicado — verificado empiricamente nas releases 10.2.0 e 10.2.1, ambas sem a dependência no nuspec. A 10.2.2 carrega corretamente<dependency id="System.Security.Cryptography.Xml" version="10.0.7" />— consumidores herdam o hardening transitivamente sem precisar de override local.NoWarn="NU1510"removido do<PackageReference Include="System.Security.Cryptography.Xml">— redundante após oRestoreEnablePackagePruning=false(warning não dispara mais).
[10.2.0] - 2026-05-01
Nota de versionamento. A partir desta release, o esquema do DevPack alinha o primeiro dígito ao framework target (.NET 10 → DevPack 10.x). Esta versão é o sucessor direto da 2.1.1 — o conteúdo é o mesmo que estava sendo desenvolvido como "10.2.0" em rascunhos/relatórios, agora rotulado como 10.2.0 ("segunda minor para .NET 10"). Versões 2.x continuam disponíveis para projetos legados que ainda não migraram. Releases futuras seguirão
10.x.yenquanto a base for .NET 10; quando .NET 11 for adotado, o versionamento passa para11.x.y.
Release temático: **auditoria com value object IANA-aware + role-based authorization
- ergonomia de DI**. Acompanhada por dois ADRs novos:
- ADR-003 —
AuditTimestampvalue object com timezone por timestamp. - ADR-004 — Role-based permission resolution.
Security
- Pin transitivo de
System.Security.Cryptography.Xml10.0.7. Endurece o graph contra os advisories de severidade alta GHSA-37gx-xxp4-5rgx e GHSA-w3x6-4m5h-cxqf, presentes nas versões 9.x e 10.0.0–10.0.6 do pacote — puxado transitivamente viaMicrosoft.AspNetCore.Identity.EntityFrameworkCore. Consumidores herdam automaticamente; o pin noDirectory.Packages.propsdo consumidor pode ser removido (era a recomendação da Fase 1 do templateITLab.Template.Next).Errata pós-publicação: o nuspec dos pacotes 10.2.0 e 10.2.1 publicados não carregou esta dependência por causa de pruning automático do NuGet 10.x SDK durante
dotnet pack. Resolvido em 10.2.2 viaRestoreEnablePackagePruning=false(entrada dedicada no[10.2.2]abaixo). Consumidores de 10.2.0/10.2.1 devem atualizar para 10.2.2+ e remover pin defensivo local.
Removed (Breaking)
Behaviors/NormalizeDateTimeOffsetBehaviorremovido. Pipeline de behaviors passa a ser Logging → Validation (Normalize não é mais registrado porAddDevPackBehaviors()). Com a migração de auditoria paraAuditTimestamp(UTC + IANA), o behavior perdeu razão de existir. Projetos que precisem de normalização de fuso em campos de request devem implementar comportamento próprio na camada de aplicação.Attributes/NormalizeDateTimeOffsetAttributeremovido. Era consumido apenas pelo behavior removido.Domain/IAuditable: contrato reescrito em torno deAuditTimestamp.- Removidas:
Created(DateTimeOffset),LastModified(DateTimeOffset?),LastModifiedBy(string?),SetCreatedProperties(string, DateTimeOffset),SetUpdatedProperties(string, DateTimeOffset). - Adicionadas / renomeadas:
CreatedAt(AuditTimestamp),UpdatedAt(AuditTimestamp?),ModifiedBy(string?— renomeado deLastModifiedBy),SetCreatedAudit(string?, AuditTimestamp),SetUpdatedAudit(string?, AuditTimestamp). - Atribuição de usuário preservada (
CreatedBypermanece,LastModifiedByrenomeado paraModifiedBy).
- Removidas:
Domain/AuditedEntity/AuditedEntity<TKey>: campos públicos migrados conformeIAuditable.Domain/ISoftDeletable: contrato reescrito em torno deAuditTimestamp.- Removidas:
DeletedOnUtc(DateTimeOffset?),SoftDelete(string, DateTimeOffset). - Adicionada / mantida:
DeletedAt(AuditTimestamp?),DeletedBy(string?, mantido),SoftDelete(string?, AuditTimestamp).
- Removidas:
Domain/AuditedSoftDeletableEntity<TKey>: ajustado ao novoISoftDeletable.Identity/BaseIdentityUser: ajustado ao novoIAuditable/ISoftDeletable. Mesmas migrações de tipo.LastModifiedBy→ModifiedBy.Identity/BaseIdentityRole: ajustado ao novoIAuditable. Mesmas migrações.Infrastructure/EfCore/Interceptors/AuditableEntityInterceptor: construtor reescrito.- Antes:
(ICurrentUser currentUser, TimeProvider dateTime). - Depois:
(IServiceProvider serviceProvider). ResolveICurrentUserpor chamada viaIServiceProvider.CreateScope()— evita captura de scoped service em singleton interceptor. Constrói umaAuditTimestampcomDateTime.UtcNow+currentUser?.LocalTimeZonee populaCreatedBy/ModifiedBycomcurrentUser?.UserName.
- Antes:
Infrastructure/EfCore/Interceptors/SoftDeleteInterceptor: construtor reescrito.- Antes:
(ICurrentUser currentUser, TimeProvider dateTime). - Depois:
(IServiceProvider serviceProvider, TimeProvider dateTime). Mesmo padrão de scope-por-chamada; preservaDeletedByno entity viacurrentUser?.UserName.
- Antes:
Added
Domain/AuditTimestamp— value objectrecord (DateTime UtcDateTime, string? LocalTimeZone)que encapsula o par "instante UTC + IANA timezone". Cada propriedade de auditoria (CreatedAt,UpdatedAt,DeletedAt) carrega seu próprioAuditTimestamp— criação, atualização e deleção podem ocorrer em fusos diferentes e ficam independentemente reconstruíveis. Métodos:AuditTimestamp.Now(string? localTimeZone)eToLocal()que devolveDateTimeOffsetaplicando a timezone (fallback silencioso para UTC se a timezone for desconhecida ao runtime). Anotado com[Owned]— EF Core auto-mapeia como propriedade owned (duas colunas por padrão:{Property}_UtcDateTimee{Property}_LocalTimeZone), sem configuração manual no consumidor para o caso comum.Authorization/IRolePermissionResolver— contrato para resolução de permissões a partir de role names.Authorization/RoleBasedPermissionEvaluator— implementação default deIPermissionEvaluatorque combinaICurrentUser.UserRoles+IRolePermissionResolver. Cache por-instância (Scoped lifetime).Authorization/DevPackAuthorizationExtensions.AddDevPackRoleBasedPermissionEvaluator<TResolver>()— registration conveniente.Infrastructure/EfCore/DevPackEfCoreInterceptorBuilder+ extensionservices.AddDevPackEfCoreInterceptors()— fluent builder para registrar interceptors EF Core. Suporta.WithAuditableEntityInterceptor(),.WithSoftDeleteInterceptor(),.WithDispatchDomainEventsInterceptor()e.WithInterceptor<TInterceptor>()para customizados. Cada interceptor é registrado como singleton sob dois canais: tipo concreto (para testes) eISaveChangesInterceptor(consumido porAddDbContextviaprovider.GetServices<ISaveChangesInterceptor>()). A ordem de registro é preservada e determina a ordem de execução. Forma legada de registro manual continua suportada — adição não-quebrante.Abstractions/UtcDateTimeProvider— implementação default deIDateTimeProvider. Todos os 11 membros do contrato resolvem para UTC (system e user-local idênticos;UserTimeZone == TimeZoneInfo.Utc). Lifetime: Singleton. Indicado para hosts sem user context (workers, batch).Abstractions/LocalDateTimeProvider— implementação deIDateTimeProviderque converte UTC paraICurrentUser.LocalTimeZone(IANA) para os membrosUserLocal*. MembrosSystem*usamTimeProvider.GetLocalNow()(timezone do servidor). Fallback silencioso para UTC quando a timezone do usuário é null, vazio ou desconhecida ao runtime — nunca lança exceção. Lifetime: Scoped (depende deICurrentUser).Abstractions/DateTimeProviderServiceCollectionExtensions—AddDevPackUtcDateTimeProvider()(Singleton) eAddDevPackLocalDateTimeProvider()(Scoped) para registro conveniente. Pré-requisito comum:services.AddSingleton(TimeProvider.System).
Migration notes
- Leituras de auditoria: substituir
entity.Created(DateTimeOffset) porentity.CreatedAt.UtcDateTime(DateTimeUTC) ouentity.CreatedAt.ToLocal()(DateTimeOffsetreconstruído na timezone original). Mesmo padrão paraLastModified→UpdatedAt,DeletedOnUtc→DeletedAt. - Atribuição de usuário:
CreatedByeDeletedBymantêm nome.LastModifiedByfoi renomeado paraModifiedBy— consumidores que liamentity.LastModifiedBydevem trocar paraentity.ModifiedBy. - Migrações EF:
- Cada propriedade de auditoria vira duas colunas:
{Name}_UtcDateTime(datetime2) e{Name}_LocalTimeZone(nvarchar(64), nullable). Por exemplo,CreatedAtse tornaCreatedAt_UtcDateTime+CreatedAt_LocalTimeZone. Ajuste seu schema migration. - Para projetos que preferem coluna única por timestamp, é possível usar
builder.Property(e => e.CreatedAt).HasConversion<string>(...)emOnModelCreating— perde queryability dos campos individuais.
- Cada propriedade de auditoria vira duas colunas:
AuditableEntityInterceptorregistro: continua sendoservices.AddScoped<AuditableEntityInterceptor>()no DI —IServiceProvideré resolvido automaticamente. Sem mudança no chamador.SoftDeleteInterceptorregistro: idem —services.AddScoped<SoftDeleteInterceptor>(). As dependências (IServiceProvider,TimeProvider) são resolvidas pelo DI.TimeProvider.Systemdeve estar registrado (services.AddSingleton(TimeProvider.System)).- Pipeline de behaviors: remover qualquer registro manual de
NormalizeDateTimeOffsetBehaviorse aplicável;AddDevPackBehaviors()cuida do par Logging → Validation automaticamente. - Permissões role-based: substituir implementação custom de
IPermissionEvaluatorporservices.AddDevPackRoleBasedPermissionEvaluator<MyRolePermissionResolver>()quando o caso de uso for "permissões via roles".
[2.1.1] - 2026-05-01
Fixed
Data/QueryFilter: o operadorcontainsagora emite(prop != null && prop.Contains(value))em vez deprop.Contains(value). O comportamento anterior lançavaNullReferenceExceptionem LINQ-to-Objects quando a propriedade alvo eranull— limitação documentada na 2.1.0 via o testeParseFilter_StringContains_OnNullProperty_*. Em LINQ-to-EF Core o comportamento já era silencioso (SQL absorveNULL); o fix alinha as duas trilhas de execução. Inclui o operador composto[any] containsque usa o mesmo caminho.tests/.../Data/QueryFilterTests.cs: o testeParseFilter_StringContains_OnNullProperty_ThrowsNullReferenceExceptionfoi renomeado paraParseFilter_StringContains_OnNullProperty_ShouldExcludeWithoutThrowing, refletindo o comportamento corrigido. O fixture passou a manterBob.Notes = nullpara exercitar a null-safety em todos os cenários decontains.
[2.1.0] - 2026-05-01
Added
tests/.../Helpers/PermissionsGeneratorTests.cs: 18 testes cobrindoBuildDictionary(classe plana, hierárquica, vazia, campos não-string),GenerateJson(outputs indented),GenerateTypeScriptUnion(flatten recursivo, filtro de empty values, default type name),GenerateTypeScriptShape(objeto literal, hierarquia, default name) eGenerate(dispatch para cada modo).tests/.../Identity/MultiTenancy/TenantResolverTests.cs: 7 testes cobrindo o pipeline de contributors — sem contributors (Unresolved), resolução porTenantIdOrName, resolução porHandledflag, short-circuit do primeiro contributor que resolve, fallback para o segundo quando o primeiro não resolve, propagação de exceções e ordem de execução.tests/.../Data/QueryFilterTests.cs: 30 testes cobrindo todos os operadores DSL (eq,ne,gt,lt,ge,le,contains,in,[any] contains), composição comand/or, parênteses, propriedades aninhadas via dot notation, comparação contranullem propriedades nullable, e validação de inputs inválidos (formato, operador desconhecido, property inexistente). Inclui também um teste explícito que documenta uma limitação conhecida — ver seção abaixo.
Known limitations (documented by tests)
Data/QueryFilteremiteprop.Contains(value)sem null-guard. Em LINQ-to-Objects (uso in-memory ou em testes) isso lançaNullReferenceExceptionquando a propriedade tem valornull. Em LINQ-to-EF Core o comportamento é silencioso — o SQL gerado absorveNULLcorretamente. Comportamento documentado no testeParseFilter_StringContains_OnNullProperty_ThrowsNullReferenceException. Fix candidato (em release futura): emitir(prop != null && prop.Contains(value))para alinhar comportamento in-memory e database.
[2.0.2] - 2026-05-01
Added
Guards/nova área dedicada com namespaceITLab.Template.DevPack.Guards. As classesObjectGuards,DateTimeOffsetGuardseDateOnlyGuardsforam extraídas deExtensions/GuardExtensions.cs(que era arquivo único com três classes coabitando) para arquivos individuais sob a nova pasta.Guards/README.mddocumenta a área seguindo o padrão das demais.DateOnlyGuardspassa a estar documentada — não havia entrada anterior em README algum.
Changed
- (Breaking, cosmético) Namespace dos guards mudou de
ITLab.Template.DevPack.ExtensionsparaITLab.Template.DevPack.Guards. Consumidores externos precisam adicionarusing ITLab.Template.DevPack.Guards;em arquivos que usemGuard.Against.NotEqual(...),Guard.Against.MustBeQuarterHourRounded(...),Guard.Against.NotFirstDayOfMonth(...)etc. Aceito como patch pelo mesmo critério aplicado em 2.0.1: DevPack ainda não tem consumidores externos; correção pré-publicação ampla. - (Breaking, cosmético)
NameValue.csmovido da raiz desrc/paraDtos/NameValue.cs; namespace atualizado deITLab.Template.DevPackparaITLab.Template.DevPack.Dtos. Mesma justificativa do bullet anterior.Dtos/README.mdagora listaNameValueeNameValue<T>. Helpers/README.md: corrigidosEPermissionsGeneratorTypemodos (Json/TsUnion/TsShapeem vez deFlat/Hierarchical/FlatWithPrefix), exemplo dePermissionsGeneratorreescrito sobreGenerate(...)real, e adicionada nota explicando queDateTimeOffsetHelpercoabita emDateTimeHelper.cs(mesmo namespaceHelpers).Dtos/README.md: tabela de Componentes ampliada para listarOptionDto<TValue>,OptionDto<TValue, TAdditional>,GenericKeyValueDto<TValue>eGenericKeyValueDto<TValue, TAdditional>(anteriormente omitidos), além de separarBaseKeyValueDtoem variantes<TValue>e<TValue, TAdditional>. Anotada a divergência arquivo↔classeScheduledJobConfig×ScheduledJobConfiguration.cs.Notifications/README.md: snippet sinaliza explicitamente queINotificationServiceé contrato do consumidor, não do DevPack; adicionado bullet em "O que NÃO faz".Identity/MultiTenancy/README.md: snippet final reescrito —currentUser.SetTenant(...)não existe; adapter de aplicação do tenant (claim, accessor custom, etc.) é responsabilidade do consumidor.Infrastructure/README.mdeInfrastructure/EfCore/README.md: nota de navegação destacando que são containers para sub-namespaces, com link direto para o nível com conteúdo real.Extensions/README.md: removidas entradas deObjectGuardseDateTimeOffsetGuardsda tabela de Componentes (movidas paraGuards/); nota cross-reference adicionada.DEVPACK.md: nova entrada paraITLab.Template.DevPack.Guardsno mapa de namespaces.README.mdraiz: inventário de READMEs refleteGuards/README.md.
Removed
Extensions/GuardExtensions.cs: substituído por três arquivos sobGuards/.src/ITLab.Template.DevPack/NameValue.csda raiz: movido paraDtos/.Directory.Packages.props: removidas pinagens dead deClosedXMLeSendGrid(pacotes haviam sido removidos do.csprojem 2.0.0).tests/ITLab.Template.DevPack.Tests/Inspector.cs: teste exploratório comtry/catchsilencioso. Tinha um[Fact] FinalInspectque sempre passava sem assertions úteis (carregavaAsp.Versioning.Httpemtry { } catch { }). Sua remoção reduz a contagem de testes em 1 (de 458 para 457) — sem perda de cobertura efetiva, já que o teste não verificava nada.script.csna raiz do repositório: código exploratório referenciando pacotes removidos (Swashbuckle.AspNetCore,Asp.Versioning.ApiExplorer). Não estava em qualquerslnx/csproj.docs/devpack-inventory.mdinterno (snapshot pré-Fase 0): conteúdo substituído por pointer ao inventário canônico mantido fora do repositório do DevPack.
[2.0.1] - 2026-05-01
Changed (Breaking)
-
Namespace
ITLab.Template.DevPack.Mediatorrenomeado paraITLab.Template.DevPack.Cqrs. A pastasrc/ITLab.Template.DevPack/Mediator/foi movida parasrc/ITLab.Template.DevPack/Cqrs/. Nenhum tipo público teve nome alterado (ICommand,IQuery,ICommandHandler,IQueryHandler,INotificationEvent,IMediatorHandler,MediatorHandler).Motivação: o nome anterior colidia visualmente com o namespace raiz
Mediator(martinothamar). Quando um arquivo do consumidor importava ambos, o IntelliSense mostrava doisICommand/IQuery/INotification(um nosso, um da biblioteca), gerando ambiguidade prejudicial — especialmente para agentes de IA. A escolha alternativa "Messaging" foi descartada porque o DevPack já tem uma pastaMessaging/reservada para integração com message broker (Service Bus). O termo "Cqrs" é específico, idiomático e sem colisão. Vejadocs/adrs/002-rename-mediator-to-cqrs.md.Migração: substituir
using ITLab.Template.DevPack.Mediator;porusing ITLab.Template.DevPack.Cqrs;em todos os arquivos do consumidor. Não há mudança em assinaturas, comportamento ou dependências.
Changed
DEVPACK.md,CLAUDE.md,README.mdraiz eCqrs/README.mdatualizados para refletir o novo nome de namespace e a ausência de colisão comMediator(martinothamar).Messaging/README.mdreescrito: o anterior foi sobrescrito durante a tentativa intermediária de renomeação que conflitou com a pasta existente; o conteúdo novo segue o mesmo padrão das outras áreas e descreveEMessageDisposition,MessageHandlingResulteMessagingErrors.
Fixed
Helpers/DomainEventHelper.TrySetTenantId: removido type check tautológico (is not IDomainEvent) e variável local de nome enganoso (multiTenantEntity), herdados de copy-paste do gêmeoEntityHelper.TrySetTenantId(IEntity). O comportamento funcional permanece idêntico — o código sempre executou o caminho feliz; a estrutura apenas escondia um null guard sob pattern matching desnecessário, gerando confusão sobre a intenção do método. Adicionado null check explícito viaArgumentNullException.ThrowIfNulle cobertura emDomainEventHelperTests(4 cenários: contexto sem usuário, tenant igual, tenant diferente, argumento nulo).
[2.0.0] — Phase 0 — Foundational refactor
This release replaces the foundational libraries to align the DevPack with the state-of-the-art .NET 10 stack. Each substitution is a clean cut: the previous library is removed entirely. There is no transition window. Consumers must migrate in one step.
Added
Mediator.Abstractions3.0.2 (Mediatorby martinothamar) replaces MediatR. Source-generated CQRS dispatch withValueTaskeverywhere.Authorization/directory: new policy-based permission system.IPermissionEvaluator— agnostic permission resolution contract.PermissionAuthorizationRequirement,PermissionAuthorizationHandler,PermissionAuthorizationPolicyProvider— ASP.NET Core authorization plumbing for dynamic permission policies (RequireAuthorization("Pedidos.Create")).RequiresPermissionAttribute— declarative permission marker for Commands and Queries (multiple form an AND).AuthorizationBehavior<TRequest, TResponse>— mediator pipeline behavior that enforces[RequiresPermission]before the handler runs, with per-type reflection cache.DevPackAuthorizationExtensions.AddDevPackAuthorization()— DI registration helper.
Authorization/README.md— full area documentation with usage example, dual endpoint+behavior convention, naming convention.Behaviors/DevPackBehaviorsExtensions.csandAddDevPackBehaviors()extension — registers the trio Logging → Validation → Normalize in the recommended order.
Changed
Mediator/ICommand,ICommand<TResponse>,ICommandHandler<...>,IQuery<TResponse>,IQueryHandler<TQuery, TResponse>,INotificationEvent,IMediatorHandler,MediatorHandlerrewritten on top ofMediator(martinothamar). Public type names preserved.- Breaking: handler
Handlesignatures now returnValueTask<TResponse>instead ofTask<TResponse>. Pipelinenextdelegate is nowMessageHandlerDelegate<TMessage, TResponse>taking(message, ct)instead ofRequestHandlerDelegate<TResponse>taking(). - Breaking:
IMediatorHandler.PublishEvent<T>returnsValueTaskinstead ofTaskand accepts aCancellationTokenparameter. - Breaking:
MediatorHandlerconstructor takesMediator.IPublisherinstead ofMediatR.IMediator. Domain/IDomainEventnow extendsMediator.INotificationinstead ofMediatR.INotification. Public surface preserved.Infrastructure/EfCore/Interceptors/DispatchDomainEventsInterceptorconstructor takesMediator.IPublisher.DispatchDomainEventsreturnsValueTaskand accepts aCancellationToken.Behaviors/ValidationPipelineBehavior,RequestLoggingPipelineBehavior,NormalizeDateTimeOffsetBehaviorrewritten on top ofMediator.IPipelineBehavior<,>. Behavior semantics preserved.CoreConsts.csmoved toCoreConsts/CoreConsts.csto align with the per-area subdirectory convention; namespace unchanged.Identity/README.md: addedBaseIdentityUser,BaseIdentityRole,CurrentUserExtensions, andIdentityProviderNameto the Componentes table.Data/README.md: addedQueryFilterto the Componentes table and a usage example.PackageTags: replacedmediatrwithmediator.
Removed
MediatRNuGet dependency.MediatR.ContractsNuGet dependency.MagicMapperNuGet dependency. The DevPack no longer ships any mapping infrastructure; products use Mapperly or hand-written extension methods.Data/GroupConverter<T>(theAutoMapper.ITypeConverteradapter for grouped query results). It was not consumed byGroupableList<T>— server-side grouping is implemented inGroupableList<T>via reflection over dynamic LINQ projections.Data/GroupKey.SplitGroupconstant — only consumed by the removedGroupConverter<T>.Swagger/directory and theConfigureSwaggerOptionsandSwaggerDefaultValuesFiltertypes. Consumers must migrate toMicrosoft.AspNetCore.OpenApiand a UI of choice (e.g.Scalar.AspNetCore).Extensions/SwaggerExtensions.csand theUseSwaggerAndUIhelper: replaced by directMapOpenApi()/MapScalarApiReference()calls in consumer projects.Swashbuckle.AspNetCoreNuGet dependency.Asp.Versioning.MvcandAsp.Versioning.Mvc.ApiExplorerNuGet dependencies. Consumers needing API versioning on Minimal APIs can installAsp.Versioning.Httpdirectly.ClosedXMLNuGet dependency: no public types or internal usage found; removed to reduce package footprint.SendGridNuGet dependency: no public types or internal usage found; removed to reduce package footprint.
Migration notes for consumers
- Replace
using MediatR;withusing Mediator;in endpoint code that injects the mediator. Handler files and command/query records useusing ITLab.Template.DevPack.Cqrs;(renamed from.Mediatorin 2.0.1) for the wrappers. - Replace
services.AddMediatR(typeof(...))withservices.AddMediator(opt => opt.Namespace = "MyApp.Mediator";)from theMediator.SourceGeneratorpackage (added in the consumer project, not in the DevPack). - Replace
services.AddSingleton(typeof(IPipelineBehavior<,>), typeof(...))calls withservices.AddDevPackBehaviors(). - Replace
Task<TResponse>handler return types withValueTask<TResponse>. - Adapt pipeline behavior implementations to
IPipelineBehavior<TMessage, TResponse>fromMediatornamespace, withwhere TMessage : IMessageconstraint andMessageHandlerDelegate<TMessage, TResponse>for the next delegate. - For mapping, replace AutoMapper/MagicMapper usage with Mapperly (
[Mapper]partial classes) or hand-written extension methods. - For OpenAPI, replace
services.AddSwaggerGen()/app.UseSwagger()/app.UseSwaggerUI()withservices.AddOpenApi()/app.MapOpenApi()/app.MapScalarApiReference()(the latter requires theScalar.AspNetCoreNuGet package). - For permissions, register
IPermissionEvaluator(your implementation) and callservices.AddDevPackAuthorization().
[1.0.0] - 2026-04-23
Added
- Initial release