• 【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 COREEF 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)

  • 相关阅读:
    常用语句
    html引入ECharts的两种方式
    最新版Navicate破解激活
    买路由器篇
    关于java8(Stream)的一些用法
    Mybatis联合查询记录,左连接参数操作
    海淘转运事宜记录
    mysql where语句多条件查询是and和or联合使用bug
    关于mysql中GROUP_CONCAT函数的使用
    关于MySQL存入的时间和取出时间不一致的解决
  • 原文地址:https://www.cnblogs.com/OlderGiser/p/9998692.html
Copyright © 2020-2023  润新知