درج زمان مشترک برای فیلد های Audit در رکورد های درج و ویرایش
نویسنده: میلاد احمدی مقدم
تاریخ: ۱۴۰۵/۰۴/۱۹ ۱۷:۴۳
آدرس: www.dntips.ir
CreatedAt 2026-07-10 13:57:49.8865870 2026-07-10 13:57:49.8865976
public interface IApplicationDateTimeService
{
DateTime Now();
}
public sealed class ApplicationDateTimeService : IApplicationDateTimeService
{
public DateTime Now() => DateTime.Now;
}UpdateAuditableEntitiesInterceptor استفاده میکنم و چون از DbContextPool استفاده میکنم و ApplicationDateTimeService به صورت Scoped ثبت شدهservices.AddSingleton<UpdateAuditableEntitiesInterceptor>(); services.AddScoped<IApplicationDateTimeService, ApplicationDateTimeService>();
services.AddSingleton<IApplicationDateTimeService, ApplicationDateTimeService>();
public interface IApplicationDateTimeService
{
DateTime Now();
void SetCurrentTimeForRequest(DateTime time);
}
====
public sealed class ApplicationDateTimeService : IApplicationDateTimeService
{
private readonly AsyncLocal<DateTime?> _currentTime = new();
public DateTime Now()
{
return _currentTime.Value
?? throw new InvalidOperationException(
"Application time has not been initialized.");
}
public void SetCurrentTimeForRequest(DateTime time)
{
_currentTime.Value = time;
}
}app.UseHttpsRedirection();
app.Use(async (context, next) =>
{
var dateService = context.RequestServices
.GetRequiredService<IApplicationDateTimeService>();
dateService.SetCurrentTimeForRequest(DateTime.Now);
await next();
});
...
...
await app.RunAsync();public sealed class UpdateAuditableEntitiesInterceptor(
IHttpContextAccessor httpContextAccessor,
IApplicationDateTimeService appDateTimeService) : SaveChangesInterceptor
{
public override ValueTask<InterceptionResult<int>> SavingChangesAsync(
DbContextEventData eventData,
InterceptionResult<int> result,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(eventData);
var db = eventData.Context;
if (db is null)
{
return base.SavingChangesAsync(eventData, result, cancellationToken);
}
var now = appDateTimeService.Now();
var userId = TryGetUserId();
foreach (var entry in db.ChangeTracker.Entries<IAuditable>()
.Where(e => e.State is EntityState.Added or EntityState.Modified))
{
if (entry.State == EntityState.Added)
{
entry.Property(AuditShadowPropertyNames.CreatedAt).CurrentValue = now;
Set(entry, AuditShadowPropertyNames.CreatedBy, userId);
}
else if (entry.State == EntityState.Modified && HasAnyChanges(entry))
{
entry.Property(AuditShadowPropertyNames.UpdatedAt).CurrentValue = now;
Set(entry, AuditShadowPropertyNames.UpdatedBy, userId);
}
}
return base.SavingChangesAsync(eventData, result, cancellationToken);
}
private static bool HasAnyChanges(EntityEntry entry)
{
return entry.Properties.Any(p => p.IsModified);
}
private static void Set(EntityEntry e, string name, object? value)
{
var p = e.Properties.FirstOrDefault(x => x.Metadata.Name.Equals(name, StringComparison.Ordinal));
if (p is null)
return;
var t = p.Metadata.ClrType;
if (t == typeof(Guid?) || t == typeof(Guid))
{
p.CurrentValue = value;
}
else if ((t == typeof(UserId?) || t == typeof(UserId)) && value is Guid g)
{
p.CurrentValue = new UserId(g);
}
}
private Guid? TryGetUserId()
{
var user = httpContextAccessor.HttpContext?.User;
var s = user?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
return Guid.TryParse(s, out var g) ? g : null;
}
}var serviceT = httpContextAccessor.HttpContext.RequestServices.GetRequiredService<T>();
builder.Services.AddScoped<UpdateAuditableEntitiesInterceptor>(); // Register as Scoped
builder.Services.AddDbContext<MyDbContext>((sp, options) =>
{
var interceptor = sp.GetRequiredService<UpdateAuditableEntitiesInterceptor>();
options.UseSqlServer(connectionString)
.AddInterceptors(interceptor);
});public class ApplicationDbContext : DbContext
{
private readonly IServiceProvider _serviceProvider;
public ApplicationDbContext(
DbContextOptions<ApplicationDbContext> options,
IServiceProvider serviceProvider)
: base(options)
{
_serviceProvider = serviceProvider;
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
// Resolve the scoped interceptor from the current scope
var scopedInterceptor = _serviceProvider.GetRequiredService<UpdateAuditableEntitiesInterceptor>();
optionsBuilder.AddInterceptors(scopedInterceptor);
base.OnConfiguring(optionsBuilder);
}
}builder.Services.AddScoped<UpdateAuditableEntitiesInterceptor>();
builder.Services.AddDbContextPool<ApplicationDbContext>(options =>
{
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"));
});UpdateAuditableEntitiesInterceptor به صورت Scopedمشکلم حل میشه ولی چون آموزش های زیادی رو دیده بودم که همگی تمامی Interceptor ها را به صورت Singleton رجیستر میکردن من فکر میکردم که این یک قانونه که بهتره همیشه Interceptor ها به صورت Singleton رجیستر بشن که الان هم شما گفتید مشکل نداره به صورت Scoped ثبت بشه هم از ChatGPT پرسیدم و در جواب اینو گفت: