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具有哪些优点:
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() ); }
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)。