主頁 >  其他 > [Abp vNext 原始碼分析] - 14. EntityFramework Core 的集成

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

2020-09-16 04:39:04 其他

一、簡要介紹

在以前的文章里面,我們介紹了 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 = https://www.cnblogs.com/dongshenjun/p/_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 =https://www.cnblogs.com/dongshenjun/p/> 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);
    }
}

三、系列文章目錄

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

轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/53306.html

標籤:其他

上一篇:請問esp8266和arduino uno r3要怎么接線 初學者

下一篇:Win10安裝.net2.0/3.0

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 網閘典型架構簡述

    網閘架構一般分為兩種:三主機的三系統架構網閘和雙主機的2+1架構網閘。 三主機架構分別為內端機、外端機和仲裁機。三機無論從軟體和硬體上均各自獨立。首先從硬體上來看,三機都用各自獨立的主板、記憶體及存盤設備。從軟體上來看,三機有各自獨立的作業系統。這樣能達到完全的三機獨立。對于“2+1”系統,“2”分為 ......

    uj5u.com 2020-09-10 02:00:44 more
  • 如何從xshell上傳檔案到centos linux虛擬機里

    如何從xshell上傳檔案到centos linux虛擬機里及:虛擬機CentOs下執行 yum -y install lrzsz命令,出現錯誤:鏡像無法找到軟體包 前言 一、安裝lrzsz步驟 二、上傳檔案 三、遇到的問題及解決方案 總結 前言 提示:其實很簡單,往虛擬機上安裝一個上傳檔案的工具 ......

    uj5u.com 2020-09-10 02:00:47 more
  • 一、SQLMAP入門

    一、SQLMAP入門 1、判斷是否存在注入 sqlmap.py -u 網址/id=1 id=1不可缺少。當注入點后面的引數大于兩個時。需要加雙引號, sqlmap.py -u "網址/id=1&uid=1" 2、判斷文本中的請求是否存在注入 從文本中加載http請求,SQLMAP可以從一個文本檔案中 ......

    uj5u.com 2020-09-10 02:00:50 more
  • Metasploit 簡單使用教程

    metasploit 簡單使用教程 浩先生, 2020-08-28 16:18:25 分類專欄: kail 網路安全 linux 文章標簽: linux資訊安全 編輯 著作權 metasploit 使用教程 前言 一、Metasploit是什么? 二、準備作業 三、具體步驟 前言 Msfconsole ......

    uj5u.com 2020-09-10 02:00:53 more
  • 游戲逆向之驅動層與用戶層通訊

    驅動層代碼: #pragma once #include <ntifs.h> #define add_code CTL_CODE(FILE_DEVICE_UNKNOWN,0x800,METHOD_BUFFERED,FILE_ANY_ACCESS) /* 更多游戲逆向視頻www.yxfzedu.com ......

    uj5u.com 2020-09-10 02:00:56 more
  • 北斗電力時鐘(北斗授時服務器)讓網路資料更精準

    北斗電力時鐘(北斗授時服務器)讓網路資料更精準 北斗電力時鐘(北斗授時服務器)讓網路資料更精準 京準電子科技官微——ahjzsz 近幾年,資訊技術的得了快速發展,互聯網在逐漸普及,其在人們生活和生產中都得到了廣泛應用,并且取得了不錯的應用效果。計算機網路資訊在電力系統中的應用,一方面使電力系統的運行 ......

    uj5u.com 2020-09-10 02:01:03 more
  • 【CTF】CTFHub 技能樹 彩蛋 writeup

    ?碎碎念 CTFHub:https://www.ctfhub.com/ 筆者入門CTF時時剛開始刷的是bugku的舊平臺,后來才有了CTFHub。 感覺不論是網頁UI設計,還是題目質量,賽事跟蹤,工具軟體都做得很不錯。 而且因為獨到的金幣制度的確讓人有一種想去刷題賺金幣的感覺。 個人還是非常喜歡這個 ......

    uj5u.com 2020-09-10 02:04:05 more
  • 02windows基礎操作

    我學到了一下幾點 Windows系統目錄結構與滲透的作用 常見Windows的服務詳解 Windows埠詳解 常用的Windows注冊表詳解 hacker DOS命令詳解(net user / type /md /rd/ dir /cd /net use copy、批處理 等) 利用dos命令制作 ......

    uj5u.com 2020-09-10 02:04:18 more
  • 03.Linux基礎操作

    我學到了以下幾點 01Linux系統介紹02系統安裝,密碼啊破解03Linux常用命令04LAMP 01LINUX windows: win03 8 12 16 19 配置不繁瑣 Linux:redhat,centos(紅帽社區版),Ubuntu server,suse unix:金融機構,證券,銀 ......

    uj5u.com 2020-09-10 02:04:30 more
  • 05HTML

    01HTML介紹 02頭部標簽講解03基礎標簽講解04表單標簽講解 HTML前段語言 js1.了解代碼2.根據代碼 懂得挖掘漏洞 (POST注入/XSS漏洞上傳)3.黑帽seo 白帽seo 客戶網站被黑帽植入劫持代碼如何處理4.熟悉html表單 <html><head><title>TDK標題,描述 ......

    uj5u.com 2020-09-10 02:04:36 more
最新发布
  • 2023年最新微信小程式抓包教程

    01 開門見山 隔一個月發一篇文章,不過分。 首先回顧一下《微信系結手機號資料庫被脫庫事件》,我也是第一時間得知了這個訊息,然后跟蹤了整件事情的經過。下面是這起事件的相關截圖以及近日流出的一萬條資料樣本: 個人認為這件事也沒什么,還不如關注一下之前45億快遞資料查詢渠道疑似在近日復活的訊息。 訊息是 ......

    uj5u.com 2023-04-20 08:48:24 more
  • web3 產品介紹:metamask 錢包 使用最多的瀏覽器插件錢包

    Metamask錢包是一種基于區塊鏈技術的數字貨幣錢包,它允許用戶在安全、便捷的環境下管理自己的加密資產。Metamask錢包是以太坊生態系統中最流行的錢包之一,它具有易于使用、安全性高和功能強大等優點。 本文將詳細介紹Metamask錢包的功能和使用方法。 一、 Metamask錢包的功能 數字資 ......

    uj5u.com 2023-04-20 08:47:46 more
  • vulnhub_Earth

    前言 靶機地址->>>vulnhub_Earth 攻擊機ip:192.168.20.121 靶機ip:192.168.20.122 參考文章 https://www.cnblogs.com/Jing-X/archive/2022/04/03/16097695.html https://www.cnb ......

    uj5u.com 2023-04-20 07:46:20 more
  • 從4k到42k,軟體測驗工程師的漲薪史,給我看哭了

    清明節一過,盲猜大家已經無心上班,在數著日子準備過五一,但一想到銀行卡里的余額……瞬間心情就不美麗了。最近,2023年高校畢業生就業調查顯示,本科畢業月平均起薪為5825元。調查一出,便有很多同學表示自己又被平均了。看著這一資料,不免讓人想到前不久中國青年報的一項調查:近六成大學生認為畢業10年內會 ......

    uj5u.com 2023-04-20 07:44:00 more
  • 最新版本 Stable Diffusion 開源 AI 繪畫工具之中文自動提詞篇

    🎈 標簽生成器 由于輸入正向提示詞 prompt 和反向提示詞 negative prompt 都是使用英文,所以對學習母語的我們非常不友好 使用網址:https://tinygeeker.github.io/p/ai-prompt-generator 這個網址是為了讓大家在使用 AI 繪畫的時候 ......

    uj5u.com 2023-04-20 07:43:36 more
  • 漫談前端自動化測驗演進之路及測驗工具分析

    隨著前端技術的不斷發展和應用程式的日益復雜,前端自動化測驗也在不斷演進。隨著 Web 應用程式變得越來越復雜,自動化測驗的需求也越來越高。如今,自動化測驗已經成為 Web 應用程式開發程序中不可或缺的一部分,它們可以幫助開發人員更快地發現和修復錯誤,提高應用程式的性能和可靠性。 ......

    uj5u.com 2023-04-20 07:43:16 more
  • CANN開發實踐:4個DVPP記憶體問題的典型案例解讀

    摘要:由于DVPP媒體資料處理功能對存放輸入、輸出資料的記憶體有更高的要求(例如,記憶體首地址128位元組對齊),因此需呼叫專用的記憶體申請介面,那么本期就分享幾個關于DVPP記憶體問題的典型案例,并給出原因分析及解決方法。 本文分享自華為云社區《FAQ_DVPP記憶體問題案例》,作者:昇騰CANN。 DVPP ......

    uj5u.com 2023-04-20 07:43:03 more
  • msf學習

    msf學習 以kali自帶的msf為例 一、msf核心模塊與功能 msf模塊都放在/usr/share/metasploit-framework/modules目錄下 1、auxiliary 輔助模塊,輔助滲透(埠掃描、登錄密碼爆破、漏洞驗證等) 2、encoders 編碼器模塊,主要包含各種編碼 ......

    uj5u.com 2023-04-20 07:42:59 more
  • Halcon軟體安裝與界面簡介

    1. 下載Halcon17版本到到本地 2. 雙擊安裝包后 3. 步驟如下 1.2 Halcon軟體安裝 界面分為四大塊 1. Halcon的五個助手 1) 影像采集助手:與相機連接,設定相機引數,采集影像 2) 標定助手:九點標定或是其它的標定,生成標定檔案及內參外參,可以將像素單位轉換為長度單位 ......

    uj5u.com 2023-04-20 07:42:17 more
  • 在MacOS下使用Unity3D開發游戲

    第一次發博客,先發一下我的游戲開發環境吧。 去年2月份買了一臺MacBookPro2021 M1pro(以下簡稱mbp),這一年來一直在用mbp開發游戲。我大致分享一下我的開發工具以及使用體驗。 1、Unity 官網鏈接: https://unity.cn/releases 我一般使用的Apple ......

    uj5u.com 2023-04-20 07:40:19 more