【ABP框架系列学习】介绍篇(1)

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 原文:【ABP框架系列学习】介绍篇(1)  0.引言 该系列博文主要在【官方文档】及【tkbSimplest】ABP框架理论研究系列博文的基础上进行总结的,或许大家会质问,别人都已经翻译过了,这不是多此一举吗?原因如下: 1.【tkbSimplest】的相关博文由于撰写得比较早的,在参照官方文档学习的过程中,发现部分知识未能及时同步(当前V4.0.2版本),如【EntityHistory】、【Multi-Lingual Engities】章节未涉及、【Caching】章节没有Entity Caching等内容。
原文: 【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)

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
相关文章
|
6月前
|
缓存 安全 PHP
【PHP开发专栏】Symfony框架核心组件解析
【4月更文挑战第30天】本文介绍了Symfony框架,一个模块化且高性能的PHP框架,以其可扩展性和灵活性备受开发者青睐。文章分为三部分,首先概述了Symfony的历史、特点和版本。接着,详细解析了HttpFoundation(处理HTTP请求和响应)、Routing(映射HTTP请求到控制器)、DependencyInjection(管理依赖关系)、EventDispatcher(实现事件驱动编程)以及Security(处理安全和认证)等核心组件。
138 3
|
开发框架 小程序 JavaScript
基于mpvue框架的小程序项目搭建入门教程一
基于mpvue框架的小程序项目搭建入门教程一
148 0
|
4月前
|
开发框架 前端开发 JavaScript
ABP开发框架前后端开发系列---(16)ABP框架升级最新版本的经验总结
ABP开发框架前后端开发系列---(16)ABP框架升级最新版本的经验总结
|
4月前
|
设计模式 前端开发 PHP
PHP框架详解 - CodeIgniter 框架
PHP框架详解 - CodeIgniter 框架
|
SQL 开发框架 缓存
C# Abp框架入门系列文章(一)(上)
C# Abp框架入门系列文章(一)
380 0
|
前端开发 API 数据库
C# Abp框架入门系列文章(一)(下)
C# Abp框架入门系列文章(一)(下)
450 0
|
开发框架 JSON 前端开发
浅入ABP(2):添加基础集成服务
浅入ABP(2):添加基础集成服务
669 0
浅入ABP(2):添加基础集成服务
|
缓存 .NET 开发框架
【ABP框架系列学习】N层架构(3)
原文:【ABP框架系列学习】N层架构(3) 目录 0.引言 1.DDD分层 2.ABP应用构架模型 客户端应用程序(Client Applications) 表现层(Presentation Layer) 分布式服务层(Distributed Service Layer) 应用层(Application Layer) 领域层 基础设施层 3.使用ABP项目模版快速生成应用程序 0.引言 应用程序的分层是一种广泛接受的技术, 可以降低复杂度和提高代码的可重用性。
1731 0