765cc632d5
Remove the Order demo (entity/feature/repo/config/gRPC/proto) and the three pre-marketplace migrations; regenerate a fresh InitialBaseline migration. Stand up the REST surface (PingController + System/Ping CQRS) proving the Mediator -> behaviors -> OperationResult -> ApiResult envelope end to end. Close wiring gaps: register LoggingBehavior (outermost) and add the built-in rate limiter (per-IP global + otp/auth/sensitive policies), placed before authentication. Add current-user + audit plumbing: ICurrentUser (HttpContext + null impls), rename BaseEntity audit fields to CreatedAt/ModifiedAt (DateTimeOffset) + CreatedById/ModifiedById, stamped by a new AuditFieldInterceptor. Introduce five cross-cutting seams (IDateTimeProvider, IFieldEncryptor, ICacheService, IObjectStorage, INotificationDispatcher) with in-memory/local mocks registered via AddCrossCuttingSeams. Add Baya.Test.Foundation (encryptor, audit interceptor, ping handler) and update docs, contracts (swagger.v1.json), handoff, report, and mocks registry.
68 lines
2.3 KiB
C#
68 lines
2.3 KiB
C#
#nullable enable
|
|
using Baya.Application.Contracts.Common;
|
|
using Baya.Domain.Common;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.EntityFrameworkCore.Diagnostics;
|
|
|
|
namespace Baya.Infrastructure.Persistence.Interceptors;
|
|
|
|
/// <summary>
|
|
/// Stamps audit fields on every save: <c>CreatedAt</c>/<c>CreatedById</c> on insert and
|
|
/// <c>ModifiedAt</c>/<c>ModifiedById</c> on update, sourcing time from <see cref="IDateTimeProvider"/>
|
|
/// and the acting user from <see cref="ICurrentUser"/>. Handlers never set these fields.
|
|
/// This is the extension point backend-phase-1 builds on to also write append-only audit-log rows.
|
|
/// </summary>
|
|
public sealed class AuditFieldInterceptor(ICurrentUser currentUser, IDateTimeProvider dateTimeProvider)
|
|
: SaveChangesInterceptor
|
|
{
|
|
public override InterceptionResult<int> SavingChanges(
|
|
DbContextEventData eventData,
|
|
InterceptionResult<int> result)
|
|
{
|
|
Stamp(eventData.Context);
|
|
return base.SavingChanges(eventData, result);
|
|
}
|
|
|
|
public override ValueTask<InterceptionResult<int>> SavingChangesAsync(
|
|
DbContextEventData eventData,
|
|
InterceptionResult<int> result,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
Stamp(eventData.Context);
|
|
return base.SavingChangesAsync(eventData, result, cancellationToken);
|
|
}
|
|
|
|
private void Stamp(DbContext? context)
|
|
{
|
|
if (context is null)
|
|
return;
|
|
|
|
var now = dateTimeProvider.UtcNow;
|
|
var userId = currentUser.UserId;
|
|
|
|
foreach (var entry in context.ChangeTracker.Entries<ITimeModification>())
|
|
{
|
|
switch (entry.State)
|
|
{
|
|
case EntityState.Added:
|
|
entry.Entity.CreatedAt = now;
|
|
entry.Entity.ModifiedAt = now;
|
|
if (entry.Entity is IAuditableEntity addedAuditable)
|
|
{
|
|
addedAuditable.CreatedById = userId;
|
|
addedAuditable.ModifiedById = userId;
|
|
}
|
|
|
|
break;
|
|
|
|
case EntityState.Modified:
|
|
entry.Entity.ModifiedAt = now;
|
|
if (entry.Entity is IAuditableEntity modifiedAuditable)
|
|
modifiedAuditable.ModifiedById = userId;
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|