Files
baya-monorepo/server/src/Infrastructure/Baya.Infrastructure.Persistence/Interceptors/AuditFieldInterceptor.cs
T
hamid 765cc632d5 backend phase 0: foundation, cross-cutting seams & starter cleanup
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.
2026-06-30 22:48:41 +03:30

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;
}
}
}
}