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