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