.net core web api + Autofac + EFCore 个人实践

.net core web api + Autofac + EFCore 个人实践

  • 商品编号:
    #45516623_455
    • 原价:
      免费
    • 会员价:
      免费
  • 分类:
    • V1.0.0.0
  • 数量:

购物车中已存在此商品,请在购物车中操作单击跳转购物车

  • 拥有者:KING_GUOKUN
  • 开发语言:C#
  • 开发环境:Visual Studio 2017
  • 数据库:SqlServer/Mysql
  • 商品架构:B/S
  • 代码管理工具:github
  • 大小(M):20 M
  • 编码格式:UTF-8
  • 是否开源:是
  • 开源协议:BSD协议

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、项目结构图    

        QQ截图20170620150529

简要介绍下各工程:

    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模块化注册,这里分别看下。

481360-20170626225021508-1517627669

此工程实现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 = 500
            };
            _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、具体请求解析

 请求流经的处理流程如下图,由上到下的顺序,线上边是组件之间通信或依赖经由的协议或契约,我们以其中消费明细管理为例,将上图中工程变为具体组件, 具体请求处理流程就变成了:

481360-20170627215405149-513716917481360-20170627215815805-1588551813

鉴于具体服务实现、数据访问等跟之前基于asp.net web api的实现已经有了很大不同,这里还是分析下各CRUD方法吧。

1)路由

基于WebAPI或者说Rest的路由,我一向倾向于用特性路由,而非MVC默认路由,因为更灵活,也更容易符合Rest模式。来看具体控制器:

481360-20170627220801446-272994241481360-20170627220322336-1566863121481360-20170627220822039-1131176228481360-20170627220719008-2064022668481360-20170627220843914-510948184 (1)

  大家看到各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(1));
    int count = await source.CountAsync();
    List<Manifest> manifests = null;
    if (count > 0)
    {
        manifests = await source.OrderBy(x => x.Date).Skip((pageIndex - 1) * 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(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)
        });
    int count = await source.CountAsync();
    List<Monthly> months = null;
    if (count > 0)
    {
        months = await source.OrderBy(x => x.Month).Skip((pageIndex - 1) * 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(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);

    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等的异同,加深印象。

10、后续计划

1)数据库 SQLServer =》 MySQL

2)部署至Linux。机器破旧,09年的,ThinkPad X201i,都不敢装虚拟机,关键是还是个穷逼,你说咋整吧。。。

3)基于认证中间件及授权过滤器,做API鉴权。授权基于传统三表权限(用户,角色,权限)

4)分布式缓存、会话缓存及负载均衡

版权所有:KING_GUOKUN


权利声明:本站所有商品信息、客户评价等信息是初心商城重要的数据资源,未经许可,禁止非法转载使用。 注:本站商品信息均来自初心商城,其真实性、准确性和合法性由初心商城负责。

                  初心源说明:初心商城主要为程序员提供开发基础的代码源以及成熟项目,网站中所有的商品有提供收费版本的, 也有提供免费版本的,按照大家各自不同的需求进行购买。实实在在的让程序员只用专注于自己的业务实现你的小梦想, 如果您对我们的成果表示认同并且觉得对你有所帮助我们愿意接受来自各方面的支持^_^。

                  支持:用手机扫描二维码支付

                  支付宝支持我们 微信支持我们

                  您的支持将被用于:
                  1、持续深入的上传更多更好的源代码
                  2、建立更加完善的技术社区
                  3、完善现在系统出现各种问题
                  4、购买域名和租赁服务器

                  1、交易规则

                  2、发货方式

                  1、自动:在上方保障服务中标有自动发货的商品,拍下后,将会自动收到来自卖家的商品获取(下载)链接

                  2、手动:在上方保障服务中标有手动发货的商品,拍下后,卖家会收到邮件,也可通过QQ或订单中的电话联系对方。

                  3、退款说明

                  1、描述:源码描述(含标题)与实际源码不一致的(例:描述PHP实际为ASP、描述的功能实际缺少、版本不符等)

                  2、演示:有演示站时,与实际源码小于95%一致的(但描述中有"不保证完全一样、有变化的可能性"类似显著声明的除外)

                  3、发货:手动发货源码,在卖家未发货前,已申请退款的

                  4、服务:卖家不提供安装服务或需额外收费的(但描述中有显著声明的除外)

                  5、其它:如质量方面的硬性常规问题等

                  备注:经核实符合上述任一,均支持退款,但卖家予以积极解决问题则除外。交易中的商品,卖家无法对描述进行修改!

                  4、注意事项

                  1、客户买完之后未确认收货,将不会收到下载地址和下载码,确认收货之后才能收到下载地址和下载码。

                  2、在未拍下前,双方在QQ上所商定的内容,亦可成为纠纷评判依据(商定与描述冲突时,商定为准);

                  3、在商品同时有网站演示与图片演示,且站演与图演不一致时,默认按图演作为纠纷评判依据(特别声明或有商定除外);

                  4、在没有"无任何正当退款依据"的前提下,写有"一旦售出,概不支持退款"等类似的声明,视为无效声明;

                  5、虽然交易产生纠纷的几率很小,但请尽量保留如聊天记录这样的重要信息,以防产生纠纷时出现问题不明确的情况。

                  5、交易声明

                  1、本站作为直卖平台,依据交易合同(商品描述、交易前商定的内容)来保障交易的安全及买卖双方的权益;

                  2、非平台线上交易的商品,出现任何后果均与本站无关;无论卖家以何理由要求线下交易的,请联系管理举报。

                  初心Logo

                  初心商城| 初心系列| 初心博客| 版本历史| 系统反馈

                  © 2016-2018 山西米立信息技术有限公司 保留所有权利 京ICP备16055626号
                  违法和不良信息举报电话:186-2950-9347,本网站所列数据,除特殊说明,所有数据均出自我工作室
                  本网站兼容所有主流浏览器,不支持手机自适应

                  返回顶部小火箭