You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

ASP.NET Core项目中如何实现不污染实体接口的业务化半自动审计日志,且确保操作全记录?

ASP.NET Core项目中如何实现不污染实体接口的业务化半自动审计日志,且确保操作全记录?

你提出的这个需求非常贴合实际业务场景——比起冷冰冰的属性变更记录,带有业务语义的审计日志才真正能帮团队快速定位问题、追溯操作。而且你担心的「实体类被审计逻辑污染」「操作记录遗漏」这两个核心痛点,完全可以通过领域事件+中介者模式+审计处理器的组合方案解决,既符合DDD的分层原则,又能保障审计的可靠性。


核心思路:用领域事件解耦业务与审计

核心逻辑是:让实体类只负责完成业务逻辑并发布「操作完成」的事件,审计逻辑完全交给独立的处理器来处理。这样实体类不需要知道任何审计、认证相关的细节,同时只要业务操作被执行,对应的事件就会触发审计,从机制上避免遗漏。


1. 定义领域事件:承载业务操作的语义信息

首先,我们需要为每个业务操作定义对应的领域事件,事件中只包含审计需要的业务核心数据,不需要涉及审计逻辑或用户信息。

第一步:定义领域事件基础接口

public interface IDomainEvent
{
    Guid EventId { get; }
    DateTime OccurredOn { get; }
}

第二步:为具体业务操作定义事件

比如学生选课、考试成绩记录的事件:

// 学生选课事件
public record StudentEnrolledEvent(string StudentId, string CourseId) : IDomainEvent
{
    public Guid EventId => Guid.NewGuid();
    public DateTime OccurredOn => DateTime.UtcNow;
}

// 考试成绩记录事件
public record ExamResultRecordedEvent(string StudentId, string ExamId, string Result, string Grade) : IDomainEvent
{
    public Guid EventId => Guid.NewGuid();
    public DateTime OccurredOn => DateTime.UtcNow;
}

2. 实体类:只负责业务逻辑与事件发布

实体类的业务方法(Enroll、PassExam等)在完成核心业务逻辑后,仅发布对应的领域事件,完全不涉及审计、用户ID等外部信息,保持领域层的纯粹性。

我们可以给所有实体定义一个基类,统一管理领域事件:

public abstract class EntityBase
{
    private readonly List<IDomainEvent> _domainEvents = new();
    public IReadOnlyCollection<IDomainEvent> DomainEvents => _domainEvents.AsReadOnly();

    protected void AddDomainEvent(IDomainEvent domainEvent)
    {
        _domainEvents.Add(domainEvent);
    }

    public void ClearDomainEvents()
    {
        _domainEvents.Clear();
    }
}

然后是Student实体类:

public class Student : EntityBase
{
    public string Id { get; private set; }
    public List<Course> EnrolledCourses { get; private set; } = new();
    public List<ExamResult> ExamResults { get; private set; } = new();

    // 业务方法:选课
    public void Enroll(Course course)
    {
        // 核心业务逻辑:校验是否已选课、课程是否可报名等
        if (EnrolledCourses.Any(c => c.Id == course.Id))
            throw new InvalidOperationException("学生已选该课程");

        EnrolledCourses.Add(course);
        // 发布领域事件:通知外部「学生完成选课」
        AddDomainEvent(new StudentEnrolledEvent(Id, course.Id));
    }

    // 业务方法:记录考试通过结果
    public void PassExam(Exam exam, string grade)
    {
        // 核心业务逻辑:校验考试是否对应学生、成绩是否合法等
        var examResult = new ExamResult(Id, exam.Id, "Pass", grade);
        ExamResults.Add(examResult);
        // 发布领域事件:通知外部「学生考试通过」
        AddDomainEvent(new ExamResultRecordedEvent(Id, exam.Id, "Pass", grade));
    }

    // 业务方法:记录考试不及格结果
    public void FailExam(Exam exam)
    {
        var examResult = new ExamResult(Id, exam.Id, "Fail", null);
        ExamResults.Add(examResult);
        AddDomainEvent(new ExamResultRecordedEvent(Id, exam.Id, "Fail", null));
    }
}

3. 审计处理器:独立处理事件生成审计日志

审计逻辑完全放在基础设施层的处理器中,处理器负责监听领域事件,获取当前用户信息,生成并保存审计日志实体。这里可以依赖IHttpContextAccessor获取当前登录用户ID,或者注入专门的用户信息服务。

第一步:定义审计日志实体

public class StudentAuditTrail
{
    public int Id { get; set; }
    public string StudentId { get; set; }
    public string Action { get; set; }
    public DateTime Timestamp { get; set; }
    public string UserId { get; set; }
    public string Details { get; set; }
}

第二步:实现事件处理器(以MediatR为例)

我们可以用MediatR这个成熟的中介者库来分发事件和处理审计:

学生选课事件的审计处理器

public class StudentEnrolledAuditHandler : INotificationHandler<StudentEnrolledEvent>
{
    private readonly AppDbContext _dbContext;
    private readonly ICurrentUserService _currentUserService;

    public StudentEnrolledAuditHandler(AppDbContext dbContext, ICurrentUserService currentUserService)
    {
        _dbContext = dbContext;
        _currentUserService = currentUserService;
    }

    public async Task Handle(StudentEnrolledEvent notification, CancellationToken cancellationToken)
    {
        // 获取当前操作的用户ID(从认证上下文或服务中获取)
        var operaterUserId = _currentUserService.GetCurrentUserId();
        
        // 生成审计日志
        var auditLog = new StudentAuditTrail
        {
            StudentId = notification.StudentId,
            Action = "Enrollment",
            Timestamp = notification.OccurredOn,
            UserId = operaterUserId,
            Details = JsonSerializer.Serialize(new { CourseId = notification.CourseId })
        };

        // 保存审计日志
        await _dbContext.StudentAuditTrails.AddAsync(auditLog, cancellationToken);
        await _dbContext.SaveChangesAsync(cancellationToken);
    }
}

考试结果事件的审计处理器

public class ExamResultAuditHandler : INotificationHandler<ExamResultRecordedEvent>
{
    private readonly AppDbContext _dbContext;
    private readonly ICurrentUserService _currentUserService;

    public ExamResultAuditHandler(AppDbContext dbContext, ICurrentUserService currentUserService)
    {
        _dbContext = dbContext;
        _currentUserService = currentUserService;
    }

    public async Task Handle(ExamResultRecordedEvent notification, CancellationToken cancellationToken)
    {
        var operaterUserId = _currentUserService.GetCurrentUserId();
        var auditLog = new StudentAuditTrail
        {
            StudentId = notification.StudentId,
            Action = "ExamResult",
            Timestamp = notification.OccurredOn,
            UserId = operaterUserId,
            Details = JsonSerializer.Serialize(new 
            { 
                ExamId = notification.ExamId,
                Result = notification.Result,
                Grade = notification.Grade 
            })
        };

        await _dbContext.StudentAuditTrails.AddAsync(auditLog, cancellationToken);
        await _dbContext.SaveChangesAsync(cancellationToken);
    }
}

其中,ICurrentUserService是获取当前用户的抽象,实现可以用IHttpContextAccessor:

public interface ICurrentUserService
{
    string GetCurrentUserId();
}

public class CurrentUserService : ICurrentUserService
{
    private readonly IHttpContextAccessor _httpContextAccessor;

    public CurrentUserService(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }

    public string GetCurrentUserId()
    {
        return _httpContextAccessor.HttpContext?.User.FindFirstValue(ClaimTypes.NameIdentifier) ?? string.Empty;
    }
}

4. 确保事件被自动处理:拦截EF Core的SaveChanges

为了避免在应用服务层手动发布事件,我们可以用EF Core的SaveChanges拦截器,在保存更改时自动捕获实体的领域事件并分发给处理器:

public class DomainEventInterceptor : SaveChangesInterceptor
{
    private readonly IMediator _mediator;

    public DomainEventInterceptor(IMediator mediator)
    {
        _mediator = mediator;
    }

    public override async ValueTask<InterceptionResult<int>> SavingChangesAsync(DbContextEventData eventData, InterceptionResult<int> result, CancellationToken cancellationToken = default)
    {
        var dbContext = eventData.Context;
        if (dbContext == null) return result;

        // 捕获所有带有领域事件的实体
        var entitiesWithEvents = dbContext.ChangeTracker
            .Entries<EntityBase>()
            .Where(e => e.Entity.DomainEvents.Any())
            .Select(e => e.Entity);

        // 分发所有领域事件
        foreach (var entity in entitiesWithEvents)
        {
            var events = entity.DomainEvents.ToList();
            entity.ClearDomainEvents();
            foreach (var domainEvent in events)
            {
                await _mediator.Publish(domainEvent, cancellationToken);
            }
        }

        return await base.SavingChangesAsync(eventData, result, cancellationToken);
    }
}

然后在Program.cs中注册拦截器:

builder.Services.AddDbContext<AppDbContext>((sp, options) =>
{
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"));
    // 添加领域事件拦截器
    options.AddInterceptors(sp.GetRequiredService<DomainEventInterceptor>());
});

// 注册MediatR、当前用户服务等
builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly()));
builder.Services.AddScoped<ICurrentUserService, CurrentUserService>();
builder.Services.AddHttpContextAccessor();

方案优势对比

你的原有方案本方案的改进
传递UserId给实体方法实体完全不需要知道用户信息,由处理器从服务获取
注入服务到实体实体与基础设施层完全解耦,仅依赖领域事件接口
实体引用Auditor实体不依赖任何审计相关服务,单一职责更清晰
控制器处理审计审计由事件驱动,只要业务操作执行就一定会触发审计,从机制上避免遗漏

额外注意事项

  1. 事务一致性:可以用EF Core的事务范围,确保业务操作和审计日志要么都成功,要么都失败,避免数据不一致。
  2. 无上下文场景:如果是后台任务等没有HttpContext的场景,可以在应用服务层传入用户ID到事件中,或者修改ICurrentUserService的实现来适配。
  3. 代码规范保障:通过单元测试确保实体的业务方法一定会触发对应的领域事件,避免业务逻辑完成但事件未发布的情况。

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.07 07:09:48