ChuanGoing 2019-11-17
這篇原本想把事務處理、日志處理、錯誤處理、授權與鑒權一並介紹完的,授權和鑒權我想結合自定義權限來介紹,全部放到這里篇幅可能太長,因此權限部分將會在下篇來介紹。先說下我接下來的打算把,下篇將介紹權限控制,結合Oauth2.0和OpenId(OIDC)以及自定義權限來介紹;完了后會結合之前所介紹的基礎來實現一個簡單的電商網站,當然是利用領域驅動設計來實現。我的這個系列的主題就是領域驅動設計,實現簡單電商網站時將會深入的講解下領域的划分原則及領域服務的場景,中間可能會嘗試部分業務實現事件驅動。
本篇學習曲線:
1.日志記錄
2.錯誤處理
3.事務處理
日志記錄
NLog是一個記錄日志組件,和log4net一樣被廣泛使用,它可以將日志保存到文本文件、CSV、控制台、VS調試窗口、數據庫等。在之前例子中的WebApi項目中添加NLog.Web.AspNetCore的Nuget包,並添加如下配置:
簡單介紹下配置信息,“targets”配置每個輸出配置,我這里有3個輸出:database、allfile、ownfile,分別表示輸出到數據庫和對應路徑的日志文件下。
"rules"規則配置了4條:
1.將Debug以上級別(含)信息輸出到allfile
2.忽略Microsoft.*開頭的信息(對應的輸出沒有配置到任何文件),此配置一般忽略即可
3.將Debug以上級別(含)信息輸出到ownfile(注意這里配置和allfile一樣,一般配置級別高點的日志信息)
4.將Warn以上級別(含)信息輸出到數據庫
完了后,在Program.cs Main方法里面注冊NLog:
var logger = NLogBuilder.ConfigureNLog($"Nlog.config").GetCurrentClassLogger(); try { CreateWebHostBuilder(args).Build().Run(); } catch (Exception ex) { logger.Error(ex, "Stopped program because of exception"); throw ex; } finally { NLog.LogManager.Shutdown(); }
注意不要忘了啟用NLog組件使之生效
在OrderController的Add方法中加入以下代碼:
用postman簡單測試下,我們可以看到執行目錄中多出來了日志信息
錯誤處理
這里一般我們關心的錯誤大概有兩類:
1.內部錯誤,即通過框架(Mvc)管道准確的傳入到內部系統中並發生錯誤的此類信息
2.框架(Mvc)執行管道的某些中間件時發生的錯誤或被中間件禁止繼續訪問的請求
因此,定義如下3個類:

public class InnerException : Exception { /// <summary> /// 內部錯誤代碼 /// </summary> public int? ErrorCode { get; } public InnerException(int errorCode) : base() { ErrorCode = errorCode; } public InnerException(int errorCode, string message) : base(message) { ErrorCode = errorCode; } public InnerException(int code, string message, Exception exception) : base(message, exception) { ErrorCode = code; } }

public class MessageCodes { #region 公用 /// <summary> /// 成功 /// </summary> public const int Success = 20101000; /// <summary> /// 警告 /// </summary> public const int Warning = 20102000; /// <summary> /// 錯誤 /// </summary> public const int Error = 20103000; /// <summary> /// 數據驗證錯誤 /// </summary> public const int DataValidationError = 20104000; /// <summary> /// 數據不存在 /// </summary> public const int DataNotFound = 20105000; /// <summary> /// 非法的數據狀態 /// </summary> public const int IllegalState = 20106000; /// <summary> /// 參數無效 /// </summary> public const int InvalidParams = 20107000; /// <summary> /// 輸入非法 /// </summary> public const int IllegalInput = 20108000; /// <summary> /// 鑒權成功 /// </summary> public const int AuthSuccess = 20109000; #endregion }

public class WebException: InnerException { public HttpStatusCode HttpStatus { get; set; } public HttpRequest Request { get; private set; } public WebException(HttpStatusCode httpStatus, int errorCode, string message) : base(errorCode, message) { HttpStatus = httpStatus; } public WebException(HttpStatusCode httpStatus, int errorCode, string message, HttpRequest request) : this(httpStatus, errorCode, message) { Request = request; } public WebException(int errorCode, string message) : base(errorCode, message) { HttpStatus = HttpStatusCode.BadRequest; } }
通過Aop,很方便就可以實現錯誤信息的處理:

public class ExceptionFilter : IExceptionFilter { private readonly ILogger<ExceptionFilter> _logger; public ExceptionFilter(ILogger<ExceptionFilter> logger) { _logger = logger; } public void OnException(ExceptionContext context) { _logger.LogError(context.Exception, context.Exception.Message); #region Ioc/automapper等中間件對錯誤信息進行了包裝,需要解包 //web錯誤:驗證/鑒權等 var webException = GetException<Base.Exceptions.WebException>(context.Exception); if (webException != null) { context.Result = new JsonResult(new { ErrorCode = webException.ErrorCode ?? MessageCodes.Error, webException.Message }) { StatusCode = (int)webException.HttpStatus }; return; } //內部錯誤 var exception = GetException<InnerException>(context.Exception); if (exception != null) { context.Result = new JsonResult(new { ErrorCode = exception.ErrorCode ?? MessageCodes.Error, exception.Message }) { StatusCode = (int)HttpStatusCode.InternalServerError }; return; } #endregion } private TException GetException<TException>(Exception exception) where TException : Exception { if (exception == null) { return null; } if (exception is TException tException) { return tException; } else { return GetException<TException>(exception.InnerException); } } }
同時,Startup.cs的ConfigureServices中注冊一下:
services.AddMvc(mvcOptions => { mvcOptions.Filters.Add<ExceptionFilter>(); })
即完成了錯誤信息並且錯誤信息會寫入相應配置的輸出中。
事務處理
UnitOfWork又稱工作單元,為了保證數據操作完整性,我們將處理數據的的操作統一放在一個事務中,我們這里利用UnitOfWork來實現事務處理。
首先定義IUnitOfWork及UnitOfWork實現:

public interface IUnitOfWork { void Begin(IsolationLevel level = IsolationLevel.Unspecified); void SaveChanges(); void Failed(); }

public class UnitOfWork : IUnitOfWork { private ITransactionRepository _repository; public UnitOfWork(ITransactionRepository repository) { _repository = repository; } public virtual void Begin(IsolationLevel level = IsolationLevel.Unspecified) { _repository.BeginTransaction(level); } public virtual void SaveChanges() { _repository.Commit(); } public virtual void Failed() { _repository.Rollback(); } }
其中,UnitOfWork依賴於ITransactionRepository的實現:

public interface ITransactionRepository { /// <summary> /// 打開事務 /// </summary> /// <param name="level"></param> void BeginTransaction(IsolationLevel level = IsolationLevel.Unspecified); /// <summary> /// 提交事務 /// </summary> void Commit(); /// <summary> /// 事務回滾 /// </summary> void Rollback(); }
利用DapperRepository繼承ITransactionRepository並實現:

public virtual void BeginTransaction(IsolationLevel level = IsolationLevel.Unspecified) { DbContext.BeginTransaction(level); } public virtual void Commit() { DbContext.Commit(); } public virtual void Rollback() { DbContext.RollBack(); }
基本功能實現后,如何使用呢?這里還是需要利用Aop:

public class UnitOfWorkAttribute : AbstractInterceptorAttribute { public override Task Invoke(AspectContext context, AspectDelegate next) { if (context.Implementation is IApplicationService applicationService) { var uow = applicationService.UnitOfWork; uow.Begin(); var aspectDelegate = next(context); if (aspectDelegate.Exception != null) { uow.Failed(); throw aspectDelegate.Exception; } else { uow.SaveChanges(); return aspectDelegate; } } else { return next(context); } } }
因此,我們還需要在Application項目中添加如下代碼:

public class ServiceBase<TEntity, TPrimaryKey> : IApplicationService where TEntity : class, IEntity<TPrimaryKey> { protected IMapper Mapper { get; private set; } public virtual IUnitOfWork UnitOfWork { get; private set; } public ServiceBase(IComponentContext container, ICommandRepository<TEntity, TPrimaryKey> repository) { Mapper = container.Resolve<IMapper>(); UnitOfWork = container.Resolve<IUnitOfWork>(new TypedParameter(typeof(ITransactionRepository), repository)); } }
Application中的每個服務去繼承上面的ServiceBase,因此每個Application服務都具有了事務處理能力
public interface IOrderService : IScopeInstance { [UnitOfWork] void Add(OrderViewModel order); OrderViewResult Get(string sn); }
程序運行時,Add方法前后形成切面,如下圖所示,next(context)這里執行的就是Add方法,執行前開啟事務,執行后提交
利用Aop特性切面實現事務的無感注入(Asp.net Core 系列之--1.事件驅動初探:簡單事件總線實現(SimpleEventBus)Ioc/DI小節中引入了AspectCore動態代理),底層還是依賴IDbConnection的事務相關接口,完整的事務處理大概就是這樣了。
詳細代碼在Github的https://github.com/ChuanGoing/Start.git 的Domain分支可以找到。