Spiga

ABP成长系列7:数据访问

2024-06-01 16:01:56

一、仓储

仓储模式是ABP框架数据访问层的核心抽象,它提供了一种统一的方式来访问数据源,无论底层使用何种数据库技术(EF Core、MongoDB等)。

  • 接口抽象:框架定义了一系列通用的仓储接口,如 IRepository<TEntity, TKey>。你的领域层和应用层只依赖于这些接口,而不是具体的实现,这严格遵循了DDD的持久化无关原则。
  • 默认实现:ABP为EF、MongoDB和Dapper提供了这些仓储接口的开箱即用的实现。这意味着对于大多数标准的CRUD操作,无需编写任何仓储实现代码。
  • 自定义仓储:对于复杂的查询或特定操作,可以定义自定义的仓储接口(如 ICustomRepository),并在基础设施层(如EntityFrameworkCore项目)中实现它。ABP的依赖注入系统会自动将其注册到容器中。

类关系图

1. 仓储接口体系

ABP定义了一套完整的仓储接口体系,位于Volo.Abp.Domain.Repositories命名空间:

//IRepository.cs
public interface IRepository { }

public interface IRepository<TEntity> : IReadOnlyRepository<TEntity>, IBasicRepository<TEntity>
    where TEntity : class, IEntity
{
    Task<TEntity> FindAsync([NotNull] Expression<Func<TEntity, bool>> predicate, bool includeDetails = true, CancellationToken cancellationToken = default);

    Task<TEntity> GetAsync([NotNull] Expression<Func<TEntity, bool>> predicate, bool includeDetails = true, CancellationToken cancellationToken = default);

    Task DeleteAsync([NotNull] Expression<Func<TEntity, bool>> predicate, bool autoSave = false, CancellationToken cancellationToken = default
    );
}

public interface IRepository<TEntity, TKey> : IRepository<TEntity>, IReadOnlyRepository<TEntity, TKey>, IBasicRepository<TEntity, TKey>
    where TEntity : class, IEntity<TKey> { }

//IBasicRepository.cs
public interface IBasicRepository<TEntity> : IReadOnlyBasicRepository<TEntity>
    where TEntity : class, IEntity
{
    [NotNull]
    Task<TEntity> InsertAsync([NotNull] TEntity entity, bool autoSave = false, CancellationToken cancellationToken = default);
    Task InsertManyAsync([NotNull] IEnumerable<TEntity> entities, bool autoSave = false, CancellationToken cancellationToken = default);
    [NotNull]
    Task<TEntity> UpdateAsync([NotNull] TEntity entity, bool autoSave = false, CancellationToken cancellationToken = default);
    Task UpdateManyAsync([NotNull] IEnumerable<TEntity> entities, bool autoSave = false, CancellationToken cancellationToken = default);
    Task DeleteAsync([NotNull] TEntity entity, bool autoSave = false, CancellationToken cancellationToken = default);
    Task DeleteManyAsync([NotNull] IEnumerable<TEntity> entities, bool autoSave = false, CancellationToken cancellationToken = default);
}

public interface IBasicRepository<TEntity, TKey> : IBasicRepository<TEntity>, IReadOnlyBasicRepository<TEntity, TKey>
    where TEntity : class, IEntity<TKey>
{
    Task DeleteAsync(TKey id, bool autoSave = false, CancellationToken cancellationToken = default); 
    Task DeleteManyAsync([NotNull] IEnumerable<TKey> ids, bool autoSave = false, CancellationToken cancellationToken = default);
}

//IReadOnlyRepository.cs
public interface IReadOnlyRepository<TEntity> : IReadOnlyBasicRepository<TEntity> where TEntity : class, IEntity
{
    IAsyncQueryableExecuter AsyncExecuter { get; }

    [Obsolete("Use WithDetailsAsync method.")]
    IQueryable<TEntity> WithDetails();

    [Obsolete("Use WithDetailsAsync method.")]
    IQueryable<TEntity> WithDetails(params Expression<Func<TEntity, object>>[] propertySelectors);

    Task<IQueryable<TEntity>> WithDetailsAsync(); //TODO: CancellationToken

    Task<IQueryable<TEntity>> WithDetailsAsync(params Expression<Func<TEntity, object>>[] propertySelectors); //TODO: CancellationToken

    Task<IQueryable<TEntity>> GetQueryableAsync(); //TODO: CancellationToken

    /// <summary>
    /// Gets a list of entities by the given <paramref name="predicate"/>.
    /// </summary>
    /// <param name="predicate">A condition to filter the entities</param>
    /// <param name="includeDetails">Set true to include details (sub-collections) of this entity</param>
    /// <param name="cancellationToken">A <see cref="T:System.Threading.CancellationToken" /> to observe while waiting for the task to complete.</param>
    Task<List<TEntity>> GetListAsync(
        [NotNull] Expression<Func<TEntity, bool>> predicate,
        bool includeDetails = false,
        CancellationToken cancellationToken = default);
}

public interface IReadOnlyRepository<TEntity, TKey> : IReadOnlyRepository<TEntity>, IReadOnlyBasicRepository<TEntity, TKey>
    where TEntity : class, IEntity<TKey> { }

//IReadOnlyBasicRepository.cs
public interface IReadOnlyBasicRepository<TEntity> : IRepository  where TEntity : class, IEntity
{
    Task<List<TEntity>> GetListAsync(bool includeDetails = false, CancellationToken cancellationToken = default);
    Task<long> GetCountAsync(CancellationToken cancellationToken = default);
    Task<List<TEntity>> GetPagedListAsync(
        int skipCount,
        int maxResultCount,
        string sorting,
        bool includeDetails = false,
        CancellationToken cancellationToken = default);
}

public interface IReadOnlyBasicRepository<TEntity, TKey> : IReadOnlyBasicRepository<TEntity>
    where TEntity : class, IEntity<TKey>
{
    [NotNull]
    Task<TEntity> GetAsync(TKey id, bool includeDetails = true, CancellationToken cancellationToken = default);
    Task<TEntity> FindAsync(TKey id, bool includeDetails = true, CancellationToken cancellationToken = default);
}

2. 核心:动态注册

ABP使用依赖注入系统动态提供仓储实现。当你在应用层或领域服务中注入IRepository时,ABP会根据配置的数据访问提供者提供相应的实现。

  • 动态注册:在模块的ConfigureServices方法中,ABP会调用AddDefaultRepositories来注册默认仓储
services.AddAbpDbContext<MyDbContext>(options =>
{
    options.AddDefaultRepositories(includeAllEntities: true);
});

其核心实现位于 Volo.Abp.EntityFrameworkCore.AbpDbContextRegistrationOptions

public AbpDbContextRegistrationOptions AddDefaultRepositories(bool includeAllEntities = false)
{
    foreach (var entityType in GetEntityTypes(_dbContextType))
    {
        if (!includeAllEntities && !typeof(IAggregateRoot).IsAssignableFrom(entityType))
        {
            continue;
        }

        var repositoryType = typeof(IRepository<>).MakeGenericType(entityType);
        if (!_serviceCollection.IsRegistered(repositoryType))
        {
            var implType = typeof(EfCoreRepository<,>)
                .MakeGenericType(_dbContextType, entityType);
            
            _serviceCollection.AddTransient(repositoryType, implType);
        }
    }
    
    return this;
}

3. 核心:ABP扩展

ABP提供了许多有用的仓储扩展方法,位于 Volo.Abp.Domain.Repositories 命名空间

public static class RepositoryExtensions
{
    public static async Task EnsureCollectionLoadedAsync<TEntity, TKey, TProperty>(
        this IBasicRepository<TEntity, TKey> repository,
        TEntity entity,
        Expression<Func<TEntity, IEnumerable<TProperty>>> propertyExpression,
        CancellationToken cancellationToken = default
    )
        where TEntity : class, IEntity<TKey>
        where TProperty : class
    {
        var repo = ProxyHelper.UnProxy(repository) as ISupportsExplicitLoading<TEntity, TKey>;
        if (repo != null)
        {
            await repo.EnsureCollectionLoadedAsync(entity, propertyExpression, cancellationToken);
        }
    }

    public static async Task EnsurePropertyLoadedAsync<TEntity, TKey, TProperty>(
        this IBasicRepository<TEntity, TKey> repository,
        TEntity entity,
        Expression<Func<TEntity, TProperty>> propertyExpression,
        CancellationToken cancellationToken = default
    )
        where TEntity : class, IEntity<TKey>
        where TProperty : class
    {
        var repo = ProxyHelper.UnProxy(repository) as ISupportsExplicitLoading<TEntity, TKey>;
        if (repo != null)
        {
            await repo.EnsurePropertyLoadedAsync(entity, propertyExpression, cancellationToken);
        }
    }

    public static async Task HardDeleteAsync<TEntity>(
        this IRepository<TEntity> repository,
        Expression<Func<TEntity, bool>> predicate,
        bool autoSave = false,
        CancellationToken cancellationToken = default
    )
        where TEntity : class, IEntity, ISoftDelete
    {
        var uowManager = repository.GetUnitOfWorkManager();

        if (uowManager.Current == null)
        {
            using (var uow = uowManager.Begin())
            {
                await HardDeleteWithUnitOfWorkAsync(repository, predicate, autoSave, cancellationToken, uowManager.Current);
                await uow.CompleteAsync(cancellationToken);
            }
        }
        else
        {
            await HardDeleteWithUnitOfWorkAsync(repository, predicate, autoSave, cancellationToken, uowManager.Current);
        }
    }

    public static async Task HardDeleteAsync<TEntity>(
        this IBasicRepository<TEntity> repository,
        IEnumerable<TEntity> entities,
        bool autoSave = false,
        CancellationToken cancellationToken = default
    )
        where TEntity : class, IEntity, ISoftDelete
    {
        var uowManager = repository.GetUnitOfWorkManager();

        if (uowManager.Current == null)
        {
            using (var uow = uowManager.Begin())
            {
                await HardDeleteWithUnitOfWorkAsync(repository, entities, autoSave, cancellationToken, uowManager.Current);
                await uow.CompleteAsync(cancellationToken);
            }
        }
        else
        {
            await HardDeleteWithUnitOfWorkAsync(repository, entities, autoSave, cancellationToken, uowManager.Current);
        }
    }

    public static async Task HardDeleteAsync<TEntity>(
        this IBasicRepository<TEntity> repository,
        TEntity entity,
        bool autoSave = false,
        CancellationToken cancellationToken = default
    )
        where TEntity : class, IEntity, ISoftDelete
    {
        var uowManager = repository.GetUnitOfWorkManager();

        if (uowManager.Current == null)
        {
            using (var uow = uowManager.Begin())
            {
                await HardDeleteWithUnitOfWorkAsync(repository, entity, autoSave, cancellationToken, uowManager.Current);
                await uow.CompleteAsync(cancellationToken);
            }
        }
        else
        {
            await HardDeleteWithUnitOfWorkAsync(repository, entity, autoSave, cancellationToken, uowManager.Current);
        }
    }

    private static IUnitOfWorkManager GetUnitOfWorkManager<TEntity>(
        this IBasicRepository<TEntity> repository,
        [CallerMemberName] string callingMethodName = nameof(GetUnitOfWorkManager)
    )
        where TEntity : class, IEntity
    {
        if (ProxyHelper.UnProxy(repository) is not IUnitOfWorkManagerAccessor unitOfWorkManagerAccessor)
        {
            throw new AbpException($"The given repository (of type {repository.GetType().AssemblyQualifiedName}) should implement the " +
                $"{typeof(IUnitOfWorkManagerAccessor).AssemblyQualifiedName} interface in order to invoke the {callingMethodName} method!");
        }

        if (unitOfWorkManagerAccessor.UnitOfWorkManager == null)
        {
            throw new AbpException($"{nameof(unitOfWorkManagerAccessor.UnitOfWorkManager)} property of the given {nameof(repository)} object is null!");
        }

        return unitOfWorkManagerAccessor.UnitOfWorkManager;
    }

    private static async Task HardDeleteWithUnitOfWorkAsync<TEntity>(
        IRepository<TEntity> repository,
        Expression<Func<TEntity, bool>> predicate,
        bool autoSave,
        CancellationToken cancellationToken,
        IUnitOfWork currentUow
    )
        where TEntity : class, IEntity, ISoftDelete
    {
        using (currentUow.ServiceProvider.GetRequiredService<IDataFilter<ISoftDelete>>().Disable())
        {
            var entities = await repository.AsyncExecuter.ToListAsync((await repository.GetQueryableAsync()).Where(predicate), cancellationToken);
            await HardDeleteWithUnitOfWorkAsync(repository, entities, autoSave, cancellationToken, currentUow);
        }
    }

    private static async Task HardDeleteWithUnitOfWorkAsync<TEntity>(
        IBasicRepository<TEntity> repository,
        IEnumerable<TEntity> entities,
        bool autoSave,
        CancellationToken cancellationToken,
        IUnitOfWork currentUow
    )
        where TEntity : class, IEntity, ISoftDelete
    {
        var hardDeleteEntities = (HashSet<IEntity>)currentUow.Items.GetOrAdd(
            UnitOfWorkItemNames.HardDeletedEntities,
            () => new HashSet<IEntity>()
        );

        hardDeleteEntities.UnionWith(entities);
        await repository.DeleteManyAsync(entities, autoSave, cancellationToken);
    }

    private static async Task HardDeleteWithUnitOfWorkAsync<TEntity>(
        IBasicRepository<TEntity> repository,
        TEntity entity,
        bool autoSave,
        CancellationToken cancellationToken,
        IUnitOfWork currentUow
    )
        where TEntity : class, IEntity, ISoftDelete
    {
        var hardDeleteEntities = (HashSet<IEntity>)currentUow.Items.GetOrAdd(
            UnitOfWorkItemNames.HardDeletedEntities,
            () => new HashSet<IEntity>()
        );

        hardDeleteEntities.Add(entity);
        await repository.DeleteAsync(entity, autoSave, cancellationToken);
    }
}

4. 核心:异步执行与取消

ABP仓储的所有方法都支持异步操作和取消令牌

public virtual async Task UpdateManyAsync(IEnumerable<TEntity> entities, bool autoSave = false, CancellationToken cancellationToken = default)
{
    foreach (var entity in entities)
    {
        await UpdateAsync(entity, cancellationToken: cancellationToken);
    }

    if (autoSave)
    {
        await SaveChangesAsync(cancellationToken);
    }
}

二、工作单元

类关系图

1. 机制

  • 自动工作单元:通过 [UnitOfWork] 特性标记方法
  • 手动工作单元:通过注入 IUnitOfWorkManager 手动控制
public class BookAppService : ApplicationService
{
    private readonly IUnitOfWorkManager _unitOfWorkManager;
    
    public BookAppService(IUnitOfWorkManager unitOfWorkManager)
    {
        _unitOfWorkManager = unitOfWorkManager;
    }
    
    public async Task CreateAsync(CreateBookDto input)
    {
        using (var uow = _unitOfWorkManager.Begin())
        {
            var book = new Book { Name = input.Name };
            await _bookRepository.InsertAsync(book);
            
            await uow.CompleteAsync();
        }
    }
}

数据库事务行为

  • HTTP GET请求不会启动事务性UOW,它们仍然启动UOW,但不创建数据库事务。
  • 如果底层数据库提供程序支持数据库事务,那么所有其他HTTP请求类型都使用数据库事务启动UOW。

2. 工作单元的事务控制

  • 通过 [UnitOfWork] 特性的属性控制事务行为
[UnitOfWork(
    IsDisabled = false, // 是否禁用工作单元
    IsTransactional = true, // 是否启用事务
    Timeout = 30, // 超时时间(秒)
    IsolationLevel = IsolationLevel.ReadCommitted // 隔离级别
)]
public async Task UpdateBookAsync(Guid id, UpdateBookDto input)
{
    // 方法实现...
}
  • 事务API:ABP定义了 ITransactionApi 接口来抽象不同数据库的事务
public interface ITransactionApi : IDisposable
{
    Task CommitAsync(CancellationToken cancellationToken = default);
    Task RollbackAsync(CancellationToken cancellationToken = default);
}

3. 嵌套工作单元

ABP支持嵌套工作单元,内部工作单元可以继承或覆盖外部工作单元的配置

[UnitOfWork(IsTransactional = true)]
public async Task OuterMethod()
{
    // 外部工作单元
    
    await _repository.InsertAsync(new Entity());
    
    // 内部工作单元默认继承外部工作单元的事务
    await InnerMethod();
    
    // 可以显式创建新工作单元
    using (var uow = _unitOfWorkManager.Begin(requiresNew: true))
    {
        await _repository.InsertAsync(new Entity());
        await uow.CompleteAsync();
    }
}

[UnitOfWork(IsTransactional = false)]
public async Task InnerMethod()
{
    // 此方法覆盖了外部工作单元的IsTransactional设置
    await _repository.InsertAsync(new Entity());
}

4. 事件系统

ABP工作单元提供了完整的事件通知机制

public class MyService : ITransientDependency
{
    private readonly IUnitOfWorkManager _unitOfWorkManager;
    
    public MyService(IUnitOfWorkManager unitOfWorkManager)
    {
        _unitOfWorkManager = unitOfWorkManager;
        
        // 订阅工作单元事件
        _unitOfWorkManager.Current.OnCompleted += OnUowCompleted;
        _unitOfWorkManager.Current.OnFailed += OnUowFailed;
        _unitOfWorkManager.Current.OnDisposed += OnUowDisposed;
    }
    
    private void OnUowCompleted(object sender, UnitOfWorkEventArgs e)
    {
        Logger.LogInformation($"Unit of work {e.UnitOfWork.Id} completed");
    }
    
    private void OnUowFailed(object sender, UnitOfWorkFailedEventArgs e)
    {
        Logger.LogError(e.Exception, $"Unit of work {e.UnitOfWork.Id} failed");
    }
    
    private void OnUowDisposed(object sender, UnitOfWorkEventArgs e)
    {
        Logger.LogInformation($"Unit of work {e.UnitOfWork.Id} disposed");
    }
}

**5. 核心1:IUnitOfWork **

public interface IUnitOfWork : IDatabaseApiContainer, ITransactionApiContainer, IDisposable
{
    // 工作单元唯一标识
    Guid Id { get; }
    
    // 工作单元选项
    IUnitOfWorkOptions Options { get; }
    
    // 外部工作单元(用于嵌套工作单元)
    IUnitOfWork Outer { get; }
    
    // 是否已释放
    bool IsDisposed { get; }
    
    // 是否已完成
    bool IsCompleted { get; }
    
    // 工作单元事件
    event EventHandler<UnitOfWorkEventArgs> OnCompleted;
    event EventHandler<UnitOfWorkFailedEventArgs> OnFailed;
    event EventHandler<UnitOfWorkEventArgs> OnDisposed;
    
    // 工作单元操作方法
    void Initialize(UnitOfWorkOptions options);
    Task SaveChangesAsync(CancellationToken cancellationToken = default);
    Task CompleteAsync(CancellationToken cancellationToken = default);
    Task RollbackAsync(CancellationToken cancellationToken = default);
    
    // 添加/获取数据库API
    void AddDatabaseApi(IDatabaseApi databaseApi);
    IDatabaseApi GetDatabaseApi();
    
    // 添加/获取事务API
    void AddTransactionApi(ITransactionApi transactionApi);
    ITransactionApi GetTransactionApi();
    
    // 添加/获取特性
    void AddFeature(string key, object value);
    TFeature GetFeature<TFeature>(string key);
}

6. 核心2:UnitOfWorkManager

UnitOfWorkManager负责创建工作单元实例和管理当前工作单元

  • Begin
  • Current
public class UnitOfWorkManager : IUnitOfWorkManager, ISingletonDependency
{
    // 当前工作单元
    public IUnitOfWork Current => _currentUow.Value;
    
    // 创建工作单元
    public IUnitOfWork Begin(UnitOfWorkOptions options, bool requiresNew = false)
    {
        // 如果不需要新工作单元且已有当前工作单元,则返回null(由内部处理嵌套)
        if (!requiresNew && _currentUow.Value != null)
        {
            return new ChildUnitOfWork(_currentUow.Value);
        }
        
        // 创建新工作单元
        var unitOfWork = _unitOfWork.CreateNew();
        unitOfWork.Initialize(options);
        
        // 设置当前工作单元
        _currentUow.Value = unitOfWork;
        
        return unitOfWork;
    }
    
    // 其他方法...
}

7: 核心3:UseUnitOfWork

ABP使用UseUnitOfWork在ASP.NET Core项目中集成工作单元模式,也就是启动了一个AbpUnitOfWorkMiddleware中间件

public static IApplicationBuilder UseUnitOfWork(this IApplicationBuilder app)
{
    return app
        .UseAbpExceptionHandling()
        .UseMiddleware<AbpUnitOfWorkMiddleware>();
}
public class AbpUnitOfWorkMiddleware : IMiddleware, ITransientDependency
{
    private readonly IUnitOfWorkManager _unitOfWorkManager;
    private readonly AbpAspNetCoreUnitOfWorkOptions _options;

    public AbpUnitOfWorkMiddleware(
        IUnitOfWorkManager unitOfWorkManager,
        IOptions<AbpAspNetCoreUnitOfWorkOptions> options)
    {
        _unitOfWorkManager = unitOfWorkManager;
        _options = options.Value;
    }

    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        if (IsIgnoredUrl(context))
        {
            await next(context);
            return;
        }

        using (var uow = _unitOfWorkManager.Reserve(UnitOfWork.UnitOfWorkReservationName))
        {
            await next(context);
            await uow.CompleteAsync(context.RequestAborted);
        }
    }

    private bool IsIgnoredUrl(HttpContext context)
    {
        return context.Request.Path.Value != null &&
               _options.IgnoredUrls.Any(x => context.Request.Path.Value.StartsWith(x));
    }
}

三、数据过滤

1. 内置数据过滤器

  • 软删除(ISoftDelete):自动过滤已删除的实体
  • 多租户(IMultiTenant):自动按租户ID过滤数据

2. 核心1:核心接口

public interface IDataFilter
{
    IDisposable Enable<TFilter>() where TFilter : class;
    IDisposable Disable<TFilter>() where TFilter : class;
    bool IsEnabled<TFilter>() where TFilter : class;
}

3. 核心2:EF的实现

// 源码位置:Volo.Abp.EntityFrameworkCore.AbpDbContext.cs
protected override Expression<Func<TEntity, bool>> CreateFilterPredicate<TEntity>()
{
    var predicate = base.CreateFilterPredicate<TEntity>();
    
    // 软删除过滤
    if (typeof(ISoftDelete).IsAssignableFrom(typeof(TEntity)) 
        && DataFilter?.IsEnabled<ISoftDelete>() == true)
    {
        Expression<Func<TEntity, bool>> softDeleteFilter = e => !EF.Property<bool>(e, "IsDeleted");
        predicate = predicate == null 
            ? softDeleteFilter 
            : QueryFilterExpressionHelper.CombineExpressions(predicate, softDeleteFilter);
    }
    
    // 多租户过滤
    if (typeof(IMultiTenant).IsAssignableFrom(typeof(TEntity)) 
        && DataFilter?.IsEnabled<IMultiTenant>() == true)
    {
        var tenantId = CurrentTenant.Id;
        Expression<Func<TEntity, bool>> multiTenantFilter = e => EF.Property<Guid>(e, "TenantId") == tenantId;
        predicate = predicate == null 
            ? multiTenantFilter 
            : QueryFilterExpressionHelper.CombineExpressions(predicate, multiTenantFilter);
    }
    
    return predicate;
}

4. 核心3:MongoDb的实现

// 源码位置:Volo.Abp.MongoDB.AbpMongoDbRepository.cs
protected virtual FilterDefinition<TEntity> CreateFilterDefinition()
{
    var filters = new List<FilterDefinition<TEntity>>();
    
    // 软删除过滤
    if (typeof(ISoftDelete).IsAssignableFrom(typeof(TEntity)) 
        && DataFilter?.IsEnabled<ISoftDelete>() == true)
    {
        filters.Add(Builders<TEntity>.Filter.Eq(x => ((ISoftDelete)x).IsDeleted, false));
    }
    
    // 多租户过滤
    if (typeof(IMultiTenant).IsAssignableFrom(typeof(TEntity)) 
        && DataFilter?.IsEnabled<IMultiTenant>() == true)
    {
        filters.Add(Builders<TEntity>.Filter.Eq(x => ((IMultiTenant)x).TenantId, CurrentTenant.Id));
    }
    
    return filters.Count > 0 
        ? Builders<TEntity>.Filter.And(filters) 
        : FilterDefinition<TEntity>.Empty;
}

EF和MongoDb的实现逻辑不一样,EF是在AbpDbContext中,而MongoDb是在仓储中。

4. 自定义过滤器:数据权限

现在,我们来实现一个自定义的数据过滤器,实现数据权限控制:管理员可以查看所有数据,普通用户只能查看自己的数据。

  1. 定义数据权限接口
public interface IDataPermissionFilter
{
    // 这是一个标记接口,用于标识数据权限过滤器
}

public interface IHasOwner
{
    Guid? OwnerId { get; }
}
  1. 实现数据权限过滤器提供者
public class MyDbContext : AbpDbContext<MyDbContext>
{
    public DbSet<Book> Books { get; set; }
    
    protected override void OnModelCreating(ModelBuilder builder)
    {
        base.OnModelCreating(builder);
        
        builder.Entity<Book>(b =>
        {
            b.Property(x => x.OwnerId).HasColumnName("OwnerId");
        });
    }
    
    protected override Expression<Func<TEntity, bool>> CreateFilterPredicate<TEntity>()
    {
        var predicate = base.CreateFilterPredicate<TEntity>();
        
        // 添加数据权限过滤
        if (typeof(IHasOwner).IsAssignableFrom(typeof(TEntity)) 
            && DataFilter?.IsEnabled<IDataPermissionFilter>() == true)
        {
            var currentUserId = CurrentUser.Id;
            
            if (!CurrentUser.IsInRole("admin") && currentUserId.HasValue)
            {
                Expression<Func<TEntity, bool>> dataPermissionFilter = 
                    e => EF.Property<Guid?>(e, "OwnerId") == currentUserId;
                
                predicate = predicate == null 
                    ? dataPermissionFilter 
                    : QueryFilterExpressionHelper.CombineExpressions(predicate, dataPermissionFilter);
            }
        }
        
        return predicate;
    }
}
  1. 实体上实现接口
public class Book : AggregateRoot<Guid>, IHasOwner
{
    public string Name { get; set; }
    public Guid? OwnerId { get; set; } // 实现IHasOwner接口
}
  1. 使用数据过滤器
public class BookAppService : ApplicationService
{
    private readonly IRepository<Book, Guid> _bookRepository;
    private readonly IDataFilter _dataFilter;
    
    public BookAppService(IRepository<Book, Guid> bookRepository, IDataFilter dataFilter)
    {
        _bookRepository = bookRepository;
        _dataFilter = dataFilter;
    }
    
    // 普通查询会自动应用数据权限过滤
    public async Task<List<BookDto>> GetMyBooksAsync()
    {
        var books = await _bookRepository.GetListAsync();
        return ObjectMapper.Map<List<Book>, List<BookDto>>(books);
    }
    
    // 管理员可以禁用过滤器查看所有数据
    public async Task<List<BookDto>> GetAllBooksAsync()
    {
        using (_dataFilter.Disable<IDataPermissionFilter>())
        {
            var books = await _bookRepository.GetListAsync();
            return ObjectMapper.Map<List<Book>, List<BookDto>>(books);
        }
    }
}
  1. 注册配置
[DependsOn(typeof(AbpDataModule))]
public class MyModule : AbpModule
{
    public override void ConfigureServices(ServiceConfigurationContext context)
    {
        // 注册自定义DbContext
        context.Services.AddAbpDbContext<MyDbContext>(options =>
        {
            options.AddDefaultRepositories();
        });
    }
}

扩展:与权限模块配合

protected override Expression<Func<TEntity, bool>> CreateFilterPredicate<TEntity>()
{
    var predicate = base.CreateFilterPredicate<TEntity>();
    
    if (typeof(IHasOwner).IsAssignableFrom(typeof(TEntity)) 
        && DataFilter?.IsEnabled<IDataPermissionFilter>() == true)
    {
        if (await PermissionChecker.IsGrantedAsync("AllDataPermission"))
        {
            // 有全部数据权限,不过滤
            return predicate;
        }
        
        if (await PermissionChecker.IsGrantedAsync("DepartmentDataPermission"))
        {
            // 部门数据权限
            var departmentIds = await GetUserDepartmentIdsAsync();
            Expression<Func<TEntity, bool>> deptFilter = 
                e => departmentIds.Contains(EF.Property<Guid>(e, "DepartmentId"));
            
            predicate = predicate == null 
                ? deptFilter 
                : QueryFilterExpressionHelper.CombineExpressions(predicate, deptFilter);
        }
        else
        {
            // 默认只能查看自己的数据
            var currentUserId = CurrentUser.Id;
            if (currentUserId.HasValue)
            {
                Expression<Func<TEntity, bool>> ownerFilter = 
                    e => EF.Property<Guid?>(e, "OwnerId") == currentUserId;
                
                predicate = predicate == null 
                    ? ownerFilter 
                    : QueryFilterExpressionHelper.CombineExpressions(predicate, ownerFilter);
            }
            else
            {
                // 未登录用户看不到任何数据
                predicate = e => false;
            }
        }
    }
    
    return predicate;
}

四、数据提供者

1. EFCore类关系图

2. MongoDb类关系图

五、数据审计

ABP的数据审计系统是一个强大的功能,它能够自动记录实体变更历史,为系统提供完整的操作追踪能力。

类关系图

1. 审计系统核心架构

  • IAuditingManager:审计管理器的接口

  • AuditingManager:审计管理器的默认实现

  • IAuditingStore:审计存储接口

  • AuditLogInfo:审计日志的数据结构

  • AuditingInterceptor:AOP拦截器

2. 核心1:AuditingManager

AuditingManager 作为审计系统的"大脑",主要承担以下职责:

  • 审计流程的启动与协调
  • 审计日志的创建与组装
  • 审计贡献者的调用执行
  • 审计日志的最终提交
public class AuditingManager : IAuditingManager, ITransientDependency
{
    private const string AmbientContextKey = "Volo.Abp.Auditing.IAuditLogScope";

    protected IServiceProvider ServiceProvider { get; }
    protected AbpAuditingOptions Options { get; }
    protected ILogger<AuditingManager> Logger { get; set; }
    private readonly IAmbientScopeProvider<IAuditLogScope> _ambientScopeProvider;
    private readonly IAuditingHelper _auditingHelper;
    private readonly IAuditingStore _auditingStore;

    public AuditingManager(
        IAmbientScopeProvider<IAuditLogScope> ambientScopeProvider,
        IAuditingHelper auditingHelper,
        IAuditingStore auditingStore,
        IServiceProvider serviceProvider,
        IOptions<AbpAuditingOptions> options)
    {
        ServiceProvider = serviceProvider;
        Options = options.Value;
        Logger = NullLogger<AuditingManager>.Instance;

        _ambientScopeProvider = ambientScopeProvider;
        _auditingHelper = auditingHelper;
        _auditingStore = auditingStore;
    }

    public IAuditLogScope Current => _ambientScopeProvider.GetValue(AmbientContextKey);

    public IAuditLogSaveHandle BeginScope()
    {
        // 1. 创建基础审计日志信息
        _auditingHelper.CreateAuditLogInfo()
        
        // 2. 创建审计作用域
    	var auditLogScope = new AuditLogScope(auditLogInfo);
        
        // 3. 注册到环境作用域提供者
        var ambientScope = _ambientScopeProvider.BeginScope(
            AmbientContextKey,
            auditLogScope	
        );

        Debug.Assert(Current != null, "Current != null");
		
        // 4. 返回保存句柄
        return new DisposableSaveHandle(this, ambientScope, Current.Log, Stopwatch.StartNew());
    }
    
    // 其他内部方法...
}

设计亮点

  • 环境上下文管理:
    • 使用 IAmbientScopeProvider 管理作用域
    • 支持异步调用链中的上下文传递
  • 延迟提交机制
    • 通过 IAuditLogSaveHandle 控制提交时机
    • 支持异常情况下的优雅处理
  • 线程安全设计
    • 通过作用域隔离保证并发安全
    • 使用不可变对象(AuditLogInfo)避免竞态条件

3. 核心2:审计拦截器

位于 Volo.Abp.Auditing 命名空间下的 AuditingInterceptor 是审计系统的入口点:

public class AuditingInterceptor : AbpInterceptor, ITransientDependency
{
    public override async Task InterceptAsync(IAbpMethodInvocation invocation)
    {
        using (var serviceScope = _serviceScopeFactory.CreateScope())
        {
            var auditingHelper = serviceScope.ServiceProvider.GetRequiredService<IAuditingHelper>();
            var auditingOptions = serviceScope.ServiceProvider.GetRequiredService<IOptions<AbpAuditingOptions>>().Value;

            if (!ShouldIntercept(invocation, auditingOptions, auditingHelper))
            {
                await invocation.ProceedAsync();
                return;
            }

            var auditingManager = serviceScope.ServiceProvider.GetRequiredService<IAuditingManager>();
            if (auditingManager.Current != null)
            {
                await ProceedByLoggingAsync(invocation, auditingHelper, auditingManager.Current);
            }
            else
            {
                var currentUser = serviceScope.ServiceProvider.GetRequiredService<ICurrentUser>();
                await ProcessWithNewAuditingScopeAsync(invocation, auditingOptions, currentUser, auditingManager, auditingHelper);
            }
        }
    }
}

4. 核心3:审计日志创建过程

AuditingHelper 类负责创建审计日志:

public virtual AuditLogInfo CreateAuditLogInfo()
{
    var auditInfo = new AuditLogInfo
    {
        ApplicationName = Options.ApplicationName,
        TenantId = CurrentTenant.Id,
        TenantName = CurrentTenant.Name,
        UserId = CurrentUser.Id,
        UserName = CurrentUser.UserName,
        ClientId = CurrentClient.Id,
        CorrelationId = CorrelationIdProvider.Get(),
        ExecutionTime = Clock.Now,
        ImpersonatorUserId = CurrentUser.FindImpersonatorUserId(),
        ImpersonatorUserName = CurrentUser.FindImpersonatorUserName(),
        ImpersonatorTenantId = CurrentUser.FindImpersonatorTenantId(),
        ImpersonatorTenantName = CurrentUser.FindImpersonatorTenantName(),
    };

    ExecutePreContributors(auditInfo);

    return auditInfo;
}

protected virtual void ExecutePreContributors(AuditLogInfo auditLogInfo)
{
    using (var scope = ServiceProvider.CreateScope())
    {
        var context = new AuditLogContributionContext(scope.ServiceProvider, auditLogInfo);

        foreach (var contributor in Options.Contributors)
        {
            try
            {
                contributor.PreContribute(context);
            }
            catch (Exception ex)
            {
                Logger.LogException(ex, LogLevel.Warning);
            }
        }
    }
}

public class AuditLogInfo : IHasExtraProperties
{
	public AuditLogInfo()
    {
        Actions = new List<AuditLogActionInfo>();
        Exceptions = new List<Exception>();
        ExtraProperties = new ExtraPropertyDictionary();
        EntityChanges = new List<EntityChangeInfo>();
        Comments = new List<string>();
    }
    // 其他代码
}

public class EntityChangeInfo
{
	public List<EntityPropertyChangeInfo> PropertyChanges { get; set; }
	// 其它代码
}

5. 实体变更审计

ABP 通过 Volo.Abp.EntityFrameworkCore.EntityHistory.EntityHistoryHelper 来跟踪实体的变更:

public class EntityHistoryHelper : IEntityHistoryHelper, ITransientDependency
{
    public virtual List<EntityChangeInfo> CreateChangeList(ICollection<EntityEntry> entityEntries)
    {
        var list = new List<EntityChangeInfo>();

        foreach (var entry in entityEntries)
        {
            if (!ShouldSaveEntityHistory(entry))
            {
                continue;
            }

            var entityChange = CreateEntityChangeOrNull(entry);
            if (entityChange == null)
            {
                continue;
            }

            list.Add(entityChange);
        }

        return list;
    }

    [CanBeNull]
    protected virtual EntityChangeInfo CreateEntityChangeOrNull(EntityEntry entityEntry)
    {
        var entity = entityEntry.Entity;

        EntityChangeType changeType;
        switch (entityEntry.State)
        {
            case EntityState.Added:
                changeType = EntityChangeType.Created;
                break;
            case EntityState.Deleted:
                changeType = EntityChangeType.Deleted;
                break;
            case EntityState.Modified:
                changeType = IsDeleted(entityEntry) ? EntityChangeType.Deleted : EntityChangeType.Updated;
                break;
            case EntityState.Detached:
            case EntityState.Unchanged:
            default:
                return null;
        }

        var entityId = GetEntityId(entity);
        if (entityId == null && changeType != EntityChangeType.Created)
        {
            return null;
        }

        var entityType = entity.GetType();
        var entityChange = new EntityChangeInfo
        {
            ChangeType = changeType,
            EntityEntry = entityEntry,
            EntityId = entityId,
            EntityTypeFullName = entityType.FullName,
            PropertyChanges = GetPropertyChanges(entityEntry),
            EntityTenantId = GetTenantId(entity)
        };

        return entityChange;
    }
}

6. 审计存储机制

ABP 提供了默认的审计存储实现 SimpleLogAuditingStore,但通常我们会实现自己的存储:

[Dependency(TryRegister = true)]
public class SimpleLogAuditingStore : IAuditingStore, ISingletonDependency
{
    public ILogger<SimpleLogAuditingStore> Logger { get; set; }

    public SimpleLogAuditingStore()
    {
        Logger = NullLogger<SimpleLogAuditingStore>.Instance;
    }

    public Task SaveAsync(AuditLogInfo auditInfo)
    {
        Logger.LogInformation(auditInfo.ToString());
        return Task.FromResult(0);
    }
}

7. 审计日志处理流程

  1. 拦截阶段:AuditingInterceptor 拦截方法调用
  2. 日志创建:AuditingHelper 创建基础审计日志
  3. 贡献者处理:调用所有注册的 AuditLogContributor
  4. 实体变更检测:EntityHistoryHelper 检测实体变更
  5. 存储阶段:调用 IAuditingStore 实现存储日志

8. 自定义审计

ABP的审计系统非常强大,其 IAuditLogContributor 接口允许我们深度定制审计日志内容。下面我将通过一个库存管理模块示例,详细说明如何实现自定义审计贡献者。

假设我们有一个库存管理系统,需要详细记录以下信息到审计日志:

  • 库存变更详情:包括商品ID、变更前库存、变更后库存、安全库存水平等。
  • 业务操作上下文:如触发此次库存变更的订单号或调拨单号。
  • 自定义业务标签:例如操作类型(如“采购入库”、“销售出库”、“盘点调整”)。
  1. 创建自定义 AuditLogContributor
public class InventoryAuditLogContributor : AuditLogContributor, ITransientDependency
{
    public override Task ContributeAsync(AuditLogContributionContext context)
    {
        // 1. 获取当前审计日志信息
        var auditLogInfo = context.AuditInfo;

        // 2. 从当前作用域或静态访问器中获取自定义业务数据
        // 这里假设我们有一个自定义的上下文提供者,用于在方法调用间传递业务数据
        var inventoryChangeContext = InventoryChangeContext.Current;
        if (inventoryChangeContext != null)
        {
            // 3. 将业务数据添加到审计日志的 ExtraProperties 字典中
            auditLogInfo.ExtraProperties["ProductId"] = inventoryChangeContext.ProductId;
            auditLogInfo.ExtraProperties["OldStockQuantity"] = inventoryChangeContext.OldQuantity;
            auditLogInfo.ExtraProperties["NewStockQuantity"] = inventoryChangeContext.NewQuantity;
            auditLogInfo.ExtraProperties["SafetyStockLevel"] = inventoryChangeContext.SafetyStockLevel;
            auditLogInfo.ExtraProperties["ReferenceOrderNumber"] = inventoryChangeContext.ReferenceOrderNumber;
            auditLogInfo.ExtraProperties["OperationType"] = inventoryChangeContext.OperationType;

            // 4. (可选)你也可以直接添加到 Comments 或自定义结构
            auditLogInfo.Comments.Add($"库存操作: {inventoryChangeContext.OperationType}. 订单参考: {inventoryChangeContext.ReferenceOrderNumber}");
        }

        return Task.CompletedTask;
    }
}
  1. 创建上下文对象(用于传递业务数据)

我们需要一个方式来在方法执行过程中传递这些业务数据。可以使用 AsyncLocal 来实现一个简单的上下文作用域。

public class InventoryChangeContext
{
    private static readonly AsyncLocal<InventoryChangeContext> _current = new AsyncLocal<InventoryChangeContext>();
    public static InventoryChangeContext Current => _current.Value;

    public static void SetCurrent(InventoryChangeContext context)
    {
    	_current.Value = context;
    }

    public Guid ProductId { get; set; }
    public int OldQuantity { get; set; }
    public int NewQuantity { get; set; }
    public int SafetyStockLevel { get; set; }
    public string ReferenceOrderNumber { get; set; }
    public string OperationType { get; set; } // 例如:"PurchaseIn", "SaleOut", "StocktakeAdjustment"
}
  1. 在应用服务方法中使用上下文
public class ProductAppService : ApplicationService, IProductAppService
{
    private readonly IRepository<Product, Guid> _productRepository;

    public ProductAppService(IRepository<Product, Guid> productRepository)
    {
    	_productRepository = productRepository;
    }

    public async Task UpdateStockAsync(Guid productId, int newQuantity, string operationType, string referenceOrderNumber)
    {
        // 1. 获取当前商品
        var product = await _productRepository.GetAsync(productId);
        var oldQuantity = product.StockQuantity;

        // 2. 创建并设置审计日志上下文
        var auditContext = new InventoryChangeContext
        {
            ProductId = productId,
            OldQuantity = oldQuantity,
            NewQuantity = newQuantity,
            SafetyStockLevel = product.SafetyStockLevel,
            ReferenceOrderNumber = referenceOrderNumber,
            OperationType = operationType
        };
        InventoryChangeContext.SetCurrent(auditContext); // 设置到AsyncLocal

        try
        {
        // 3. 执行实际的业务操作(更新库存)
            product.UpdateStock(newQuantity);
            await _productRepository.UpdateAsync(product);

            // 4. 审计日志将由拦截器自动捕获,并通过InventoryAuditLogContributor贡献自定义数据
        }
        finally
        {
            // 5. 清理上下文(重要!避免内存泄漏或数据污染后续操作)
            InventoryChangeContext.SetCurrent(null);
    	}
    }
}
  1. 注册依赖(通常无需手动注册)

由于 InventoryAuditLogContributor 实现了 ITransientDependency,ABP框架会自动将其注册到依赖注入容器中。审计系统在保存日志前会自动调用所有实现的 IAuditLogContributor。

  1. 检查效果

当 UpdateStockAsync 方法被调用后,审计日志的 ExtraProperties 字段将包含你添加的自定义数据。如果你使用的是ABP内置的审计日志模块,这些数据会以JSON格式保存在 AbpAuditLogs 表的 ExtraProperties 列中

// AbpAuditLogs 表 ExtraProperties 列的内容示例
{
  "ProductId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "OldStockQuantity": 100,
  "NewStockQuantity": 80,
  "SafetyStockLevel": 50,
  "ReferenceOrderNumber": "SO-20230909-001",
  "OperationType": "SaleOut"
}

9. 最佳实践

  • 实现自定义的 IAuditingStore 将审计日志存储到数据库
  • 对于高频操作的方法,考虑使用 [DisableAuditing] 特性
  • 通过 AbpAuditingOptions 配置审计系统的行为
  • 使用 AuditLogContributor 添加业务相关的审计信息