0.引言
該系列博文主要在【
官方文檔】及【
tkbSimplest】ABP架構理論研究系列博文的基礎上進行總結的,或許大家會質問,别人都已經翻譯過了,這不是多此一舉嗎?原因如下:
1.【
】的相關博文由于撰寫得比較早的,在參照官方文檔學習的過程中,發現部分知識未能及時同步(目前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具有哪些優點:
public class 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()
);
}
倉儲(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;
}
}
通過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);
}
驗證(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)));
}
}
}
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();
}
}
自定義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;
}
}
審計日志(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)));
}
};
}
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;
}
}
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);
}
}
工作單元(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()
);
}
}
}
異常處理(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;
}
}
日志(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.");
}
}
本地化(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);
}
......
}
動态API層(Dynamic API Layer):在上述事例中,TaskAppService實際上是一個簡單的類。通常必須編寫一個Web API Controller包裝器給js用戶端暴露方法,而ABP會在運作時自動完成。通過這種方式,可以在用戶端直接使用應用服務方法。
動态javascript ajax代理(Dynamic JavaScript AJAX Proxy):ABP建立動态代理方法,進而使得調用應用服務方法就像調用用戶端的js方法一樣簡單。
4.本章小節
通過上述簡單的類可以看到ABP的優點。完成所有這些任務通常需要花費大量的時間,但是ABP架構會自動處理。
除了這個上述簡單的事例外,ABP還提供了一個健壯的基礎設施和開發模型,如子產品化、多租戶、緩存、背景工作、資料過濾、設定管理、領域事件、單元&內建測試等等,那麼你可以專注于業務代碼,而不需要重複做這些工作(DRY)。