.net core web api + Autofac + EFCore 我的實踐

一、背景html

  去年時候,寫過一篇《Vue2.0 + Element-UI + WebAPI實踐:簡易我的記帳系統》,採用Asp.net Web API + Element-UI。當時主要是爲了練手新學的Vue及基於Vue的PC端前端框架Element-UI,因此文章重點放在了Element-UI上。最近,從鵬城回江城工做已三月有餘,人算安頓,項目也行將上線,算是閒下來了,便想着實踐下以前跟進的.net core,恰好把以前練手系統的後端給重構掉,因而,便有了此文。前端

 

二、技術棧git

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

 

三、項目結構圖web

簡要介紹下各工程:sql

Account:net core Web API類型,爲前端提供Rest服務數據庫

Account.Common:公共工程,與具體業務無關,目前裏邊僅僅有兩個類,自定義業務異常類及錯誤碼枚舉類json

Account.Entity:這個不要問我後端

Account.Repository.Contract:倉儲契約,通常用於隔離服務層與具體的倉儲實現。作隔離的目的是由於與倉儲實現直接依賴的數據訪問技術可能有不少種,隔離後咱們能夠隨時切換api

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。

 

四、.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;
        }

 

五、跨域設置

  鑑於先後端分離,並分屬兩個不一樣的站點,先後端通訊那就涉及到跨域問題,這裏直接採用.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(個人前端站點)、任意標頭、任意請求方式

 

六、異常處理

  按照我的之前慣例,異常處理採用異常過濾器,這裏也不意外, 過濾器定義以下:

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吧,是否是很蛋疼,這句話就是解決這個問題的。

 七、具體請求解析

 請求流經的處理流程以下圖:

由上到下的順序,線上邊是組件之間通訊或依賴經由的協議或契約

咱們以其中消費明細管理爲例,將上圖中工程變爲具體組件, 具體請求處理流程就變成了:

鑑於具體服務實現、數據訪問等跟以前基於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(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目前是自動實現事務的,因此傳統的工做單元啊,應用層面的非分佈式數據庫事務,已經不用咱們操心了。

八、總結

  至此,後端的一個初步重構算是完成了,文章中提到的東西,你們若是有更好的實踐,望不吝賜教告訴我,共同進步。建議你們看的時候,能夠結合新舊兩個不一樣版本,看下路由,跨域,數據訪問,DI等的異同,加深印象。

九、源碼地址

  https://github.com/KINGGUOKUN/Account/tree/master/Account.Core

順便請教各位一個問題,個人解決方案中,有些工程有鎖標記,有些麼有,以下圖,沒天理,誰知道是什麼鬼狀況啊?

十、後續計劃

1)數據庫 SQLServer =》 MySQL

2)部署至Linux。機器破舊,09年的,ThinkPad X201i,都不敢裝虛擬機,關鍵是仍是個窮逼,你說咋整吧。。。

3)基於認證中間件及受權過濾器,作API鑑權。受權基於傳統三表權限(用戶,角色,權限)

4)分佈式緩存、會話緩存及負載均衡

相關文章
相關標籤/搜索