记一次对老服务改造
关于现有老服务
使用的技术.net4.6.1+Nancy+Dapper,数据库是需要支持老版本MsSql + 新版本MySql、MsSql、DM,项目解决方案单层结构。看到这样的项目内心是...,好了再看内部实现,完全不想说话。
if (connectionString.Contains("xx"))
{
//对应数据实现业务
}
else if (connectionString.Contains("xx"))
{
//对应数据实现业务
}
else
{
//对应数据实现业务
}
每个接口都有一个如上的结构代码根据配置字符串判断数据库类型,每种数据库类型写一个实现,两个版本库结构三种不同数据库也就是一个业务要写四遍实现。
接口返回数据直接使用数据库表对象不管前端是否需要。吐血...血...
能用4.6.1版本创建项目算不上太老,这时间节点都支持.net standard2。完全不明白为什么要使用这样的技术栈组合去实现业务。
改造
项目只给了实现业务时间,但是在老项目上继续做下去已经难以为继。使用aspnetboilerplate进行改造(为啥不直接用vnext,因有些客户端使用prism自带了ioc容器,而老本abp是有IocManager方便融合把服务端中AppService也可直接在客户端中重用),考虑到几个改造点
1、使用aspnetboilerplate MultipleDbContext对不同数据库类型支持减少重复写不同数据库实现。
2、接口兼容现有返回数据结构减少客户端改动。
3、客户端构造http请求不统一,接收请求兼容form-data、queryString。
4、接口请求路径和现有服务保持一致。
5、审计日志记录。
6、请求与返回对象单独定义生成Swagger。
1、MultipleDbContext
获取数据库连接字符串
public class MyConnectionStringResolver : DefaultConnectionStringResolver
{
private readonly IConfigurationRoot _appConfiguration;
public MyConnectionStringResolver(IAbpStartupConfiguration configuration, IHostingEnvironment hostingEnvironment)
: base(configuration)
{
_appConfiguration =
AppConfigurations.Get(hostingEnvironment.ContentRootPath, hostingEnvironment.EnvironmentName);
}
public override string GetNameOrConnectionString(ConnectionStringResolveArgs args)
{
if (args["DbContextConcreteType"] as Type == typeof(OtherDbContext))
{
return _appConfiguration.GetConnectionString(YouApiConsts.OtherConnectionStringName);
}
return base.GetNameOrConnectionString(args);
}
}
添加DbContext
public override void PreInitialize()
{
//替换ConnectionStringResolver
Configuration.ReplaceService<IConnectionStringResolver, MyConnectionStringResolver>();
if (!SkipDbContextRegistration)
{
Configuration.Modules.AbpEfCore().AddDbContext<YouApiDbContext>(options =>
{
if (options.ExistingConnection != null)
{
YouApiDbContextConfigurer.Configure(options.DbContextOptions, options.ExistingConnection);
}
else
{
YouApiDbContextConfigurer.Configure(options.DbContextOptions, options.ConnectionString);
}
});
//新增DbContext
Configuration.Modules.AbpEfCore().AddDbContext<OtherDbContext>(options =>
{
if (options.ExistingConnection != null)
{
OtherDbContextConfigurer.Configure(options.DbContextOptions, options.ExistingConnection);
}
else
{
OtherDbContextConfigurer.Configure(options.DbContextOptions, options.ConnectionString);
}
});
}
}
2、abp api自定义返回数据结构
先找到AjaxResponse
与AjaxResponseBase
被使用的相关对象AbpObjectActionResultWrapper、AbpJsonActionResultWrapper、AbpEmptyActionResultWrapper
都在AbpActionResultWrapperFactory : IAbpActionResultWrapperFactory
中被使用,将Response与Wrapper稍微改造即可。
正常情况下返回
public override void PreInitialize()
{
//替换默认的AbpActionResultWrapperFactory
Configuration.ReplaceService<IAbpActionResultWrapperFactory, MyActionResultWrapperFactory>(DependencyLifeStyle.Transient);
}
异常情况下返回处理
实现自定义MyExceptionFilter
,按照原有实现稍微改造即可。
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpClient();
//MVC
services.AddControllersWithViews(
options => {
options.Filters.Add(new AbpAutoValidateAntiforgeryTokenAttribute());
//添加自定义异常拦截器MyExceptionFilter,指定order: 1执行优先生效
options.Filters.AddService(typeof(MyExceptionFilter), order: 1);
}
).AddNewtonsoftJson(options =>
{
options.SerializerSettings.ContractResolver = new AbpMvcContractResolver(IocManager.Instance)
{
NamingStrategy = new CamelCaseNamingStrategy()
};
});
//...
}
3、接收请求兼容form-data、queryString
使用
//MVC
services.AddControllersWithViews(
options => {
options.Filters.Add(new AbpAutoValidateAntiforgeryTokenAttribute());
//添加自定义异常拦截器MyExceptionFilter,指定order: 1执行优先生效
options.Filters.AddService(typeof(MyExceptionFilter), order: 1);
//请求参数绑定兼容from-data
options.ModelBinderProviders.InsertBodyOrDefaultBinding();
}
接收不同请求类型参数json、form-data、queryString都可正常兼容
public class MyModelBinder : IModelBinder
{
private readonly IModelBinder _bodyBinder;
private readonly IModelBinder _complexBinder;
public MyModelBinder (IModelBinder bodyBinder, IModelBinder complexBinder)
{
_bodyBinder = bodyBinder;
_complexBinder = complexBinder;
}
public async Task BindModelAsync(ModelBindingContext bindingContext)
{
await DefaultBindModel(bindingContext);
}
private async Task DefaultBindModel(ModelBindingContext bindingContext)
{
//两种对不同形式(json、form-data、queryString)请求参数绑定
await _bodyBinder.BindModelAsync(bindingContext);
if (bindingContext.Result.IsModelSet)
{
return;
}
bindingContext.ModelState.Clear();
await _complexBinder.BindModelAsync(bindingContext);
}
}
public class MyModelBinderProvider : IModelBinderProvider
{
private readonly BodyModelBinderProvider _bodyModelBinderProvider;
private readonly ComplexObjectModelBinderProvider _complexDataModelBinderProvider;
public MyModelBinderProvider(BodyModelBinderProvider bodyModelBinderProvider, ComplexObjectModelBinderProvider complexDataModelBinderProvider)
{
_bodyModelBinderProvider = bodyModelBinderProvider;
_complexDataModelBinderProvider = complexDataModelBinderProvider;
}
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context.BindingInfo.BindingSource != null && context.BindingInfo.BindingSource.CanAcceptDataFrom(BindingSource.Body))
{
var bodyBinder = _bodyModelBinderProvider.GetBinder(context);
var complexBinder = _complexDataModelBinderProvider.GetBinder(context);
return new MyModelBinder(bodyBinder, complexBinder);
}
return null;
}
}
public static class MyModelBinderProviderSetup
{
public static void InsertBodyOrDefaultBinding(this IList<IModelBinderProvider> providers)
{
var bodyProvider = providers.OfType<BodyModelBinderProvider>().Single();
var complexDataProvider = providers.OfType<ComplexObjectModelBinderProvider>().Single();
providers.Insert(0, new MyModelBinderProvider(bodyProvider, complexDataProvider));
}
}
4、接口请求路径和现有服务保持一致
在Controller添加转换,时间不够原有老接口直接做转发,新接口和需要改造的接口直接在新项目中实现。
[ApiController]
[Route("api/[controller]/[action]")]
由于有不同数据库版本使用适配器模式获取不同实现调用
private readonly IYouService _youService;
public InventoryController(IAppConfigurationAccessor configurationAccessor, IHttpClientFactory httpClientFactory)
{
_appConfiguration = configurationAccessor.Configuration;
_httpClientFactory = httpClientFactory;
if (_dic.ContainsKey(_key))
_youService = IocManager.Instance.IocContainer.Resolve<IInventory>(_dic[_key]);
}
转发到老服务请求
private async Task<TO> GetExecute<TO>(string url)
{
TO result = Activator.CreateInstance<TO>();
var httpClient = _httpClientFactory.CreateClient();
var response = await httpClient.GetAsync($"{_baseUrl}{url}");
// 处理响应
if (response.IsSuccessStatusCode)
{
var responseData = await response.Content.ReadAsStringAsync();
var resData = JsonConvert.DeserializeObject<ResParameter>(responseData);
if (resData.code == ResponseCode.success)
{
if (resData.data != null)
result = JsonConvert.DeserializeObject<TO>(resData.data.ToString());
else
result = default(TO);
}
else
{
throw new UserFriendlyException(resData.info);
}
}
return result;
}
private async Task<TO> PostExecute<TO, TI>(string url, TI input)
{
TO result = Activator.CreateInstance<TO>();
var httpClient = _httpClientFactory.CreateClient();
HttpContent httpContent = new StringContent(JsonConvert.SerializeObject(input), System.Text.Encoding.UTF8, "application/json");
var response = await httpClient.PostAsync($"{_baseUrl}{url}", httpContent);
// 处理响应
if (response.IsSuccessStatusCode)
{
var responseData = await response.Content.ReadAsStringAsync();
var resData = JsonConvert.DeserializeObject<ResParameter>(responseData);
if (resData.code == ResponseCode.success)
{
if (resData.data != null)
result = JsonConvert.DeserializeObject<TO>(resData.data.ToString());
else
result = default(TO);
}
else
{
throw new UserFriendlyException(resData.info);
}
}
return result;
}
5、审计日志记录
由于现有数据库是非aspnetboilerplate格式的数据库审计日志替换成MySimpleLogAuditingStore
/// <summary>
/// Implements <see cref="IAuditingStore"/> to simply write audits to logs.
/// </summary>
public class MySimpleLogAuditingStore : IAuditingStore
{
/// <summary>
/// Singleton instance.
/// </summary>
public static SimpleLogAuditingStore Instance { get; } = new SimpleLogAuditingStore();
public ILogger Logger { get; set; }
public MySimpleLogAuditingStore()
{
Logger = NullLogger.Instance;
}
public Task SaveAsync(AuditInfo auditInfo)
{
if (auditInfo.Exception == null)
{
Logger.Info(auditInfo.LogToString());
//Logger.Info(auditInfo.ToString());
}
else
{
Logger.Warn(auditInfo.LogToString());
//Logger.Warn(auditInfo.ToString());
}
return Task.FromResult(0);
}
public void Save(AuditInfo auditInfo)
{
if (auditInfo.Exception == null)
{
Logger.Info(auditInfo.LogToString());
//Logger.Info(auditInfo.ToString());
}
else
{
Logger.Warn(auditInfo.LogToString());
//Logger.Warn(auditInfo.ToString());
}
}
}
public static class SimpleLogAuditingStoreExtensions
{
public static string LogToString(this AuditInfo auditInfo)
{
var loggedUserId = auditInfo.UserId.HasValue
? "user " + auditInfo.UserId.Value
: "an anonymous user";
var exceptionOrSuccessMessage = auditInfo.Exception != null
? "exception: " + auditInfo.Exception.Message
: "succeed";
return @$"AUDIT LOG: {auditInfo.ServiceName}.{auditInfo.MethodName} is executed by {loggedUserId} in {auditInfo.ExecutionDuration} ms
from {auditInfo.ClientIpAddress} IP address
param {auditInfo.Parameters}
with {exceptionOrSuccessMessage}.";
}
}
6、请求与返回对象单独定义生成Swagger
原有的Nancy是没办法生成Swagger文档前端也不知道请求路径参数与返回徒增沟通成本,且原有接收参数很多是直接写死字符串接收。将原有请求入参与返回单独定义对象也能在对象中使用ICustomValidate
做一些简单效验。
由于返回结果也是直接使用老系统定义的数据结构导致很多非必要的xx:null
数据返回,为了避免混淆视听添加全局忽略
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpClient();
//MVC
services.AddControllersWithViews(
//...
).AddNewtonsoftJson(options =>
{
options.SerializerSettings.ContractResolver = new AbpMvcContractResolver(IocManager.Instance)
{
NamingStrategy = new CamelCaseNamingStrategy(),
};
//全局忽略返回null
options.SerializerSettings.NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore;
});
//...
}
最后
吐...血...。止住了。