【ABP框架系列學習】介紹篇(1)

 

0.引言

該系列博文主要在【官方文檔】及【tkbSimplest】ABP框架理論研究系列博文的基礎上進行總結的,或許你們會質問,別人都已經翻譯過了,這不是畫蛇添足嗎?緣由以下:javascript

1.【tkbSimplest】的相關博文因爲撰寫得比較早的,在參照官方文檔學習的過程當中,發現部分知識未能及時同步(當前V4.0.2版本),如【EntityHistory】、【Multi-Lingual Engities】章節未涉及、【Caching】章節沒有Entity Caching等內容。html

2.進一步深刻學習ABP的理論知識。java

3.藉此機會提升英文文檔的閱讀能力,故根據官方當前最新的版本,並在前人的基礎上,本身也感覺一下英文幫助文檔的魅力。git

好了,下面開始進入正題。github

1.APB是什麼?

ABP是ASP.NET Boilerplate的簡稱,從英文字面上理解它是一個關於ASP.NET的模板,在github上已經有5.7k的star(截止2018年11月21日)。官方的解釋:ABP是一個開源且文檔友好的應用程序框架。ABP不只僅是一個框架,它還提供了一個最徍實踐的基於領域驅動設計(DDD)的體系結構模型。ajax

ABP與最新的ASP.NET COREEF CORE版本保持同步,一樣也支持ASP.NET MVC 5.x和EF6.x。json

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的一些優勢:app

依賴注入(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)

相關文章
相關標籤/搜索