1、背景

  去年时候,写过一篇《Vue2.0 + Element-UI + WebAPI实践:简易个人记账系统》,采用Asp.net Web API + Element-UI。当时主要是为了练手新学的Vue及基于Vue的PC端前端框架Element-UI,所以文章重点放在了Element-UI上。最近,从鹏城回江城工作已三月有余,人算安顿,项目也行将上线,算是闲下来了,便想着实践下之前跟进的.net core,刚好把之前练手系统的后端给重构掉,于是,便有了此文。

2、技术栈

  Asp.net core Web API + Autofac + EFCore + Element-UI + SqlServer2008R2

3、项目结构图

简要介绍下各工程:

Account:net core Web API类型,为前端提供Rest服务

Account.Common:公共工程,与具体业务无关,目前里边仅仅有两个类,自定义业务异常类及错误码枚举类

Account.Entity:这个不要问我

Account.Repository.Contract:仓储契约,一般用于隔离服务层与具体的仓储实现。做隔离的目的是因为与仓储实现直接依赖的数据访问技术可能有很多种,隔离后我们可以随时切换

Account.Repository.EF:仓储服务的EFCore实现,从工程名字应该很容易可以看出来,它实现Account.Repository.Contract。如果这里不想用EF,那我们可以随时新建个工程Account.Repository.Dapper,增加Dapper的实现

Account.Service.Contract:服务层契约,用来隔离Account工程与具体业务服务实现

Account.Service:业务服务,实现Account.Service.Contract这个业务服务层中的契约

Account.VueFE:这个与之前一样,静态前端站点,从项目工程图标上那个互联网球球还有名字中VueFE你就应该能猜出来

  与之前那篇文章重点在Element-UI和Vue不同,这篇文章重点在后台,在.net core。

4、.net core与Autofac集成

1)Startup构造函数中添加Autofac配置文件

public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
.AddJsonFile("autofac.json")
.AddEnvironmentVariables();
Configuration = builder.Build();
}

  红色部分便是Autofac的配置文件,具体内容如下:

{
"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<AccountContext>(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<AccountContext>(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 readonly ILogger<CustomExceptionFilterAttribute> _logger; public CustomExceptionFilterAttribute(ILogger<CustomExceptionFilterAttribute> logger)
{
_logger = logger;
} public override void OnException(ExceptionContext context)
{
Exception exception = context.Exception;
JsonResult result = null;
if (exception is BusinessException)
{
result = new JsonResult(exception.Message)
{
StatusCode = exception.HResult
};
}
else
{
result = new JsonResult("服务器处理出错")
{
StatusCode =
};
_logger.LogError(null, exception, "服务器处理出错", null);
} context.Result = result;
}
}

  简言之就是,判断操作方法中抛出的是什么异常,如果是由我们业务代码主动引发的业务级别异常,也就是类型为自定义BusinessException,则直接设置相应json结果状态码及 错误信息为我们引发异常时定义的状态码及错误信息;如果是框架或数据库操作失败引发的,被动式的异常,这种错误信息不应该暴露给前端,而且,这种服务器内部处理出错,理应统一设置状态码为500,还需要记录异常堆栈,如上的else分支所做。

  之后,将此过滤器全局注册。Core中全局注册过滤器的德行如下:

public IServiceProvider ConfigureServices(IServiceCollection services)
{
services.AddDbContext<AccountContext>(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 async Task<PaginatedList<Manifest>> GetManifests(DateTime start, DateTime end, int pageIndex, int pageSize)
{
var source = _context.Manifests.Where(x => x.Date >= start && x.Date < new DateTime(end.Year, end.Month, end.Day).AddDays());
int count = await source.CountAsync();
List<Manifest> manifests = null;
if (count > )
{
manifests = await source.OrderBy(x => x.Date).Skip((pageIndex - ) * pageSize).Take(pageSize).ToListAsync();
} return new PaginatedList<Manifest>(pageIndex, pageSize, count, manifests ?? new List<Manifest>());
}

典型的EF分页查询,先获取符合条件总记录数,然后排序并取指定页数据,没毛病。

日消费清单也类似,但关于月清单和年清单,这里要多说下。 月清单和年清单都是统计的日消费清单Daily,具体Daily又是由日消费明细Manifest支撑的。

来看下月消费清单的查询:

public async Task<PaginatedList<Monthly>> GetMonthlys(string start, string end, int pageIndex, int pageSize)
{
var source = _context.Dailys
.Where(x => x.Date >= DateTime.Parse(start) && x.Date <= DateTime.Parse(end).AddMonths().AddSeconds(-))
.GroupBy(x => x.Date.ToString("yyyy-MM"), (k, v) =>
new Monthly
{
ID = Guid.NewGuid().ToString(),
Month = k,
Cost = v.Sum(x => x.Cost)
});
int count = await source.CountAsync();
List<Monthly> months = null;
if (count > )
{
months = await source.OrderBy(x => x.Month).Skip((pageIndex - ) * pageSize).Take(pageSize).ToListAsync();
} return new PaginatedList<Monthly>(pageIndex, pageSize, count, months ?? new List<Monthly>());
}

大家注意红色部分,日消费清单按照x.Date.ToString("yyyy-MM")分组,然后统计各分组合计构建出月消费明细代表。我本来以为这里会生成终极统计sql到数据库执行,可跟踪EFCore执行,发现并没有,而是先从数据库取出所有日消费明细,之后内存中进行分组统计,坑爹。。。这里,给下之前旧版本实现月度统计的sql吧:

SELECT NEWID() ID, ROW_NUMBER() OVER(ORDER BY CONVERT(CHAR(), DATE, )) RowNum, CONVERT(CHAR(), DATE, ) MONTH, SUM(COST) COST
FROM DAILY
WHERE CONVERT(CHAR(), DATE, ) BETWEEN @START AND @END
GROUP BY CONVERT(CHAR(), DATE, )                                                                            

本以为EFCore会生成类似sql,可是并没有,可能是因为那个分组非直接数据库字段而是做了特定映射,比如x.Date.ToString("yyyy-MM")吧。很明显,手动写统计sql的方式效率要高出很多,这里为什么没有手写,还是用了EFCore呢?两个原因吧,其一,我想练习下EFCore,其二,这样可以做到随意切换数据库,我不想在代码层面引入过多跟具体数据库有关的语法。

3)消费明细添加

public Manifest AddManifest(Manifest manifest)
{
_context.Add(manifest); var daily = _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)分布式缓存、会话缓存及负载均衡

.net core web api + Autofac + EFCore 个人实践的更多相关文章

  1. List多个字段标识过滤 IIS发布.net core mvc web站点 ASP.NET Core 实战:构建带有版本控制的 API 接口 ASP.NET Core 实战:使用 ASP.NET Core Web API 和 Vue.js 搭建前后端分离项目 Using AutoFac

    List多个字段标识过滤 class Program{  public static void Main(string[] args) { List<T> list = new List& ...

  2. ASP.NET Core Web API 最佳实践指南

    原文地址: ASP.NET-Core-Web-API-Best-Practices-Guide 介绍 当我们编写一个项目的时候,我们的主要目标是使它能如期运行,并尽可能地满足所有用户需求. 但是,你难 ...

  3. [转]ASP.NET Core Web API 最佳实践指南

    原文地址: ASP.NET-Core-Web-API-Best-Practices-Guide 转自 介绍# 当我们编写一个项目的时候,我们的主要目标是使它能如期运行,并尽可能地满足所有用户需求. 但 ...

  4. .Net Core Web Api实践之中间件的使用(一)

    前言:从2019年年中入坑.net core已半年有余,总体上来说虽然感觉坑多,但是用起来还是比较香的.本来我是不怎么喜欢写这类实践分享或填坑记录的博客的,因为初步实践坑多,文章肯定也会有各种错误,跟 ...

  5. ASP.NET Core Web API下事件驱动型架构的实现(二):事件处理器中对象生命周期的管理

    在上文中,我介绍了事件驱动型架构的一种简单的实现,并演示了一个完整的事件派发.订阅和处理的流程.这种实现太简单了,百十行代码就展示了一个基本工作原理.然而,要将这样的解决方案运用到实际生产环境,还有很 ...

  6. 在docker中运行ASP.NET Core Web API应用程序

    本文是一篇指导快速演练的文章,将介绍在docker中运行一个ASP.NET Core Web API应用程序的基本步骤,在介绍的过程中,也会对docker的使用进行一些简单的描述.对于.NET Cor ...

  7. docker中运行ASP.NET Core Web API

    在docker中运行ASP.NET Core Web API应用程序 本文是一篇指导快速演练的文章,将介绍在docker中运行一个ASP.NET Core Web API应用程序的基本步骤,在介绍的过 ...

  8. Gitlab CI 自动部署 asp.net core web api 到Docker容器

    为什么要写这个? 在一个系统长大的过程中会经历不断重构升级来满足商业的需求,而一个严谨的商业系统需要高效.稳定.可扩展,有时候还不得不考虑成本的问题.我希望能找到比较完整的开源解决方案来解决持续集成. ...

  9. [译]ASP.NET Core Web API 中使用Oracle数据库和Dapper看这篇就够了

    [译]ASP.NET Core Web API 中使用Oracle数据库和Dapper看这篇就够了 本文首发自:博客园 文章地址: https://www.cnblogs.com/yilezhu/p/ ...

随机推荐

  1. Day1 Python 介绍及基础

    ******************本文目录******************一.Pyhon基本介绍 1.Why Python? 2. Python 是一门什么样的语言? 3.Python的优缺点: ...

  2. “永恒之蓝"漏洞的紧急应对--毕业生必看

    早上6点多起床了,第一次起这么早,昨天晚上12点多,看到了一则紧急通知,勒索软件通过微软"永恒之蓝"漏洞针对教育网进行了大规模的攻击,而且有很多同学中招.中招后的结果如下图所示. ...

  3. Collection<E>、Iterable<T>和Iterator<E>接口

    Collection接口 public interface Collection<E>extends Iterable<E> Collection接口主要包含以下方法: Ite ...

  4. Day4-内置函数--未完待续,慢慢写

    内置函数:https://docs.python.org/3/library/functions.html?highlight=built#ascii 未完待续....

  5. Servlet起步

    什么是Servlet Servlet是sun公司制定的用来扩展web服务器功能的组件规范,通俗理解为遵循Servlet规范开发的实现了某个功能的Java组件.该组件没有 main 方法,不能独立地运行 ...

  6. 2-LPC1778之GPIO

    其实这篇文章主要是介绍自己为其写的GPIO库,自己借鉴了原子写的STM32,野火写的K60,还有LPC官方库,然后按照自己平时用的,然后写了一个..其实写库的主要目的是为了方便(主要是方便操作)以后自 ...

  7. 2017年PHP培训机构排名

    2017年PHP培训机构排名 PHP培训属于IT培训的一个领域.随着互联网的火爆,PHP也变得异常火爆.通过对PHP培训机构的调查与了解,到底学员选择哪一家的PHP培训机构才能够学到真正的技术,PHP ...

  8. 基于binlog来分析mysql的行记录修改情况(python脚本分析)

          最近写完mysql flashback,突然发现还有有这种使用场景:有些情况下,可能会统计在某个时间段内,MySQL修改了多少数据量?发生了多少事务?主要是哪些表格发生变动?变动的数量是怎 ...

  9. find命令之xargs,exec

    一,find命令之xargs: 在 使用 find命令的-exec选项处理匹配到的文件时, find命令将所有匹配到的文件一起传递给exec执行.但有些系统对能够传递给exec的命 令长度有限制,这样 ...

  10. mysql 数据库优化要点

    1尽可能使用更小的类型 2尽可能的定义字段为not null,除非这个字段需要设置成null 3如果没有可变长度的字段varchar,尽可使用char 4所有字段应该有默认值 5所有的数据应该在保存之 ...