在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 算成功了。  当然 只成功了工作单元模式。  不用工作单元的情况 以后再调整下。

最后  膜拜大神!

 
原文地址:https://www.cnblogs.com/luckstar007/p/10949811.html