- {
- "modules": [
- {
- "type": "Account.Repository.EF.RepositoryModule, Account.Repository.EF"
- },
- {
- "type": "Account.Service.ServiceModule, Account.Service"
- }
- ]
- }
这是一份模块配置文件。熟悉 Autofac 的都应该对这个概念比较熟悉,这种配置介于纯代码注册所有服务,以及纯配置文件注册所有服务之间,算是一个平衡,也是我最喜欢的方式。至于具体的模块内服务注册,待会儿讲解。
2)ConfigureServices 适配
- public IServiceProvider ConfigureServices(IServiceCollection services)
- {
- services.AddDbContext(options =>
- options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"), db => db.UseRowNumberForPaging()));
- services.AddCors();
- // Add framework services.services.AddMvc(options => options.Filters.Add(typeof(CustomExceptionFilterAttribute)))
- .AddJsonOptions(options => options.SerializerSettings.DateFormatString ="yyyy-MM-dd HH:mm:ss");
- var builder = new ContainerBuilder();
- builder.Populate(services);
- var module = new ConfigurationModule(Configuration);
- builder.RegisterModule(module);
- this.Container = builder.Build();
- return new AutofacServiceProvider(this.Container);
- }
这里有两个要注意的,其一,修改 ConfigureServices 返回类型:void => IServiceProvider ;其二,如红色部分,这个懒得说太细,太费事儿,总之跟. NET 其他框架下的集成大同小异,没杀特别。
3) 具体 Autofac 模块文件实现
项目中,业务服务实现和仓储实现这两个实现工程用到了 Autofac 模块化注册,这里分别看下。
此工程实现 Account.Service.Contract 业务服务契约,我们重点看 ServiceModule 这个模块注册类:
- public class ServiceModule : Module
- {
- protected override void Load(ContainerBuilder builder)
- {
- //builder.RegisterType<ManifestService>().As<IManifestService>();
- //builder.RegisterType<DailyService>().As<IDailyService>();
- //builder.RegisterType<MonthlyService>().As<IMonthlyService>();
- //builder.RegisterType<YearlyService>().As<IYearlyService>();
- builder.RegisterAssemblyTypes(this.ThisAssembly)
- .Where(t => t.Name.EndsWith("Service"))
- .AsImplementedInterfaces()
- .InstancePerLifetimeScope();
- }
- }
上述注释起来的代码,是最开始逐个服务注册的,后来,想偷点儿懒,就采取了官方的那种做法,既然都已经模块化这一步了,那还不更进一步。于是,这个模块类就成了你现在看到的这个样子,通俗点儿讲就是找出当前模块文件所在程序集中的所有类型注册为其实现的服务接口,注册模式为生命周期模式。这里跟旧版本的 MVC 或 API 有点儿不同的地方,旧版本用的是 InstancePerRquest,但 Core 下面已经没有这种模式了,而是 InstancePerLifetimeScope,起同样的效果。这里,我所有的服务类都以 Service 结尾。
Account.Repository.EF 工程与此类似,不再赘述。
如此以来,控制器中,以及业务服务中,我们便可以遵循显示依赖模式来请求依赖组件,如下:
- [Route("[controller]")]
- public class ManifestController : Controller
- {
- private readonly IManifestService _manifestService;
- public ManifestController(IManifestService manifestService)
- {
- _manifestService = manifestService;
- }
- public class ManifestService : IManifestService
- {
- private readonly IManifestRepository _manifestRepository;
- public ManifestService(IManifestRepository manifestRepository)
- {
- _manifestRepository = manifestRepository;
- }
5、跨域设置
鉴于前后端分离,并分属两个不同的站点,前后端通信那就涉及到跨域问题,这里直接采用. net core 内置的跨域解决方案,设置步骤如下:
1)ConfigureServices 添加跨域相关服务
- public IServiceProvider ConfigureServices(IServiceCollection services)
- {
- services.AddDbContext(options =>
- options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"), db => db.UseRowNumberForPaging()));
- services.AddCors();
2)Configure 注册跨域中间件
- public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, AccountContext context, IApplicationLifetime appLifetime)
- {
- loggerFactory.AddConsole(Configuration.GetSection("Logging"));
- loggerFactory.AddDebug();
- app.UseCors(builder => builder.WithOrigins("http://localhost:65062")
- .AllowAnyHeader().AllowAnyMethod());
两点需要注意:其一,跨域中间件注册放在 MVC 路由注册之前,这个不用解释了吧;其二,红色部分设置你要允许的前端域名、标头及请求方法。这里允许 http://localhost:65062(我的前端站点)、任意标头、任意请求方式
6、异常处理
按照个人以前惯例,异常处理采用异常过滤器,这里也不意外, 过滤器定义如下:
- public class CustomExceptionFilterAttribute : ExceptionFilterAttribute
- {
- private readonlyILogger _logger;
- publicCustomExceptionFilterAttribute(ILogger logger)
- {
- _logger = logger;
- }
- public override void OnException(ExceptionContext context)
- {
- Exception exception = context.Exception;
- JsonResult result =null;
- if(exceptionis BusinessException)
- {
- result =new JsonResult(exception.Message)
- {
- StatusCode = exception.HResult
- };
- }
- else
- {
- result =newJsonResult("服务器处理出错")
- {
- StatusCode =500
- };
- _logger.LogError(null, exception,"服务器处理出错",null);
- }
- context.Result = result;
- }
- }
简言之就是,判断操作方法中抛出的是什么异常,如果是由我们业务代码主动引发的业务级别异常,也就是类型为自定义 BusinessException,则直接设置相应 json 结果状态码及 错误信息为我们引发异常时定义的状态码及错误信息;如果是框架或数据库操作失败引发的,被动式的异常,这种错误信息不应该暴露给前端,而且,这种服务器内部处理出错,理应统一设置状态码为 500,还需要记录异常堆栈,如上的 else 分支所做。
之后,将此过滤器全局注册。Core 中全局注册过滤器的德行如下:
- public IServiceProvider ConfigureServices(IServiceCollection services)
- {
- services.AddDbContext(options =>
- options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"), db => db.UseRowNumberForPaging()));
- services.AddCors();
- // Add framework services.services.AddMvc(options => options.Filters.Add(typeof(CustomExceptionFilterAttribute)))
- .AddJsonOptions(options => options.SerializerSettings.DateFormatString ="yyyy-MM-dd HH:mm:ss");
顺便说下那个 AddJsonOptions 的,大家应该经常遇到时间字符串表示中有个 T 吧,是不是很蛋疼,这句话就是解决这个问题的。
7、具体请求解析
请求流经的处理流程如下图:
由上到下的顺序,线上边是组件之间通信或依赖经由的协议或契约
我们以其中消费明细管理为例,将上图中工程变为具体组件, 具体请求处理流程就变成了:
鉴于具体服务实现、数据访问等跟之前基于 asp.net web api 的实现已经有了很大不同,这里还是分析下各 CRUD 方法吧。
1) 路由
基于 WebAPI 或者说 Rest 的路由,我一向倾向于用特性路由,而非 MVC 默认路由,因为更灵活,也更容易符合 Rest 模式。来看具体控制器:
旧版本中,我们只能在控制器层面使用 RoutePrefix 特性,.NET CORE 中已经不再有 RoutePrefix,直接上 Route。而且,注意路由模板中那个 [controller],这是一个控制器占位符,具体运行时会被控制器名称替换,比写死爽多了吧。接下来,看控制器方法层面:
大家看到各 CRUD 操作上的特性标记没有。老 WebAPI 中,是需要通过 Route 来设置,具体请求方法约束需要单独通过类似 HttpGet、HttpPut 等来约束,而. NET CORE 中,可以合二为一,路由设置和请求方法约束一起搞定。当然,你依然可以按照老方式来玩儿,没毛病,无非就是多写一行代码,累赘点儿而已。实际上,路由中不光可以有控制器占位符,还可以有操作占位符,运行时会被操作名称代替,但这里是 Rest 服务,不是 MVC 终结点,所以我没有添加控制器方法占位符 [action]。
另外,注意看添加和编辑,以添加为例:
- [HttpPost("")] public IActionResult Add([FromBody] Manifest manifest) {
- manifest = _manifestService.AddManifest(manifest);
- return CreatedAtRoute(new {
- ID = manifest.ID
- },
- manifest);
- }
看到那个红色 FromBody 特性标记没有?起初,我是没有添加这个特性的,因为根据旧版本的经验,前端设置 Content-type 为 json,后端 Put,POST 实体参数那不就是自动绑定么。.NET CORE 中不行了,必须明确指定,参数来源于哪儿,否则,绑定失败,而且不报错,更操蛋的,这个包需要我们单独引用,包名是 Microsoft.AspNetCore.Mvc.Core,默认 MVC 工程是没有引用的。
2)分页查询
来看日消费明细吧:
- public asyncTask> GetManifests(DateTime start, DateTime end, intpageIndex,int pageSize)
- {
- varsource = _context.Manifests.Where(x => x.Date >= start && x.Date <newDateTime(end.Year, end.Month, end.Day).AddDays(1));
- intcount =await source.CountAsync();
- List manifests = null;
- if(count >0)
- {
- manifests =awaitsource.OrderBy(x => x.Date).Skip((pageIndex -1) * pageSize).Take(pageSize).ToListAsync();
- }
- return newPaginatedList(pageIndex, pageSize, count, manifests ?? newList());
- }
典型的 EF 分页查询,先获取符合条件总记录数,然后排序并取指定页数据,没毛病。
日消费清单也类似,但关于月清单和年清单,这里要多说下。 月清单和年清单都是统计的日消费清单 Daily,具体 Daily 又是由日消费明细 Manifest 支撑的。
来看下月消费清单的查询:
- public asyncTask> GetMonthlys(stringstart,stringend,intpageIndex,int pageSize)
- {
- varsource = _context.Dailys
- .Where(x => x.Date >= DateTime.Parse(start) && x.Date <= DateTime.Parse(end).AddMonths(1).AddSeconds(-1))
- .GroupBy(x => x.Date.ToString("yyyy-MM"), (k, v) =>
- new Monthly
- {
- ID = Guid.NewGuid().ToString(),
- Month = k,
- Cost = v.Sum(x => x.Cost)
- });
- intcount =await source.CountAsync();
- List months = null;
- if(count >0)
- {
- months =awaitsource.OrderBy(x => x.Month).Skip((pageIndex -1) * pageSize).Take(pageSize).ToListAsync();
- }
- return newPaginatedList(pageIndex, pageSize, count, months ?? newList());
- }
大家注意红色部分,日消费清单按照 x.Date.ToString("yyyy-MM") 分组,然后统计各分组合计构建出月消费明细代表。我本来以为这里会生成终极统计 sql 到数据库执行,可跟踪 EFCore 执行,发现并没有,而是先从数据库取出所有日消费明细,之后内存中进行分组统计,坑爹。。。这里,给下之前旧版本实现月度统计的 sql 吧:
- SELECT NEWID() ID, ROW_NUMBER() OVER(ORDER BY CONVERT(CHAR(7), DATE,120)) RowNum, CONVERT(CHAR(7), DATE,120) MONTH, SUM(COST) COST
- FROM DAILY
- WHERE CONVERT(CHAR(7), DATE,120) BETWEEN @START AND @END
- GROUP BY CONVERT(CHAR(7), DATE,120)
本以为 EFCore 会生成类似 sql,可是并没有,可能是因为那个分组非直接数据库字段而是做了特定映射,比如 x.Date.ToString("yyyy-MM") 吧。很明显,手动写统计 sql 的方式效率要高出很多,这里为什么没有手写,还是用了 EFCore 呢?两个原因吧,其一,我想练习下 EFCore,其二,这样可以做到随意切换数据库,我不想在代码层面引入过多跟具体数据库有关的语法。
3)消费明细添加
- public Manifest AddManifest(Manifest manifest)
- {
- _context.Add(manifest);
- vardaily = _context.Dailys.FirstOrDefault(x => x.Date.Date == manifest.Date.Date);
- if(daily !=null)
- {
- daily.Cost += manifest.Cost;
- _context.Update(daily);
- }
- else
- {
- daily =new Daily
- {
- ID = Guid.NewGuid().ToString(),
- Date = manifest.Date,
- Cost = manifest.Cost
- };
- _context.Add(daily);
- }
- _context.SaveChanges();
- return manifest;
- }
这里有 2 点啰嗦下,其一,如果看过我写的旧版本的后端,就会发现,DAL 中添加消费明细就只有一个往 Manifest 表中添加消费明细记录的操作,日消费清单 Daily 表的数据实际上是由 SQLserver 触发器来自动维护的。这里,CodeFirst 生成数据库后,我没添加任何触发器,直接在代码层面去维护,也是想做到应用层面对底层存储无感知。其二,这里直接就_context.SaveChanges(); 了,这是多次数据库操作啊,你的事务呢?需要说明,EFCore 目前是自动实现事务的,所以传统的工作单元啊,应用层面的非分布式数据库事务,已经不用我们操心了。
8、总结
至此,后端的一个初步重构算是完成了,文章中提到的东西,大家如果有更好的实践,望不吝赐教告诉我,共同进步。建议大家看的时候,可以结合新旧两个不同版本,看下路由,跨域,数据访问,DI 等的异同,加深印象。
9、源码地址
https://github.com/KINGGUOKUN/Account/tree/master/Account.Core
顺便请教各位一个问题,我的解决方案中,有些工程有锁标记,有些么有,如下图,没天理,谁知道是什么鬼情况啊?
10、后续计划
1)数据库 SQLServer =》 MySQL
2)部署至 Linux。机器破旧,09 年的,ThinkPad X201i,都不敢装虚拟机,关键是还是个穷逼,你说咋整吧。。。
3)基于认证中间件及授权过滤器,做 API 鉴权。授权基于传统三表权限(用户,角色,权限)
4)分布式缓存、会话缓存及负载均衡
来源: http://www.cnblogs.com/guokun/p/7082987.html