[Abp vNext 源碼分析] - 14. EntityFramework Core 的集成

来源:https://www.cnblogs.com/myzony/archive/2020/01/08/12165354.html
-Advertisement-
Play Games

一、簡要介紹 在以前的文章裡面,我們介紹了 ABP vNext 在 DDD 模塊定義了倉儲的介面定義和基本實現。本章將會介紹,ABP vNext 是如何將 EntityFramework Core 框架跟倉儲進行深度集成。 ABP vNext 在集成 EF Core 的時候,不只是簡單地實現了倉儲模 ...


一、簡要介紹

在以前的文章裡面,我們介紹了 ABP vNext 在 DDD 模塊定義了倉儲的介面定義和基本實現。本章將會介紹,ABP vNext 是如何將 EntityFramework Core 框架跟倉儲進行深度集成。

ABP vNext 在集成 EF Core 的時候,不只是簡單地實現了倉儲模式,除開倉儲以外,還提供了一系列的基礎設施,如領域事件的發佈,數據過濾器的實現。

二、源碼分析

EntityFrameworkCore 相關的模塊基本就下麵幾個,除了第一個是核心 EntityFrameworkCore 模塊以外,其他幾個都是封裝的 EntityFrameworkCore Provider,方便各種資料庫進行集成。

2.1 EF Core 模塊集成與初始化

首先從 Volo.Abp.EntityFrameworkCoreAbpEntityFrameworkCoreModule 開始分析,該模塊只重寫了 ConfigureServices() 方法,在內部也只有兩句代碼。

public override void ConfigureServices(ServiceConfigurationContext context)
{
    // 調用 AbpDbContextOptions 的預配置方法,為瞭解決下麵的問題。
    // https://stackoverflow.com/questions/55369146/eager-loading-include-with-using-uselazyloadingproxies
    Configure<AbpDbContextOptions>(options =>
    {
        options.PreConfigure(abpDbContextConfigurationContext =>
        {
            abpDbContextConfigurationContext.DbContextOptions
                .ConfigureWarnings(warnings =>
                {
                    warnings.Ignore(CoreEventId.LazyLoadOnDisposedContextWarning);
                });
        });
    });

    // 註冊 IDbContextProvider 組件。
    context.Services.TryAddTransient(typeof(IDbContextProvider<>), typeof(UnitOfWorkDbContextProvider<>));
}

首先看第一句代碼,它在內部會調用 AbpDbContextOptions 提供的 PreConfigure() 方法。這個方法邏輯很簡單,會將傳入的 Action<AbpDbContextConfigurationContext> 委托添加到一個 List<Action<AbpDbContextConfigurationContext>> 集合,並且在 DbContextOptionsFactory 工廠中使用。

第二局代碼則比較簡單,為 IDbContextProvider<> 類型註入預設實現 UnitOfWorkDbContextProvider<>

public class AbpDbContextOptions
{
    internal List<Action<AbpDbContextConfigurationContext>> DefaultPreConfigureActions { get; set; }

    // ...

    public void PreConfigure([NotNull] Action<AbpDbContextConfigurationContext> action)
    {
        Check.NotNull(action, nameof(action));

        DefaultPreConfigureActions.Add(action);
    }

    // ...
}


從上面的代碼可以看出來,這個 AbpDbContextConfigurationContext 就是一個配置上下文,用於 ABP vNext 框架在初始化的時候進行各種配置。

2.1.1 EF Core Provider 的集成

在翻閱 AbpDbContextOptions 代碼的時候,我發現除了預配置方法,它還提供了一個 Configure([NotNull] Action<AbpDbContextConfigurationContext> action) 方法,以及它的泛型重載 Configure<TDbContext>([NotNull] Action<AbpDbContextConfigurationContext<TDbContext>> action),它們的內部實現與預配置類似。

這兩個方法在 ABP vNext 框架內部的應用,主要在各個 EF Provider 模塊當中有體現。

這裡我以 Volo.Abp.EntityFrameworkCore.PostgreSql 模塊作為例子,在項目內部只有兩個擴展方法的定義類。在 AbpDbContextOptionsPostgreSqlExtensions 當中,就使用到了 Configure() 方法。

public static void UsePostgreSql(
    [NotNull] this AbpDbContextOptions options,
    [CanBeNull] Action<NpgsqlDbContextOptionsBuilder> postgreSqlOptionsAction = null)
{
    options.Configure(context =>
    {
        // 這裡的 context 類型是 AbpDbContextConfigurationContext。
        context.UsePostgreSql(postgreSqlOptionsAction);
    });
}

上面代碼中的 UsePostgreSql() 方法很明顯不是 EF Core Provider 所定義的擴展方法,跳轉到具體實現,發現就是一層簡單的封裝。由於 AbpDbContextConfigurationContext 內部提供了 DbContextOptionsBuilder ,所以直接使用這個 DbContextOptionsBuilder 調用提供的擴展方法即可。

public static class AbpDbContextConfigurationContextPostgreSqlExtensions
{
    public static DbContextOptionsBuilder UsePostgreSql(
        [NotNull] this AbpDbContextConfigurationContext context,
        [CanBeNull] Action<NpgsqlDbContextOptionsBuilder> postgreSqlOptionsAction = null)
    {
        if (context.ExistingConnection != null)
        {
            return context.DbContextOptions.UseNpgsql(context.ExistingConnection, postgreSqlOptionsAction);
        }
        else
        {
            return context.DbContextOptions.UseNpgsql(context.ConnectionString, postgreSqlOptionsAction);
        }
    }
}

2.1.2 資料庫上下文的配置工廠

無論是 PreConfigure() 的委托集合,還是 Configure() 配置的委托,都會在 DbContextOptionsFactory 提供的 Create<TDbContext>(IServiceProvider serviceProvider) 方法中被使用。該方法的作用只有一個,執行框架的配置方法,然後生成資料庫上下文的配置對象。

internal static class DbContextOptionsFactory
{
    public static DbContextOptions<TDbContext> Create<TDbContext>(IServiceProvider serviceProvider)
        where TDbContext : AbpDbContext<TDbContext>
    {
        // 獲取一個 DbContextCreationContext 對象。
        var creationContext = GetCreationContext<TDbContext>(serviceProvider);

        // 依據 creationContext 信息構造一個配置上下文。
        var context = new AbpDbContextConfigurationContext<TDbContext>(
            creationContext.ConnectionString,
            serviceProvider,
            creationContext.ConnectionStringName,
            creationContext.ExistingConnection
        );

        // 獲取 AbpDbOptions 配置。
        var options = GetDbContextOptions<TDbContext>(serviceProvider);

        // 從 Options 當中獲取添加的 PreConfigure 與 Configure 委托,並執行。
        PreConfigure(options, context);
        Configure(options, context);

        // 
        return context.DbContextOptions.Options;
    }

    // ...
}

首先我們來看看 GetCreationContext<TDbContext>() 方法是如何構造一個 DbContextCreationContext 對象的,它會優先從 Current 取得一個上下文對象,如果存在則直接返回,不存在則使用連接字元串等信息構建一個新的上下文對象。

private static DbContextCreationContext GetCreationContext<TDbContext>(IServiceProvider serviceProvider)
    where TDbContext : AbpDbContext<TDbContext>
{
    // 優先從一個 AsyncLocal 當中獲取。
    var context = DbContextCreationContext.Current;
    if (context != null)
    {
        return context;
    }

    // 從 TDbContext 的 ConnectionStringName 特性獲取連接字元串名稱。
    var connectionStringName = ConnectionStringNameAttribute.GetConnStringName<TDbContext>();
    // 使用 IConnectionStringResolver 根據指定的名稱獲得連接字元串。
    var connectionString = serviceProvider.GetRequiredService<IConnectionStringResolver>().Resolve(connectionStringName);

    // 構造一個新的 DbContextCreationContext 對象。
    return new DbContextCreationContext(
        connectionStringName,
        connectionString
    );
}

2.1.3 連接字元串解析器

與老版本的 ABP 一樣,ABP vNext 將連接字元串解析的工作,抽象了一個解析器。連接字元串解析器預設有兩種實現,適用於普通系統和多租戶系統。

普通的解析器,名字叫做 DefaultConnectionStringResolver,它的連接字元串都是從 AbpDbConnectionOptions 當中獲取的,而這個 Option 最終是從 IConfiguration 映射過來的,一般來說就是你 appsetting.json 文件當中的連接字元串配置。

多租戶解析器 的實現叫做 MultiTenantConnectionStringResolver,它的內部核心邏輯就是獲得到當前的租戶,並查詢租戶所對應的連接字元串,這樣就可以實現每個租戶都擁有不同的資料庫實例。

2.1.4 資料庫上下文配置工廠的使用

回到最開始的地方,方法 Create<TDbContext>(IServiceProvider serviceProvider) 在什麼地方會被使用呢?跳轉到唯一的調用點是在 AbpEfCoreServiceCollectionExtensions 靜態類的內部,它提供的 AddAbpDbContext<TDbContext>() 方法內部,就使用了 Create<TDbContext>() 作為 DbContextOptions<TDbContext> 的工廠方法。

public static class AbpEfCoreServiceCollectionExtensions
{
    public static IServiceCollection AddAbpDbContext<TDbContext>(
        this IServiceCollection services, 
        Action<IAbpDbContextRegistrationOptionsBuilder> optionsBuilder = null)
        where TDbContext : AbpDbContext<TDbContext>
    {
        services.AddMemoryCache();

        // 構造一個資料庫註冊配置對象。
        var options = new AbpDbContextRegistrationOptions(typeof(TDbContext), services);
        // 回調傳入的委托。
        optionsBuilder?.Invoke(options);

        // 註入指定 TDbContext 的 DbOptions<TDbContext> ,將會使用 Create<TDbContext> 方法進行瞬時對象構造。
        services.TryAddTransient(DbContextOptionsFactory.Create<TDbContext>);

        // 替換指定類型的 DbContext 為當前 TDbContext。
        foreach (var dbContextType in options.ReplacedDbContextTypes)
        {
            services.Replace(ServiceDescriptor.Transient(dbContextType, typeof(TDbContext)));
        }

        // 構造 EF Core 倉儲註冊器,並添加倉儲。
        new EfCoreRepositoryRegistrar(options).AddRepositories();

        return services;
    }
}

2.2 倉儲的註入與實現

關於倉儲的註入,其實在之前的文章就有講過,這裡我就大概說一下情況。

在上述代碼當中,調用了 AddAbpDbContext<TDbContext>() 方法之後,就會通過 Repository Registrar 進行倉儲註入。

public virtual void AddRepositories()
{
    // 遍歷用戶添加的自定義倉儲。
    foreach (var customRepository in Options.CustomRepositories)
    {
        // 調用 AddDefaultRepository() 方法註入倉儲。
        Options.Services.AddDefaultRepository(customRepository.Key, customRepository.Value);
    }

    // 判斷是否需要註冊實體的預設倉儲。
    if (Options.RegisterDefaultRepositories)
    {
        RegisterDefaultRepositories();
    }
}

可以看到,在註入倉儲的時候,分為兩種情況。第一種是用戶的自定義倉儲,這種倉儲是通過 AddRepository() 方法添加的,添加之後將會把它的 實體類型倉儲類型 放在一個字典內部。在倉儲註冊器進行初始化的時候,就會遍歷這個字典,進行註入動作。

第二種情況則是用戶在設置了 RegisterDefaultRepositories=true 的情況下,ABP vNext 就會從資料庫上下文的類型定義上遍歷所有實體類型,然後進行預設倉儲註冊。

具體的倉儲註冊實現:

public static IServiceCollection AddDefaultRepository(this IServiceCollection services, Type entityType, Type repositoryImplementationType)
{
    // 註冊 IReadOnlyBasicRepository<TEntity>。
    var readOnlyBasicRepositoryInterface = typeof(IReadOnlyBasicRepository<>).MakeGenericType(entityType);
    // 如果具體實現類型繼承了該介面,則進行註入。
    if (readOnlyBasicRepositoryInterface.IsAssignableFrom(repositoryImplementationType))
    {
        services.TryAddTransient(readOnlyBasicRepositoryInterface, repositoryImplementationType);

        // 註冊 IReadOnlyRepository<TEntity>。
        var readOnlyRepositoryInterface = typeof(IReadOnlyRepository<>).MakeGenericType(entityType);
        if (readOnlyRepositoryInterface.IsAssignableFrom(repositoryImplementationType))
        {
            services.TryAddTransient(readOnlyRepositoryInterface, repositoryImplementationType);
        }

        // 註冊 IBasicRepository<TEntity>。
        var basicRepositoryInterface = typeof(IBasicRepository<>).MakeGenericType(entityType);
        if (basicRepositoryInterface.IsAssignableFrom(repositoryImplementationType))
        {
            services.TryAddTransient(basicRepositoryInterface, repositoryImplementationType);

            // 註冊 IRepository<TEntity>。
            var repositoryInterface = typeof(IRepository<>).MakeGenericType(entityType);
            if (repositoryInterface.IsAssignableFrom(repositoryImplementationType))
            {
                services.TryAddTransient(repositoryInterface, repositoryImplementationType);
            }
        }
    }

    // 獲得實體的主鍵類型,如果不存在則忽略。
    var primaryKeyType = EntityHelper.FindPrimaryKeyType(entityType);
    if (primaryKeyType != null)
    {
        // 註冊 IReadOnlyBasicRepository<TEntity, TKey>。
        var readOnlyBasicRepositoryInterfaceWithPk = typeof(IReadOnlyBasicRepository<,>).MakeGenericType(entityType, primaryKeyType);
        if (readOnlyBasicRepositoryInterfaceWithPk.IsAssignableFrom(repositoryImplementationType))
        {
            services.TryAddTransient(readOnlyBasicRepositoryInterfaceWithPk, repositoryImplementationType);

            // 註冊 IReadOnlyRepository<TEntity, TKey>。
            var readOnlyRepositoryInterfaceWithPk = typeof(IReadOnlyRepository<,>).MakeGenericType(entityType, primaryKeyType);
            if (readOnlyRepositoryInterfaceWithPk.IsAssignableFrom(repositoryImplementationType))
            {
                services.TryAddTransient(readOnlyRepositoryInterfaceWithPk, repositoryImplementationType);
            }

            // 註冊 IBasicRepository<TEntity, TKey>。
            var basicRepositoryInterfaceWithPk = typeof(IBasicRepository<,>).MakeGenericType(entityType, primaryKeyType);
            if (basicRepositoryInterfaceWithPk.IsAssignableFrom(repositoryImplementationType))
            {
                services.TryAddTransient(basicRepositoryInterfaceWithPk, repositoryImplementationType);

                // 註冊 IRepository<TEntity, TKey>。
                var repositoryInterfaceWithPk = typeof(IRepository<,>).MakeGenericType(entityType, primaryKeyType);
                if (repositoryInterfaceWithPk.IsAssignableFrom(repositoryImplementationType))
                {
                    services.TryAddTransient(repositoryInterfaceWithPk, repositoryImplementationType);
                }
            }
        }
    }

    return services;
}

回到倉儲自動註冊的地方,可以看到實現類型是由 GetDefaultRepositoryImplementationType() 方法提供的。

protected virtual void RegisterDefaultRepository(Type entityType)
{
    Options.Services.AddDefaultRepository(
        entityType,
        GetDefaultRepositoryImplementationType(entityType)
    );
}

protected virtual Type GetDefaultRepositoryImplementationType(Type entityType)
{
    var primaryKeyType = EntityHelper.FindPrimaryKeyType(entityType);

    if (primaryKeyType == null)
    {
        return Options.SpecifiedDefaultRepositoryTypes
            ? Options.DefaultRepositoryImplementationTypeWithoutKey.MakeGenericType(entityType)
            : GetRepositoryType(Options.DefaultRepositoryDbContextType, entityType);
    }

    return Options.SpecifiedDefaultRepositoryTypes
        ? Options.DefaultRepositoryImplementationType.MakeGenericType(entityType, primaryKeyType)
        : GetRepositoryType(Options.DefaultRepositoryDbContextType, entityType, primaryKeyType);
}

protected abstract Type GetRepositoryType(Type dbContextType, Type entityType);

protected abstract Type GetRepositoryType(Type dbContextType, Type entityType, Type primaryKeyType);

這裡的兩個 GetRepositoryType() 都是抽象方法,具體的實現分別在 EfCoreRepositoryRegistrarMemoryDbRepositoryRegistrarMongoDbRepositoryRegistrar 的內部,這裡我們只講 EF Core 相關的。

protected override Type GetRepositoryType(Type dbContextType, Type entityType)
{
    return typeof(EfCoreRepository<,>).MakeGenericType(dbContextType, entityType);
}

可以看到,在方法內部是構造了一個 EfCoreRepository 類型作為預設倉儲的實現。

2.3 資料庫上下文提供者

在 Ef Core 倉儲的內部,需要操作資料庫時,必須要獲得一個資料庫上下文。在倉儲內部的資料庫上下文都是由 IDbContextProvider<TDbContext> 提供了,這個東西在 EF Core 模塊初始化的時候就已經被註冊,它的預設實現是 UnitOfWorkDbContextProvider<TDbContext>

public class EfCoreRepository<TDbContext, TEntity> : RepositoryBase<TEntity>, IEfCoreRepository<TEntity>
    where TDbContext : IEfCoreDbContext
    where TEntity : class, IEntity
{
    public virtual DbSet<TEntity> DbSet => DbContext.Set<TEntity>();

    DbContext IEfCoreRepository<TEntity>.DbContext => DbContext.As<DbContext>();

    protected virtual TDbContext DbContext => _dbContextProvider.GetDbContext();

    // ...
    
    private readonly IDbContextProvider<TDbContext> _dbContextProvider;

    // ...

    public EfCoreRepository(IDbContextProvider<TDbContext> dbContextProvider)
    {
        _dbContextProvider = dbContextProvider;
        
        // ...
    }

    // ...
}

首先來看一下這個實現類的基本定義,比較簡單,註入了兩個介面,分別用於獲取工作單元和構造 DbContext。需要註意的是,這裡通過 where 約束來指定 TDbContext 必須實現 IEfCoreDbContext 介面。

public class UnitOfWorkDbContextProvider<TDbContext> : IDbContextProvider<TDbContext>
    where TDbContext : IEfCoreDbContext
{
    private readonly IUnitOfWorkManager _unitOfWorkManager;
    private readonly IConnectionStringResolver _connectionStringResolver;

    public UnitOfWorkDbContextProvider(
        IUnitOfWorkManager unitOfWorkManager,
        IConnectionStringResolver connectionStringResolver)
    {
        _unitOfWorkManager = unitOfWorkManager;
        _connectionStringResolver = connectionStringResolver;
    }

    // ...
}

接著想下看,介面只定義了一個方法,就是 GetDbContext(),在這個預設實現裡面,首先會從緩存裡面獲取資料庫上下文,如果沒有獲取到,則創建一個新的資料庫上下文。

public TDbContext GetDbContext()
{
    // 獲得當前的可用工作單元。
    var unitOfWork = _unitOfWorkManager.Current;
    if (unitOfWork == null)
    {
        throw new AbpException("A DbContext can only be created inside a unit of work!");
    }

    // 獲得資料庫連接上下文的連接字元串名稱。
    var connectionStringName = ConnectionStringNameAttribute.GetConnStringName<TDbContext>();
    // 根據名稱解析具體的連接字元串。
    var connectionString = _connectionStringResolver.Resolve(connectionStringName);

    // 構造資料庫上下文緩存 Key。
    var dbContextKey = $"{typeof(TDbContext).FullName}_{connectionString}";

    // 從工作單元的緩存當中獲取資料庫上下文,不存在則調用 CreateDbContext() 創建。
    var databaseApi = unitOfWork.GetOrAddDatabaseApi(
        dbContextKey,
        () => new EfCoreDatabaseApi<TDbContext>(
            CreateDbContext(unitOfWork, connectionStringName, connectionString)
        ));

    return ((EfCoreDatabaseApi<TDbContext>)databaseApi).DbContext;
}

回到最開始的資料庫上下文配置工廠,在它的內部會優先從一個 Current 獲取一個 DbContextCreationContext 實例。而在這裡,就是 Current 被賦值的地方,只要調用了 Use() 方法,在釋放之前都會獲取到同一個實例。

private TDbContext CreateDbContext(IUnitOfWork unitOfWork, string connectionStringName, string connectionString)
{
    var creationContext = new DbContextCreationContext(connectionStringName, connectionString);
    using (DbContextCreationContext.Use(creationContext))
    {
        // 這裡是重點,真正創建資料庫上下文的地方。
        var dbContext = CreateDbContext(unitOfWork);

        if (unitOfWork.Options.Timeout.HasValue &&
            dbContext.Database.IsRelational() &&
            !dbContext.Database.GetCommandTimeout().HasValue)
        {
            dbContext.Database.SetCommandTimeout(unitOfWork.Options.Timeout.Value.TotalSeconds.To<int>());
        }

        return dbContext;
    }
}

// 如果是事務型的工作單元,則調用 CreateDbContextWithTransaction() 進行創建,但不論如何都是通過工作單元提供的 IServiceProvider 解析出來 DbContext 的。
private TDbContext CreateDbContext(IUnitOfWork unitOfWork)
{
    return unitOfWork.Options.IsTransactional
        ? CreateDbContextWithTransaction(unitOfWork)
        : unitOfWork.ServiceProvider.GetRequiredService<TDbContext>();
}

以下代碼才是在真正地創建 DbContext 實例。

public TDbContext CreateDbContextWithTransaction(IUnitOfWork unitOfWork) 
{
    var transactionApiKey = $"EntityFrameworkCore_{DbContextCreationContext.Current.ConnectionString}";
    var activeTransaction = unitOfWork.FindTransactionApi(transactionApiKey) as EfCoreTransactionApi;

    // 沒有取得緩存。
    if (activeTransaction == null)
    {
        var dbContext = unitOfWork.ServiceProvider.GetRequiredService<TDbContext>();

        // 判斷是否指定了事務隔離級別,並開始事務。
        var dbtransaction = unitOfWork.Options.IsolationLevel.HasValue
            ? dbContext.Database.BeginTransaction(unitOfWork.Options.IsolationLevel.Value)
            : dbContext.Database.BeginTransaction();

        // 跟工作單元綁定添加一個已經激活的事務。
        unitOfWork.AddTransactionApi(
            transactionApiKey,
            new EfCoreTransactionApi(
                dbtransaction,
                dbContext
            )
        );

        // 返回構造好的資料庫上下文。
        return dbContext;
    }
    else
    {
        DbContextCreationContext.Current.ExistingConnection = activeTransaction.DbContextTransaction.GetDbTransaction().Connection;

        var dbContext = unitOfWork.ServiceProvider.GetRequiredService<TDbContext>();

        if (dbContext.As<DbContext>().HasRelationalTransactionManager())
        {
            dbContext.Database.UseTransaction(activeTransaction.DbContextTransaction.GetDbTransaction());
        }
        else
        {
            dbContext.Database.BeginTransaction(); //TODO: Why not using the new created transaction?
        }

        activeTransaction.AttendedDbContexts.Add(dbContext);

        return dbContext;
    }
}

2.4 數據過濾器

ABP vNext 還提供了數據過濾器機制,可以讓你根據指定的標識過濾數據,例如租戶 Id 和軟刪除標記。它的基本介面定義在 Volo.Abp.Data 項目的 IDataFilter.cs 文件中,提供了啟用、禁用、檢測方法。

public interface IDataFilter<TFilter>
    where TFilter : class
{
    IDisposable Enable();

    IDisposable Disable();

    bool IsEnabled { get; }
}

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

    bool IsEnabled<TFilter>()
        where TFilter : class;
}

預設實現也在該項目下麵的 DataFilter.cs 文件,首先看以下 IDataFilter 的預設實現 DataFilter,內部有一個解析器和併發字典。這個併發字典存儲了所有的過濾器,其鍵是真實過濾器的類型(ISoftDeleteIMultiTenant),值是 DataFilter<TFilter>,具體對象根據 TFilter 的不同而不同。

public class DataFilter : IDataFilter, ISingletonDependency
{
    private readonly ConcurrentDictionary<Type, object> _filters;

    private readonly IServiceProvider _serviceProvider;

    public DataFilter(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
        _filters = new ConcurrentDictionary<Type, object>();
    }

    // ...
}

看一下其他的方法,都是對 IDataFilter<Filter> 的包裝。

public class DataFilter : IDataFilter, ISingletonDependency
{
    // ...

    public IDisposable Enable<TFilter>()
        where TFilter : class
    {
        return GetFilter<TFilter>().Enable();
    }

    public IDisposable Disable<TFilter>()
        where TFilter : class
    {
        return GetFilter<TFilter>().Disable();
    }

    public bool IsEnabled<TFilter>()
        where TFilter : class
    {
        return GetFilter<TFilter>().IsEnabled;
    }

    private IDataFilter<TFilter> GetFilter<TFilter>()
        where TFilter : class
    {
        // 併發字典當中獲取指定類型的過濾器,如果不存在則從 IoC 中解析。
        return _filters.GetOrAdd(
            typeof(TFilter),
            () => _serviceProvider.GetRequiredService<IDataFilter<TFilter>>()
        ) as IDataFilter<TFilter>;
    }
}

這麼看來,IDataFilter 叫做 IDataFilterManager 更加合適一點,最開始我還沒搞明白兩個介面和實現的區別,真正搞事情的是 DataFilter<Filter>

public class DataFilter<TFilter> : IDataFilter<TFilter>
    where TFilter : class
{
    public bool IsEnabled
    {
        get
        {
            EnsureInitialized();
            return _filter.Value.IsEnabled;
        }
    }

    // 註入數據過濾器配置類。
    private readonly AbpDataFilterOptions _options;

    // 用於存儲過濾器的啟用狀態。
    private readonly AsyncLocal<DataFilterState> _filter;

    public DataFilter(IOptions<AbpDataFilterOptions> options)
    {
        _options = options.Value;
        _filter = new AsyncLocal<DataFilterState>();
    }

    // ...

    // 確保初始化成功。
    private void EnsureInitialized()
    {
        if (_filter.Value != null)
        {
            return;
        }

        // 如果過濾器的預設狀態為 NULL,優先從配置類中取得指定過濾器的預設啟用狀態,如果不存在則預設為啟用。
        _filter.Value = _options.DefaultStates.GetOrDefault(typeof(TFilter))?.Clone() ?? new DataFilterState(true);
    }
}

數據過濾器在設計的時候,也是按照工作單元的形式進行設計的。不論是啟用還是停用都是範圍性的,會返回一個用 DisposeAction 包裝的可釋放對象,這樣在離開 using 語句塊的時候,就會還原為來的狀態。比如調用 Enable() 方法,在離開 using 語句塊之後,會調用 Disable() 禁用掉數據過濾器。

public IDisposable Enable()
{
    if (IsEnabled)
    {
        return NullDisposable.Instance;
    }

    _filter.Value.IsEnabled = true;

    return new DisposeAction(() => Disable());
}

public IDisposable Disable()
{
    if (!IsEnabled)
    {
        return NullDisposable.Instance;
    }

    _filter.Value.IsEnabled = false;

    return new DisposeAction(() => Enable());
}

2.4.1 MongoDb 與 Memory 的集成

可以看到有兩處使用,分別是 Volo.Abp.Domain 項目與 Volo.Abp.EntityFrameworkCore 項目。

首先看第一個項目的用法:

public abstract class RepositoryBase<TEntity> : BasicRepositoryBase<TEntity>, IRepository<TEntity>
    where TEntity : class, IEntity
{
    public IDataFilter DataFilter { get; set; }

    // ...

    // 分別在查詢的時候判斷實體是否實現了兩個介面。
    protected virtual TQueryable ApplyDataFilters<TQueryable>(TQueryable query)
        where TQueryable : IQueryable<TEntity>
    {
        // 如果實現了軟刪除介面,則從 DataFilter 中獲取過濾器的開啟狀態。
        // 如果已經開啟,則過濾掉被刪除的數據。
        if (typeof(ISoftDelete).IsAssignableFrom(typeof(TEntity)))
        {
            query = (TQueryable)query.WhereIf(DataFilter.IsEnabled<ISoftDelete>(), e => ((ISoftDelete)e).IsDeleted == false);
        }

        // 如果實現了多租戶介面,則從 DataFilter 中獲取過濾器的開啟狀態。
        // 如果已經開啟,則按照租戶 Id 過濾數據。
        if (typeof(IMultiTenant).IsAssignableFrom(typeof(TEntity)))
        {
            var tenantId = CurrentTenant.Id;
            query = (TQueryable)query.WhereIf(DataFilter.IsEnabled<IMultiTenant>(), e => ((IMultiTenant)e).TenantId == tenantId);
        }

        return query;
    }

    // ...
}

邏輯比較簡單,都是判斷實體是否實現某個介面,並且結合啟用狀態來進行過濾,在原有 IQuerable 拼接 WhereIf() 即可。但是 EF Core 使用這種方式不行,所以上述方法只會在 Memory 和 MongoDb 有使用。

2.4.2 EF Core 的集成

EF Core 集成數據過濾器則是放在資料庫上下文基類 AbpDbContext<TDbContext> 中,在資料庫上下文的 OnModelCreating() 方法內通過 ConfigureBasePropertiesMethodInfo 進行反射調用。

public abstract class AbpDbContext<TDbContext> : DbContext, IEfCoreDbContext, ITransientDependency
    where TDbContext : DbContext
{
    // ...
    protected virtual bool IsMultiTenantFilterEnabled => DataFilter?.IsEnabled<IMultiTenant>() ?? false;

    protected virtual bool IsSoftDeleteFilterEnabled => DataFilter?.IsEnabled<ISoftDelete>() ?? false;

    // ...

    public IDataFilter DataFilter { get; set; }

    // ...

    private static readonly MethodInfo ConfigureBasePropertiesMethodInfo = typeof(AbpDbContext<TDbContext>)
        .GetMethod(
            nameof(ConfigureBaseProperties),
            BindingFlags.Instance | BindingFlags.NonPublic
        );

    // ...

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        foreach (var entityType in modelBuilder.Model.GetEntityTypes())
        {
            ConfigureBasePropertiesMethodInfo
                .MakeGenericMethod(entityType.ClrType)
                .Invoke(this, new object[] { modelBuilder, entityType });

            // ...
        }
    }

    // ...

    protected virtual void ConfigureBaseProperties<TEntity>(ModelBuilder modelBuilder, IMutableEntityType mutableEntityType)
        where TEntity : class
    {
        if (mutableEntityType.IsOwned())
        {
            return;
        }

        ConfigureConcurrencyStampProperty<TEntity>(modelBuilder, mutableEntityType);
        ConfigureExtraProperties<TEntity>(modelBuilder, mutableEntityType);
        ConfigureAuditProperties<TEntity>(modelBuilder, mutableEntityType);
        ConfigureTenantIdProperty<TEntity>(modelBuilder, mutableEntityType);
        // 在這裡,配置全局過濾器。
        ConfigureGlobalFilters<TEntity>(modelBuilder, mutableEntityType);
    }

    // ...

    protected virtual void ConfigureGlobalFilters<TEntity>(ModelBuilder modelBuilder, IMutableEntityType mutableEntityType)
        where TEntity : class
    {
        // 符合條件則為其創建過濾表達式。
        if (mutableEntityType.BaseType == null && ShouldFilterEntity<TEntity>(mutableEntityType))
        {
            // 創建過濾表達式。
            var filterExpression = CreateFilterExpression<TEntity>();
            if (filterExpression != null)
            {
                // 為指定的實體配置查詢過濾器。
                modelBuilder.Entity<TEntity>().HasQueryFilter(filterExpression);
            }
        }
    }

    // ...

    // 判斷實體是否擁有過濾器。
    protected virtual bool ShouldFilterEntity<TEntity>(IMutableEntityType entityType) where TEntity : class
    {
        if (typeof(IMultiTenant).IsAssignableFrom(typeof(TEntity)))
        {
            return true;
        }

        if (typeof(ISoftDelete).IsAssignableFrom(typeof(TEntity)))
        {
            return true;
        }

        return false;
    }

    // 構建表達式。
    protected virtual Expression<Func<TEntity, bool>> CreateFilterExpression<TEntity>()
        where TEntity : class
    {
        Expression<Func<TEntity, bool>> expression = null;

        if (typeof(ISoftDelete).IsAssignableFrom(typeof(TEntity)))
        {
            expression = e => !IsSoftDeleteFilterEnabled || !EF.Property<bool>(e, "IsDeleted");
        }

        if (typeof(IMultiTenant).IsAssignableFrom(typeof(TEntity)))
        {
            Expression<Func<TEntity, bool>> multiTenantFilter = e => !IsMultiTenantFilterEnabled || EF.Property<Guid>(e, "TenantId") == CurrentTenantId;
            expression = expression == null ? multiTenantFilter : CombineExpressions(expression, multiTenantFilter);
        }

        return expression;
    }
        
    // ...
}

2.5 領域事件集成

在講解事件匯流排與 DDD 這塊的時候,我有提到過 ABP vNext 有實現領域事件功能,用戶可以在聚合根內部使用 AddLocalEvent(object eventData)AddDistributedEvent(object eventData) 添加了領域事件。

public abstract class AggregateRoot : Entity, 
    IAggregateRoot,
    IGeneratesDomainEvents, 
    IHasExtraProperties,
    IHasConcurrencyStamp
{
    // ...

    private readonly ICollection<object> _localEvents = new Collection<object>();
    private readonly ICollection<object> _distributedEvents = new Collection<object>();

    // ...

    // 添加本地事件。
    protected virtual void AddLocalEvent(object eventData)
    {
        _localEvents.Add(eventData);
    }

    // 添加分散式事件。
    protected virtual void AddDistributedEvent(object eventData)
    {
        _distributedEvents.Add(eventData);
    }

    // 獲得所有本地事件。
    public virtual IEnumerable<object> GetLocalEvents()
    {
        return _localEvents;
    }

    // 獲得所有分散式事件。
    public virtual IEnumerable<object> GetDistributedEvents()
    {
        return _distributedEvents;
    }

    // 清空聚合需要觸發的所有本地事件。
    public virtual void ClearLocalEvents()
    {
        _localEvents.Clear();
    }

    // 清空聚合需要觸發的所有分散式事件。
    public virtual void ClearDistributedEvents()
    {
        _distributedEvents.Clear();
    }
}

可以看到,我們在聚合內部執行任何業務行為的時候,可以通過上述的方法發送領域事件。那這些事件是在什麼時候被髮布的呢?

發現這幾個 Get 方法有被 AbpDbContext 所調用,其實在它的內部,會在每次 SaveChangesAsync() 的時候,遍歷所有實體,並獲取它們的本地事件與分散式事件集合,最後由 EntityChangeEventHelper 進行觸發。

public abstract class AbpDbContext<TDbContext> : DbContext, IEfCoreDbContext, ITransientDependency
    where TDbContext : DbContext
{
    // ...
    public override async Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default)
    {
        try
        {
            var auditLog = AuditingManager?.Current?.Log;

            List<EntityChangeInfo> entityChangeList = null;
            if (auditLog != null)
            {
                entityChangeList = EntityHistoryHelper.CreateChangeList(ChangeTracker.Entries().ToList());
            }

            var changeReport = ApplyAbpConcepts();

            var result = await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken).ConfigureAwait(false);

            // 觸發領域事件。
            await EntityChangeEventHelper.TriggerEventsAsync(changeReport).ConfigureAwait(false);

            if (auditLog != null)
            {
                EntityHistoryHelper.UpdateChangeList(entityChangeList);
                auditLog.EntityChanges.AddRange(entityChangeList);
                Logger.LogDebug($"Added {entityChangeList.Count} entity changes to the current audit log");
            }

            return result;
        }
        catch (DbUpdateConcurrencyException ex)
        {
            throw new AbpDbConcurrencyException(ex.Message, ex);
        }
        finally
        {
            ChangeTracker.AutoDetectChangesEnabled = true;
        }
    }

    // ...

    protected virtual EntityChangeReport ApplyAbpConcepts()
    {
        var changeReport = new EntityChangeReport();

        // 遍歷所有的實體變更事件。
        foreach (var entry in ChangeTracker.Entries().ToList())
        {
            ApplyAbpConcepts(entry, changeReport);
        }

        return changeReport;
    }

    protected virtual void ApplyAbpConcepts(EntityEntry entry, EntityChangeReport changeReport)
    {
        // 根據不同的實體操作狀態,執行不同的操作。
        switch (entry.State)
        {
            case EntityState.Added:
                ApplyAbpConceptsForAddedEntity(entry, changeReport);
                break;
            case EntityState.Modified:
                ApplyAbpConceptsForModifiedEntity(entry, changeReport);
                break;
            case EntityState.Deleted:
                ApplyAbpConceptsForDeletedEntity(entry, changeReport);
                break;
        }

        // 添加領域事件。
        AddDomainEvents(changeReport, entry.Entity);
    }

    // ...

    protected virtual void AddDomainEvents(EntityChangeReport changeReport, object entityAsObj)
    {
        var generatesDomainEventsEntity = entityAsObj as IGeneratesDomainEvents;
        if (generatesDomainEventsEntity == null)
        {
            return;
        }

        // 獲取到所有的本地事件和分散式事件,將其加入到 EntityChangeReport 對象當中。
        var localEvents = generatesDomainEventsEntity.GetLocalEvents()?.ToArray();
        if (localEvents != null && localEvents.Any())
        {
            changeReport.DomainEvents.AddRange(localEvents.Select(eventData => new DomainEventEntry(entityAsObj, eventData)));
            generatesDomainEventsEntity.ClearLocalEvents();
        }

        var distributedEvents = generatesDomainEventsEntity.GetDistributedEvents()?.ToArray();
        if (distributedEvents != null && distributedEvents.Any())
        {
            changeReport.DistributedEvents.AddRange(distributedEvents.Select(eventData => new DomainEventEntry(entityAsObj, eventData)));
            generatesDomainEventsEntity.ClearDistributedEvents();
        }
    }
}

轉到 `` 的內部,發現有如下代碼:

// ...
public async Task TriggerEventsAsync(EntityChangeReport changeReport)
{
    // 觸發領域事件。
    await TriggerEventsInternalAsync(changeReport).ConfigureAwait(false);

    if (changeReport.IsEmpty() || UnitOfWorkManager.Current == null)
    {
        return;
    }

    await UnitOfWorkManager.Current.SaveChangesAsync().ConfigureAwait(false);
}

protected virtual async Task TriggerEventsInternalAsync(EntityChangeReport changeReport)
{
    // 觸發預設的實體變更事件,例如某個實體被創建、修改、刪除。
    await TriggerEntityChangeEvents(changeReport.ChangedEntities).ConfigureAwait(false);

    // 觸發用戶自己發送的領域事件。
    await TriggerLocalEvents(changeReport.DomainEvents).ConfigureAwait(false);
    await TriggerDistributedEvents(changeReport.DistributedEvents).ConfigureAwait(false);
}

// ...

protected virtual async Task TriggerLocalEvents(List<DomainEventEntry> localEvents)
{
    foreach (var localEvent in localEvents)
    {
        await LocalEventBus.PublishAsync(localEvent.EventData.GetType(), localEvent.EventData).ConfigureAwait(false);
    }
}

protected virtual async Task TriggerDistributedEvents(List<DomainEventEntry> distributedEvents)
{
    foreach (var distributedEvent in distributedEvents)
    {
        await DistributedEventBus.PublishAsync(distributedEvent.EventData.GetType(), distributedEvent.EventData).ConfigureAwait(false);
    }
}

三、系列文章目錄

點擊我 跳轉到文章總目錄。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 雖然領域驅動設計的思想很誘人,但我們依然會面臨各種隱藏的困難,就比如今天我們要講的主題“持久化”:即使前期我們設計了足夠完整的領域對象,但是依然需要持久化它們到資料庫中,而普通的關係型資料庫可能很難維持領域對象的原有結構,所以我們必須要使用一些特有的手段來處理它。將值對象持久化成欄位好呢?還是將值對... ...
  • 我的項目中某一部分信息比較敏感,但是.Net程式反編譯之後連註釋都看得到。需要把exe保護起來,如:代碼混淆之後再加殼。 Bing到一款.Net混淆工具 ILProtector 作為資深工具黨,先"拿來主義"體驗體驗。本篇主要介紹工具使用 1.加密前的exe,反編譯之後什麼都有 2.用ILProte ...
  • 模擬實現時鐘效果,學習WPF動畫好例子,本文承接上文 "C WPF 時鐘動畫(1/2)" 。 微信公眾號: "Dotnet9" ,網站: "Dotnet9" ,問題或建議: "請網站留言" , 如果對您有所幫助: "歡迎贊賞" 。 C WPF 時鐘動畫(2/2) 內容目錄 1. 實現效果 2. 業務 ...
  • 第一個隨筆,使用了OPEN Live Write,作為客戶端.最近使用c#開發一個小軟體,主要功能是OPC客戶端.以後會開發各類別的協議,作為,協議的轉發棧.因為我本人是搞自動化的,所以搞自動化小伙伴像我這樣喜歡編程的可能有,但是一般是邏輯思維強,但是底子相對還是弱的.1,C# 開發OPC的準備工作... ...
  • 微信公眾號: "Dotnet9" ,網站: "Dotnet9" ,問題或建議: "請網站留言" , 如果對您有所幫助: "歡迎贊賞" 。 C WPF 時鐘動畫(1/2) 內容目錄 1. 實現效果 2. 業務場景 3. 編碼實現 4. 本文參考 5. 源碼下載 1.實現效果 目前只實現了秒針動畫,下篇 ...
  • 簡單介紹 HttpReports 是 .Net Core 下的一個Web項目, 適用於WebAPI,Ocelot網關應用,MVC項目,非常適合針對微服務應用使用,通過中間件的形式集成到您的項目中,可以讓開發人員快速的搭建出一個 數據統計,分析,圖表,監控 一體化的 Web站點。 主要模塊 主要包含H ...
  • Windows 安裝.net2.0/3.0 將下列代碼拷到本地bat文件中(bat文件和sxs文件夾同級),下載適用的.net安裝包版本後放置到sxs文件夾,用管理員許可權執行bat文件即可。 .net2.0/3.0安裝包下載(下載後解壓到sxs文件夾中) widows 1909前版本 適用.net2 ...
  • 我在較早之前的隨筆《基於MVC4+EasyUI的Web開發框架形成之旅--附件上傳組件uploadify的使用》Web框架介紹中介紹了基於Uploadify的文件上傳操作,免費版本用的是Jquery+Flash實現文件的上傳處理,HTML5收費版本的沒有試過。隨著Flash逐漸退出整個環境,很多瀏覽... ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...