在Abp上使用CAP


Abp 是什么。  大佬們把單體.net程序能涉及到的東西 都涉及到了。 對於單體web開發 一步到位的東西。 當然不能只用不理解, 不然出問題了就懵逼了。 通過看源碼還是能學到很多東西

Abp的git地址:  https://github.com/aspnetboilerplate/aspnetboilerplate

ABP vNext  據說是全新的.net core思想的版本, 目前還是pre階段     git地址: https://github.com/abpframework/abp

 

CAP 

CAP是一個解決分布式事務,帶有分布式事務總線的一個東西。 作者是 Savorboard 。  git地址:  https://github.com/dotnetcore/CAP

我理解是CAP可以用在微服務上,在服務之間保證數據一致性;

目前項目上有多個系統之間的事務。變相的分布式事務問題。   項目是在Abp上寫的單體應用。 所以就想在Abp上使用CAP這個東西;

好了  場景介紹完了。 技術一般  若有錯 請指正。 下面記錄一下這次遇到的一些問題;

 

先看一下Abp的Startup

 

   // Configure Abp and Dependency Injection
            return services.AddAbp<ms_CAPWebHostModule>(
                // Configure Log4Net logging
                options => options.IocManager.IocContainer.AddFacility<LoggingFacility>( f => f.UseAbpLog4Net().WithConfig("log4net.config") ) );

 在ConfigureServices方法的最后調用AddAbp方法

 

看一下AddAbp的源碼

 public static IServiceProvider AddAbp<TStartupModule>(this IServiceCollection services, [CanBeNull] Action<AbpBootstrapperOptions> optionsAction = null)
            where TStartupModule : AbpModule { var abpBootstrapper = AddAbpBootstrapper<TStartupModule>(services, optionsAction); ConfigureAspNetCore(services, abpBootstrapper.IocManager); return WindsorRegistrationHelper.CreateServiceProvider(abpBootstrapper.IocManager.IocContainer, services); }

把Abp的所有模塊的類注入到IServiceCollection 然后加入到abp的依賴注入容器 Castle里面;  然后返回一個IServiceProvider;

 

所以   必須在 AddAbp()方法之前調用CAP的AddCap()方法 

 

看一下CAP的源碼,EF+ MySql的例子

services.AddDbContext<AppDbContext>();

            services.AddCap(x =>
            {
                x.UseEntityFramework<AppDbContext>();
                x.UseRabbitMQ("localhost");
                x.UseDashboard();
                x.FailedRetryCount = 5;
                x.FailedThresholdCallback = (type, name, content) =>
                {
                    Console.WriteLine($@"A message of type {type} failed after executing {x.FailedRetryCount} several times, requiring manual troubleshooting. Message name: {name}, message body: {content}");
                };
            });

 

我用的是EF + Sql server 數據庫 不過相差不大。 只是大佬沒寫sql server的例子而已;

看一下Sql server  的UseEntityFramework 這個方法

 

  public static CapOptions UseEntityFramework<TContext>(this CapOptions options, Action<EFOptions> configure)
            where TContext : DbContext
        {
            if (configure == null)
            {
                throw new ArgumentNullException(nameof(configure));
            }

            options.RegisterExtension(new SqlServerCapOptionsExtension(x =>
            {
                configure(x);
                x.Version = options.Version;
                x.DbContextType = typeof(TContext);
            }));

            return options;
        }

 

RegisterExtension注冊擴展;

在SqlServerCapOptionsExtension的 AddSqlServerOptions()方法里面 問題來了。

  private void AddSqlServerOptions(IServiceCollection services)
        {
            var sqlServerOptions = new SqlServerOptions();

            _configure(sqlServerOptions);

            if (sqlServerOptions.DbContextType != null)
            {
                services.AddSingleton(x =>
                {
                    using (var scope = x.CreateScope())
                    {
                        var provider = scope.ServiceProvider;
                        var dbContext = (DbContext) provider.GetService(sqlServerOptions.DbContextType);
                        sqlServerOptions.ConnectionString = dbContext.Database.GetDbConnection().ConnectionString; return sqlServerOptions;
                    }
                });
            }
            else
            {
                services.AddSingleton(sqlServerOptions);
            }
        }

 

會在當前請求里面 去找 DbContext  然后把 DbContext的ConnectionString賦值給CAP的sqlServerOptions;

 

但是Abp的DbContext 不是簡單的由容器創建的, 在工作單位 里面 是由 ICurrentUnitOfWorkProvider 這個東西來管理的。 所有要拿到當前scope的dbcontext 不能由容器來 ;如下Abp 的 EfCoreUnitOfWork源碼:

 

 

public virtual TDbContext GetOrCreateDbContext<TDbContext>(MultiTenancySides? multiTenancySide = null, string name = null)
            where TDbContext : DbContext
        {
            var concreteDbContextType = _dbContextTypeMatcher.GetConcreteType(typeof(TDbContext));

            var connectionStringResolveArgs = new ConnectionStringResolveArgs(multiTenancySide);
            connectionStringResolveArgs["DbContextType"] = typeof(TDbContext);
            connectionStringResolveArgs["DbContextConcreteType"] = concreteDbContextType;
            var connectionString = ResolveConnectionString(connectionStringResolveArgs);

            var dbContextKey = concreteDbContextType.FullName + "#" + connectionString;
            if (name != null)
            {
                dbContextKey += "#" + name;
            }

            DbContext dbContext;
            if (!ActiveDbContexts.TryGetValue(dbContextKey, out dbContext))
            {
                if (Options.IsTransactional == true)
                {
                    dbContext = _transactionStrategy.CreateDbContext<TDbContext>(connectionString, _dbContextResolver);
                }
                else
                {
                    dbContext = _dbContextResolver.Resolve<TDbContext>(connectionString, null);
                }

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

                //TODO: Object materialize event
                //TODO: Apply current filters to this dbcontext

                ActiveDbContexts[dbContextKey] = dbContext;
            }

            return (TDbContext)dbContext;
        }

 

拿到Abp 一scope的ef 的dbcontext代碼如下:

  using (var scope = serviceProvider.CreateScope())
                {
                    var provider = scope.ServiceProvider;
                    var currentUnitOfWorkProvider = provider.GetService<ICurrentUnitOfWorkProvider>();
                    var unitOfWork = currentUnitOfWorkProvider.Current;
                    var efCoreUnitOfWork = unitOfWork as EfCoreUnitOfWork;
                    foreach (var item in efCoreUnitOfWork.GetAllActiveDbContexts())
                    {
                        if (item.GetType() == sqlServerOptions.DbContextType)
                        {
                            _dbContext = efCoreUnitOfWork.GetAllActiveDbContexts()[0];
                            break;
                        }
                    }

 

經過測試 在Abp的Startup 的ConfigureServices時,  efCoreUnitOfWork.GetAllActiveDbContexts() 數量為0。 表示這個時候沒有數據庫請求。。。

走到這里走死了。。。。

回頭看了下 AddSqlServerOptions的代碼

  private void AddSqlServerOptions(IServiceCollection services)
        {
            var sqlServerOptions = new SqlServerOptions();

            _configure(sqlServerOptions);

            if (sqlServerOptions.DbContextType != null)
            {
                services.AddSingleton(x =>
                {
                    using (var scope = x.CreateScope())
                    {
                        var provider = scope.ServiceProvider;
                        var dbContext = (DbContext) provider.GetService(sqlServerOptions.DbContextType);
                        sqlServerOptions.ConnectionString = dbContext.Database.GetDbConnection().ConnectionString;
                        return sqlServerOptions;
                    }
                });
            }
            else
            {
                services.AddSingleton(sqlServerOptions);
            }
        }

只有去改CAP的 AddSqlServerOptions方法。 它里面的代碼是注入一個單例SqlServerOptions  應該是程序第一次跑的時候 去給數據創建CAP的表。  需要指定ConnectionString;

我在Abp的EF模塊里面 找到。

 public override void PreInitialize()
        {
            if (!SkipDbContextRegistration)
            {
                Configuration.Modules.AbpEfCore().AddDbContext<ms_UserDbContext>(options =>
                {
                    if (options.ExistingConnection != null)
                    {
                        ms_UserDbContextConfigurer.Configure(options.DbContextOptions, options.ExistingConnection);
                    }
                    else
                    {
                        ms_UserDbContextConfigurer.Configure(options.DbContextOptions, options.ConnectionString, this.IocManager);
                    }
                });
            }
        }

因為上面的SqlServerOptions是單例 所以在這邊應該是能找到它實例的, 在Configure方法里面給ConnectionString賦值:

   public static void Configure(DbContextOptionsBuilder<ms_UserDbContext> builder, string connectionString, IIocManager iocManager = null)
        {
            builder.UseSqlServer(connectionString);

            if (iocManager != null)
            {
                var sqlServerOptions = iocManager.Resolve<SqlServerOptions>();
                if (string.IsNullOrWhiteSpace(sqlServerOptions.ConnectionString))
                    sqlServerOptions.ConnectionString = connectionString;
            }
        }

 

如此:CAP的初始化搞定了。 下面還有一個問題:

在 CAP的AddServices里面  注入了事務:

services.AddTransient<CapTransactionBase, SqlServerCapTransaction>();

 

在SqlServerCapTransaction同樣要通過容器去拿dbcontext。 這里又懵逼咯。

abp這邊大部分方法都是開啟了工作單元 是一個事務。  所以把注入方式改為Scoped

 

   services.AddScoped<CapTransactionBase, SqlServerCapTransaction>();

 

同樣  修改獲取dbcontext的地方:這里就要引用Abp.EntityFrameworkCore

 

   public SqlServerCapTransaction(
            IDispatcher dispatcher,
            SqlServerOptions sqlServerOptions,
            IServiceProvider serviceProvider) : base(dispatcher)
        {
            if (sqlServerOptions.DbContextType != null)
            {
                using (var scope = serviceProvider.CreateScope())
                {
                    var provider = scope.ServiceProvider;
                    var currentUnitOfWorkProvider = provider.GetService<ICurrentUnitOfWorkProvider>();
                    var unitOfWork = currentUnitOfWorkProvider.Current;
                    var efCoreUnitOfWork = unitOfWork as EfCoreUnitOfWork;
                    foreach (var item in efCoreUnitOfWork.GetAllActiveDbContexts())
                    {
                        if (item.GetType() == sqlServerOptions.DbContextType)
                        {
                            _dbContext = efCoreUnitOfWork.GetAllActiveDbContexts()[0];
                            break;
                        }
                    }

                }

            }

            _diagnosticProcessor = serviceProvider.GetRequiredService<DiagnosticProcessorObserver>();
        }

 

 后面有個數據庫事務鎖級別的設置: 在Module里面

  public override void PreInitialize()
        {
            Configuration.UnitOfWork.IsolationLevel = System.Transactions.IsolationLevel.ReadCommitted;


        }

 

 

 

 

這樣 Abp上用CAP 算成功了。  當然 只成功了工作單元模式。  不用工作單元的情況 以后再調整下。

 

最后  膜拜大神!

 

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM