【ABP框架系列學習】介紹篇(1)
0.引言
該系列博文主要在【官方文件】及【tkbSimplest】ABP框架理論研究系列博文的基礎上進行總結的,或許大家會質問,別人都已經翻譯過了,這不是多此一舉嗎?原因如下:
1.【tkbSimplest】的相關博文由於撰寫得比較早的,在參照官方文件學習的過程中,發現部分知識未能及時同步(當前V4.0.2版本),如【EntityHistory】、【Multi-Lingual Engities】章節未涉及、【Caching】章節沒有Entity Caching等內容。
2.進一步深入學習ABP的理論知識。
3.藉此機會提高英文文件的閱讀能力,故根據官方當前最新的版本,並在前人的基礎上,自己也感受一下英文幫助文件的魅力。
好了,下面開始進入正題。
1.APB是什麼?
ABP是ASP.NET Boilerplate的簡稱,從英文字面上理解它是一個關於ASP.NET的模板,在github上已經有5.7k的star(截止2018年11月21日)。官方的解釋:ABP是一個開源且文件友好的應用程式框架。ABP不僅僅是一個框架,它還提供了一個最徍實踐的基於領域驅動設計(DDD)的體系結構模型。
ABP與最新的ASP.NET CORE和EF CORE版本保持同步,同樣也支援ASP.NET MVC 5.x和EF6.x。
2.一個快速事例
讓我們研究一個簡單的類,看看ABP具有哪些優點:
publicclass TaskAppService : ApplicationService, ITaskAppService { private readonly IRepository<Task> _taskRepository; public TaskAppService(IRepository<Task> taskRepository) { _taskRepository = taskRepository; } [AbpAuthorize(MyPermissions.UpdateTasks)]public async Task UpdateTask(UpdateTaskInput input) { Logger.Info("Updating a task for input: " + input); var task = await _taskRepository.FirstOrDefaultAsync(input.TaskId); if (task == null) { throw new UserFriendlyException(L("CouldNotFindTheTaskMessage")); } input.MapTo(task); } }
這裡我們看到一個Application Service(應用服務)方法。在DDD中,應用服務直接用於表現層(UI)執行應用程式的用例。那麼在UI層中就可以通過javascript ajax的方式呼叫UpdateTask方法。
var _taskService = abp.services.app.task; _taskService.updateTask(...);
3.ABP的優點
通過上述事例,讓我們來看看ABP的一些優點:
依賴注入(Dependency Injection):ABP使用並提供了傳統的DI基礎設施。上述TaskAppService類是一個應用服務(繼承自ApplicationService),所以它按照慣例以短暫(每次請求建立一次)的形式自動註冊到DI容器中。同樣的,也可以簡單地注入其他依賴(如事例中的IRepository<Task>)。
部分原始碼分析:TaskAppService類繼承自ApplicationService,IApplicaitonServcie又繼承自ITransientDependency介面,在ABP框架中已經將ITransientDependency介面注入到DI容器中,所有繼承自ITransientDependency介面的類或介面都會預設注入。
//空介面 public interface ITransientDependency { } //應用服務介面 public interface IApplicationService : ITransientDependency { } //倉儲介面 public interface IRepository : ITransientDependency { }View Code
public class BasicConventionalRegistrar : IConventionalDependencyRegistrar { public void RegisterAssembly(IConventionalRegistrationContext context) { //注入到IOC,所有繼承自ITransientDependency的類、介面等都會預設注入 context.IocManager.IocContainer.Register( Classes.FromAssembly(context.Assembly) .IncludeNonPublicTypes() .BasedOn<ITransientDependency>() .If(type => !type.GetTypeInfo().IsGenericTypeDefinition) .WithService.Self() .WithService.DefaultInterfaces() .LifestyleTransient() ); //Singleton context.IocManager.IocContainer.Register( Classes.FromAssembly(context.Assembly) .IncludeNonPublicTypes() .BasedOn<ISingletonDependency>() .If(type => !type.GetTypeInfo().IsGenericTypeDefinition) .WithService.Self() .WithService.DefaultInterfaces() .LifestyleSingleton() ); //Windsor Interceptors context.IocManager.IocContainer.Register( Classes.FromAssembly(context.Assembly) .IncludeNonPublicTypes() .BasedOn<IInterceptor>() .If(type => !type.GetTypeInfo().IsGenericTypeDefinition) .WithService.Self() .LifestyleTransient() ); }View Code
倉儲(Repository):ABP可以為每一個實體建立一個預設的倉儲(如事例中的IRepository<Task>)。預設的倉儲提供了很多有用的方法,如事例中的FirstOrDefault方法。當然,也可以根據需求擴充套件預設的倉儲。倉儲抽象了DBMS和ORMs,並簡化了資料訪問邏輯。
授權(Authorization):ABP可以通過宣告的方式檢查許可權。如果當前使用者沒有【update task】的許可權或沒有登入,則會阻止訪問UpdateTask方法。ABP不僅提供了宣告屬性的方式授權,而且還可以通過其它的方式。
部分原始碼分析:AbpAuthorizeAttribute類實現了Attribute,可在類或方法上通過【AbpAuthorize】宣告。
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)] public class AbpAuthorizeAttribute : Attribute, IAbpAuthorizeAttribute { /// <summary> /// A list of permissions to authorize. /// </summary> public string[] Permissions { get; } /// <summary> /// If this property is set to true, all of the <see cref="Permissions"/> must be granted. /// If it's false, at least one of the <see cref="Permissions"/> must be granted. /// Default: false. /// </summary> public bool RequireAllPermissions { get; set; } /// <summary> /// Creates a new instance of <see cref="AbpAuthorizeAttribute"/> class. /// </summary> /// <param name="permissions">A list of permissions to authorize</param> public AbpAuthorizeAttribute(params string[] permissions) { Permissions = permissions; } }View Code
通過AuthorizationProvider類中的SetPermissions方法進行自定義授權。
public abstract class AuthorizationProvider : ITransientDependency { /// <summary> /// This method is called once on application startup to allow to define permissions. /// </summary> /// <param name="context">Permission definition context</param> public abstract void SetPermissions(IPermissionDefinitionContext context); }View Code
驗證(Validation):ABP自動檢查輸入是否為null。它也基於標準資料註釋特性和自定義驗證規則驗證所有的輸入屬性。如果請求無效,它會在客戶端丟擲適合的驗證異常。
部分原始碼分析:ABP框架中主要通過攔截器ValidationInterceptor(AOP實現方式之一,)實現驗證,該攔截器在ValidationInterceptorRegistrar的Initialize方法中呼叫。
internal static class ValidationInterceptorRegistrar { public static void Initialize(IIocManager iocManager) { iocManager.IocContainer.Kernel.ComponentRegistered += Kernel_ComponentRegistered; } private static void Kernel_ComponentRegistered(string key, IHandler handler) { if (typeof(IApplicationService).GetTypeInfo().IsAssignableFrom(handler.ComponentModel.Implementation)) { handler.ComponentModel.Interceptors.Add(new InterceptorReference(typeof(ValidationInterceptor))); } } }View Code
public class ValidationInterceptor : IInterceptor { private readonly IIocResolver _iocResolver; public ValidationInterceptor(IIocResolver iocResolver) { _iocResolver = iocResolver; } public void Intercept(IInvocation invocation) { if (AbpCrossCuttingConcerns.IsApplied(invocation.InvocationTarget, AbpCrossCuttingConcerns.Validation)) { invocation.Proceed(); return; } using (var validator = _iocResolver.ResolveAsDisposable<MethodInvocationValidator>()) { validator.Object.Initialize(invocation.MethodInvocationTarget, invocation.Arguments); validator.Object.Validate(); } invocation.Proceed(); } }View Code
自定義Customvalidator類
public class CustomValidator : IMethodParameterValidator { private readonly IIocResolver _iocResolver; public CustomValidator(IIocResolver iocResolver) { _iocResolver = iocResolver; } public IReadOnlyList<ValidationResult> Validate(object validatingObject) { var validationErrors = new List<ValidationResult>(); if (validatingObject is ICustomValidate customValidateObject) { var context = new CustomValidationContext(validationErrors, _iocResolver); customValidateObject.AddValidationErrors(context); } return validationErrors; } }View Code
審計日誌(Audit Logging):基於約定和配置,使用者、瀏覽器、IP地址、呼叫服務、方法、引數、呼叫時間、執行時長以及其它資訊會為每一個請求自動儲存。
部分原始碼分析:ABP框架中主要通過攔截器AuditingInterceptor(AOP實現方式之一,)實現審計日誌,該攔截器在AuditingInterceptorRegistrar的Initialize方法中呼叫。
internal static class AuditingInterceptorRegistrar { public static void Initialize(IIocManager iocManager) { iocManager.IocContainer.Kernel.ComponentRegistered += (key, handler) => { if (!iocManager.IsRegistered<IAuditingConfiguration>()) { return; } var auditingConfiguration = iocManager.Resolve<IAuditingConfiguration>(); if (ShouldIntercept(auditingConfiguration, handler.ComponentModel.Implementation)) { handler.ComponentModel.Interceptors.Add(new InterceptorReference(typeof(AuditingInterceptor))); } }; }View Code
private static bool ShouldIntercept(IAuditingConfiguration auditingConfiguration, Type type) { if (auditingConfiguration.Selectors.Any(selector => selector.Predicate(type))) { return true; } if (type.GetTypeInfo().IsDefined(typeof(AuditedAttribute), true)) { return true; } if (type.GetMethods().Any(m => m.IsDefined(typeof(AuditedAttribute), true))) { return true; } return false; } }View Code
internal class AuditingInterceptor : IInterceptor { private readonly IAuditingHelper _auditingHelper; public AuditingInterceptor(IAuditingHelper auditingHelper) { _auditingHelper = auditingHelper; } public void Intercept(IInvocation invocation) { if (AbpCrossCuttingConcerns.IsApplied(invocation.InvocationTarget, AbpCrossCuttingConcerns.Auditing)) { invocation.Proceed(); return; } if (!_auditingHelper.ShouldSaveAudit(invocation.MethodInvocationTarget)) { invocation.Proceed(); return; } var auditInfo = _auditingHelper.CreateAuditInfo(invocation.TargetType, invocation.MethodInvocationTarget, invocation.Arguments); if (invocation.Method.IsAsync()) { PerformAsyncAuditing(invocation, auditInfo); } else { PerformSyncAuditing(invocation, auditInfo); } } private void PerformSyncAuditing(IInvocation invocation, AuditInfo auditInfo) { var stopwatch = Stopwatch.StartNew(); try { invocation.Proceed(); } catch (Exception ex) { auditInfo.Exception = ex; throw; } finally { stopwatch.Stop(); auditInfo.ExecutionDuration = Convert.ToInt32(stopwatch.Elapsed.TotalMilliseconds); _auditingHelper.Save(auditInfo); } } private void PerformAsyncAuditing(IInvocation invocation, AuditInfo auditInfo) { var stopwatch = Stopwatch.StartNew(); invocation.Proceed(); if (invocation.Method.ReturnType == typeof(Task)) { invocation.ReturnValue = InternalAsyncHelper.AwaitTaskWithFinally( (Task) invocation.ReturnValue, exception => SaveAuditInfo(auditInfo, stopwatch, exception) ); } else //Task<TResult> { invocation.ReturnValue = InternalAsyncHelper.CallAwaitTaskWithFinallyAndGetResult( invocation.Method.ReturnType.GenericTypeArguments[0], invocation.ReturnValue, exception => SaveAuditInfo(auditInfo, stopwatch, exception) ); } } private void SaveAuditInfo(AuditInfo auditInfo, Stopwatch stopwatch, Exception exception) { stopwatch.Stop(); auditInfo.Exception = exception; auditInfo.ExecutionDuration = Convert.ToInt32(stopwatch.Elapsed.TotalMilliseconds); _auditingHelper.Save(auditInfo); } }View Code
工作單元(Unit Of Work):在ABP中,應用服務方法預設視為一個工作單元。它會自動建立一個連線並在方法的開始位置開啟事務。如果方法成功完成並沒有異常,事務會提交併釋放連線。即使這個方法使用不同的倉儲或方法,它們都是原子的(事務的)。當事務提交時,實體的所有改變都會自動儲存。如上述事例所示,甚至不需要呼叫_repository.Update(task)方法。
部分原始碼分析:ABP框架中主要通過攔截器UnitOfWorkInterceptor(AOP實現方式之一,)實現工作單元,該攔截器在UnitOfWorkRegistrar的Initialize方法中呼叫。
internal class UnitOfWorkInterceptor : IInterceptor { private readonly IUnitOfWorkManager _unitOfWorkManager; private readonly IUnitOfWorkDefaultOptions _unitOfWorkOptions; public UnitOfWorkInterceptor(IUnitOfWorkManager unitOfWorkManager, IUnitOfWorkDefaultOptions unitOfWorkOptions) { _unitOfWorkManager = unitOfWorkManager; _unitOfWorkOptions = unitOfWorkOptions; } /// <summary> /// Intercepts a method. /// </summary> /// <param name="invocation">Method invocation arguments</param> public void Intercept(IInvocation invocation) { MethodInfo method; try { method = invocation.MethodInvocationTarget; } catch { method = invocation.GetConcreteMethod(); } var unitOfWorkAttr = _unitOfWorkOptions.GetUnitOfWorkAttributeOrNull(method); if (unitOfWorkAttr == null || unitOfWorkAttr.IsDisabled) { //No need to a uow invocation.Proceed(); return; } //No current uow, run a new one PerformUow(invocation, unitOfWorkAttr.CreateOptions()); } private void PerformUow(IInvocation invocation, UnitOfWorkOptions options) { if (invocation.Method.IsAsync()) { PerformAsyncUow(invocation, options); } else { PerformSyncUow(invocation, options); } } private void PerformSyncUow(IInvocation invocation, UnitOfWorkOptions options) { using (var uow = _unitOfWorkManager.Begin(options)) { invocation.Proceed(); uow.Complete(); } } private void PerformAsyncUow(IInvocation invocation, UnitOfWorkOptions options) { var uow = _unitOfWorkManager.Begin(options); try { invocation.Proceed(); } catch { uow.Dispose(); throw; } if (invocation.Method.ReturnType == typeof(Task)) { invocation.ReturnValue = InternalAsyncHelper.AwaitTaskWithPostActionAndFinally( (Task) invocation.ReturnValue, async () => await uow.CompleteAsync(), exception => uow.Dispose() ); } else //Task<TResult> { invocation.ReturnValue = InternalAsyncHelper.CallAwaitTaskWithPostActionAndFinallyAndGetResult( invocation.Method.ReturnType.GenericTypeArguments[0], invocation.ReturnValue, async () => await uow.CompleteAsync(), exception => uow.Dispose() ); } } }View Code
異常處理(Exception):在使用了ABP框架的Web應用程式中,我們幾乎不用手動處理異常。預設情況下,所有的異常都會自動處理。如果發生異常,ABP會自動記錄並給客戶端返回合適的結果。例如:對於一個ajax請求,返回一個json物件給客戶端,表明發生了錯誤。但會對客戶端隱藏實際的異常,除非像上述事例那樣使用UserFriendlyException方法丟擲。它也理解和處理客戶端的錯誤,並向客戶端顯示合適的資訊。
部分原始碼分析:UserFriendlyException丟擲異常方法。
[Serializable] public class UserFriendlyException : AbpException, IHasLogSeverity, IHasErrorCode { /// <summary> /// Additional information about the exception. /// </summary> public string Details { get; private set; } /// <summary> /// An arbitrary error code. /// </summary> public int Code { get; set; } /// <summary> /// Severity of the exception. /// Default: Warn. /// </summary> public LogSeverity Severity { get; set; } /// <summary> /// Constructor. /// </summary> public UserFriendlyException() { Severity = LogSeverity.Warn; } /// <summary> /// Constructor for serializing. /// </summary> public UserFriendlyException(SerializationInfo serializationInfo, StreamingContext context) : base(serializationInfo, context) { } /// <summary> /// Constructor. /// </summary> /// <param name="message">Exception message</param> public UserFriendlyException(string message) : base(message) { Severity = LogSeverity.Warn; } /// <summary> /// Constructor. /// </summary> /// <param name="message">Exception message</param> /// <param name="severity">Exception severity</param> public UserFriendlyException(string message, LogSeverity severity) : base(message) { Severity = severity; } /// <summary> /// Constructor. /// </summary> /// <param name="code">Error code</param> /// <param name="message">Exception message</param> public UserFriendlyException(int code, string message) : this(message) { Code = code; } /// <summary> /// Constructor. /// </summary> /// <param name="message">Exception message</param> /// <param name="details">Additional information about the exception</param> public UserFriendlyException(string message, string details) : this(message) { Details = details; } /// <summary> /// Constructor. /// </summary> /// <param name="code">Error code</param> /// <param name="message">Exception message</param> /// <param name="details">Additional information about the exception</param> public UserFriendlyException(int code, string message, string details) : this(message, details) { Code = code; } /// <summary> /// Constructor. /// </summary> /// <param name="message">Exception message</param> /// <param name="innerException">Inner exception</param> public UserFriendlyException(string message, Exception innerException) : base(message, innerException) { Severity = LogSeverity.Warn; } /// <summary> /// Constructor. /// </summary> /// <param name="message">Exception message</param> /// <param name="details">Additional information about the exception</param> /// <param name="innerException">Inner exception</param> public UserFriendlyException(string message, string details, Exception innerException) : this(message, innerException) { Details = details; } }View Code
日誌(Logging):由上述事例可見,可以通過在基類定義的Logger物件來寫日誌。ABP預設使用了Log4Net,但它是可更改和可配置的。
部分原始碼分析:Log4NetLoggerFactory類。
public class Log4NetLoggerFactory : AbstractLoggerFactory { internal const string DefaultConfigFileName = "log4net.config"; private readonly ILoggerRepository _loggerRepository; public Log4NetLoggerFactory() : this(DefaultConfigFileName) { } public Log4NetLoggerFactory(string configFileName) { _loggerRepository = LogManager.CreateRepository( typeof(Log4NetLoggerFactory).GetAssembly(), typeof(log4net.Repository.Hierarchy.Hierarchy) ); var log4NetConfig = new XmlDocument(); log4NetConfig.Load(File.OpenRead(configFileName)); XmlConfigurator.Configure(_loggerRepository, log4NetConfig["log4net"]); } public override ILogger Create(string name) { if (name == null) { throw new ArgumentNullException(nameof(name)); } return new Log4NetLogger(LogManager.GetLogger(_loggerRepository.Name, name), this); } public override ILogger Create(string name, LoggerLevel level) { throw new NotSupportedException("Logger levels cannot be set at runtime. Please review your configuration file."); } }View Code
本地化(Localization):注意,在上述事例中使用了L("XXX")方法處理丟擲的異常。因此,它會基於當前使用者的文化自動實現本地化。詳細見後續本地化章節。
部分原始碼分析:......
自動對映(Auto Mapping):在上述事例最後一行程式碼,使用了ABP的MapTo擴充套件方法將輸入物件的屬性對映到實體屬性。ABP使用AutoMapper第三方庫執行對映。根據命名慣例可以很容易的將屬性從一個物件對映到另一個物件。
部分原始碼分析:AutoMapExtensions類中的MapTo()方法。
public static class AutoMapExtensions { public static TDestination MapTo<TDestination>(this object source) { return Mapper.Map<TDestination>(source); } public static TDestination MapTo<TSource, TDestination>(this TSource source, TDestination destination) { return Mapper.Map(source, destination); } ...... }View Code
動態API層(Dynamic API Layer):在上述事例中,TaskAppService實際上是一個簡單的類。通常必須編寫一個Web API Controller包裝器給js客戶端暴露方法,而ABP會在執行時自動完成。通過這種方式,可以在客戶端直接使用應用服務方法。
部分原始碼分析:......
動態javascript ajax代理(Dynamic JavaScript AJAX Proxy):ABP建立動態代理方法,從而使得呼叫應用服務方法就像呼叫客戶端的js方法一樣簡單。
部分原始碼分析:......
4.本章小節
通過上述簡單的類可以看到ABP的優點。完成所有這些任務通常需要花費大量的時間,但是ABP框架會自動處理。
除了這個上述簡單的事例外,ABP還提供了一個健壯的基礎設施和開發模型,如模組化、多租戶、快取、後臺工作、資料過濾、設定管理、領域事件、單元&整合測試等等,那麼你可以專注於業務程式碼,而不需要重複做這些工作(DRY)。