为了了解应用程序是如何配置和初始化,本文将探讨ASP.NET Core和ABP框架最基本的构建模块。我们将从 ASP.NET Core 的 Startup类开始了解为什么我们需要模块化系统,以及 ABP 如何提供模块化方式来配置和初始化应用程序。然后我们将探索 ASP.NET Core 的依赖注入,以及ABP是如何使用预定义规则(predefined rules)自动进行依赖注入。最后,我们将了解 ASP.NET Core 的配置和选项框架,以及其他类库。

以下是本文的所有主题:

  • 了解模块化
  • 使用依赖注入系统
  • 配置应用程序
  • 实现选项模式
  • 日志系统

一、了解模块化

模块化是一种将大型软件按功能分解为更小的部分,并允许每个部分通过标准化接口进行通信。模块化有以下主要好处:

  • 模块按规则进行隔离后,大大降低了系统复杂性。
  • 模块之间松散耦合,提供了更大的灵活性。因为模块是可组装、可替换的。
  • 因为模块是独立的,所以它允许跨应用被重用。

大多数企业的软件被设计成模块化,但是,实现模块化并不容易。ABP 框架的主要目标之一是为模块化提供基础设施和工具。我们将在后面详细介绍模块化开发,本节只介绍 ABP 模块的基础知识。

Startup 类

在定义ABP的模块之前,建议先熟悉 ASP.NET Core 中的StartUp类,我们看下ASP.NET Core 的Startup类:

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc();
        services.AddTransient<MyService>();
    }
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        app.UseRouting();
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });
    }
}

ConfigureServices方法用于配置服务并将新服务注册到依赖注入系统。另一方面,Configure方法用于配置 ASP.NET Core 管道中间件,用于处理 HTTP 请求。

在应用程序启动之前,我们需要在Program.cs中配置Startup类:

public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }
    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args).ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            });
}

这个Startup类是独一无二的,我们只有一个点来配置和初始化所有的服务。但是,在模块化应用程序中,我们希望每个模块都能独立配置和初始化与该模块相关的服务。此外,一个模块通常需要使用或依赖于其他模块,因此模块配置顺序和初始化就非常重要了。我们来看下 ABP 的模块是如何定义的

模块定义

ABP 模块是一组类型(比如类或接口),它们一同开发一同交付的。它是一个程序集(一般来说是Visual Studio 中的一个项目),派生自AbpModule,模块类负责配置和初始化,并在必要时配置依赖模块。

下面是一个短信发送模块的简单定义:

using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.Modularity;
namespace SmsSending
{
    public class SmsSendingModule : AbpModule
    {
        public override void ConfigureServices(
ServiceConfigurationContext context)
        {
            context.Services.AddTransient<SmsService>();
        }
    }
}

每个模块都可以重写ConfigureServices方法,以便将其服务注册到依赖注入系统。此示例中的SmsService服务被注册为瞬态生命周期。该示例和上面Startup类似。但是,大多时候,您不需要手动注册服务,这要归功ABP 框架的按约定注册系统。

OnApplicationInitialization方法用在服务注册完成后,并且在应用准备就绪后执行。使用此方法,您可以在应用启动时执行任何操作。例如,您可以初始化一个服务:

public class SmsSendingModule : AbpModule
{
    //...
    public override void OnApplicationInitialization(ApplicationInitializationContext context)
    {
        var service = context.ServiceProvider.GetRequiredService<SmsService>();
        service.Initialize();
    }
}

这里,我们使用context.ServiceProvider从依赖注入系统请求并初始化服务。可见,此时服务已经完成注册。

您也可以将OnApplicationInitialization方法等同于Startup类的Configure方法。

您可以在此处构建 ASP.NET Core 请求管道。但是,通常我们会在启动模块中配置请求管道,如下一节所述。

模块依赖和启动模块

一个业务应用通常由多个模块组成,ABP 框架允许您声明模块之间的依赖关系。一个应用必须要有一个启动模块。启动模块可以依赖于其他模块,其他模块可以再依赖于其他模块,以此类推。

下图是一个简单的模块依赖关系图:

如果所示,如果模块 A 依赖于模块 B,则模块 B 总是在模块 A 之前初始化。这允许模块 A 使用、设置、更改或覆盖模块 B 定义的配置和服务。

对于示例图,模块初始化的顺序应该是:G、F、E、D、B、C、A。

您不必知道确切的初始化顺序;只需要知道如果你的模块依赖于模块xx,那么模块xx在你的模块之前被初始化。

ABP使用[DependsOn](属性声明)方式来定义模块依赖:

[DependsOn(typeof(ModuleB), typeof(ModuleC))]
public class ModuleA : AbpModule
{    
}

这里,ModuleA通过[DependsOn]依赖于ModuleBModuleC

本例中,启动模块ModuleA负责设置ASP.NET Core 的请求管道:

[DependsOn(typeof(ModuleB), typeof(ModuleC))]
public class ModuleA : AbpModule
{
    //...
    public override void OnApplicationInitialization(ApplicationInitializationContext context)
    {
        var app = context.GetApplicationBuilder();
        var env = context.GetEnvironment();
        
        app.UseRouting();
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });
    }
}

[代码块和之前ASP.NET Core的 Startup类 创建请求管道相同。context.GetApplicationBuilder()context.GetEnvironment()用于从依赖注入中获IApplicationBuilderIWebHostEnvironment服务。

最后,我们在Startup里将ASP.NET Core 和 ABP 框架进行集成:

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddApplication<ModuleA>();
    }
    public void Configure(IApplicationBuilder app)
    {
        app.InitializeApplication();
    }
}

services.AddApplication()方法由 ABP 框架定义,用于ABP的模块配置。它按顺序执行了所有模块的ConfigureServices方法。而app.InitializeApplication()方法也是由 ABP 框架定义,它也是按照模块依赖的顺序来执行所有模块的OnApplicationInitialization方法。

ConfigureServicesOnApplicationInitialization方法是模块类中最常用的方法。

模块生命周期

AbpModule中定义的生命周期方法,除了上面看到的ConfigureServicesOnApplicationInitialization,下面罗列其他生命周期相关方法:

  • PreConfigureServices: 这个方法在ConfigureServices方法之前被调用。它允许您配置服务之前执行的代码。
  • ConfigureServices:这是配置模块和注册服务的主要方法。
  • PostConfigureServices: 该方法在ConfigureServices之后调用(包括依赖于您模块的模块),这里可以配置服务后执行的代码。
  • OnPreApplicationInitialization: 这个方法在OnApplicationInitialization之前被调用。在这个阶段,您可以从依赖注入中解析服务,因为服务已经被初始化。
  • OnApplicationInitialization:此方法用来配置 ASP.NET Core 请求管道并初始化您的服务。
  • OnPostApplicationInitialization: 这个方法在初始化阶段后被调用。
  • OnApplicationShutdown:您可以根据需要自己实现模块的关闭逻辑。

    Pre…Post…前缀的方法与原始方法具有相同的目的。它们提供了一种在模块之前或之后执行的一些配置/初始化代码,一般情况下我们很少使用到。

异步生命周期方法

本节介绍的生命周期方法是同步的。在编写本书时,ABP 框架团队正努力在 框架 5.1 版本引入异步生命周期方法。

如前所述,模块类主要包含注册和配置与该模块相关的服务的代码。在下一节中,我们将介绍如何使用 ABP 框架注册服务。

二、使用依赖注入系统

.NET 原生依赖注入

依赖注入是一种获取类的依赖的技术,它将创建类与使用该类分开。

假设我们有一个UserRegistrationService类,它调用SmsService类来发送验证短信,如下:

public class UserRegistrationService
{
    private readonly SmsService _smsService;
    public UserRegistrationService(SmsService smsService)
    {
        _smsService = smsService;
    }
    public async Task RegisterAsync(
        string username,
        string password,
        string phoneNumber)
    {
        //...save user in the database
        await _smsService.SendAsync(
            phoneNumber,
            "Your verification code: 1234"
        );
    }
}

这里的SmsService使用构造函数注入来获取实例。也就是说,依赖注入系统会自动帮我们实例化类的依赖项,并将它们赋值给我们的_smsService。

注意:ABP采用的是ASP.NET Core原生的依赖注入框架,他自己并没有发明依赖注入框架。

在设计服务时,我们还要考虑另外一件重要的事情:服务生命周期。

ASP.NET Core 为服务注册提供了三个生命周期选项:

  • Transient(瞬态):每次您请求/注入服务时,都会创建一个新实例。
  • Scoped(范围): 通常这由请求生命周期来评估,您只有在同一范围内才能共享相同的实例。
  • Singleton(单例):在应用内有且仅有一个实例。所有请求都使用相同的实例。该对象在第一次请求创建。

    以下模块注册了两个服务,一个是瞬态的,另一个是单例的:
public class MyModule : AbpModule
{
    public override void ConfigureServices(ServiceConfigurationContext context)
    {
        context.Services.AddTransient<ISmsService, SmsService>();
        context.Services.AddSingleton<OtherService>();
    }
}

context.Services的类型是IServiceCollection,它是一个扩展方法。

在第一个示例中使用接口注册,第二个示例使用引用类注册为单例。

ABP的依赖注入

使用 ABP 框架时,您不必考虑服务注册,这要归功于 ABP 框架独特的服务注册系统。

1.约定式注册

在 ASP.NET Core 中,所有服务需要显式注册到IServiceCollection,如上一节所示。这些注册大多重复,完全可以自动化操作。

ABP 对于以下类型采用自动注册:

  • MVC controllers
  • Razor page models
  • View components
  • Razor components
  • SignalR hubs
  • Application services
  • Domain services
  • Repositories

    以上类型均使用瞬态生命周期自动注册。如果您还有别的类型,可以考虑接口注册。

2.接口注册

您可以实现以下三种接口来注册:

  • ITransientDependency
  • IScopedDependency
  • ISingletonDependency

例如,在下面代码块中,我们将服务注册为单例:

public class UserPermissionCache : ISingletonDependency
{ }

接口注册很容易并且是推荐的方式,但与下面的属性注册相比,它有一定的局限性。

3.属性注册

属性注册更精细,下面是和属性注册相关的配置参数

  • Lifetime(enum): 服务的生命周期,包括Singleton,TransientScoped
  • TryRegister(bool):仅当服务尚未注册时才注册
  • ReplaceServices(bool):如果服务已经注册,则替换之前的注册

示例代码:

using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.DependencyInjection;
namespace UserManagement
{
    [Dependency(ServiceLifetime.Transient, TryRegister = true)]
    public class UserPermissionCache
    { }
}

4.接口属性混合注册

属性接口一起使用。如果属性定义了属性,属性比接口优先级更高。

如果一个类可能被注入不同的类或接口,具体取决于暴露的类型。

暴露服务

当一个类没有实现接口时,只能通过类引用注入。上一节中的UserPermissionCache类就是通过注入类引用来使用的。

假设我们有一个抽象 SMS 发送的接口:

public interface ISmsService
{
    Task SendAsync(string phoneNumber, string message);
}

假设您要ISmsService实现 Azure 服务:

public class AzureSmsService : ISmsService, ITransientDependency
{
    public async Task SendAsync(string phoneNumber, string message)
    {
        //TODO: ...
    }
}

这里的AzureSmsService实现了ISmsServiceITransientDependency两个接口。而ITransientDependency接口才是用于自动注册到依赖注入中的。这里的注入主要通过命名约定来实现,因为AzureSmsServiceSmsService作为后缀结尾。

我们再举一个通过命名约定的例子,假设我们有一个实现多个接口的类:

public class PdfExporter: IExporter, IPdfExporter, ICanExport, ITransientDependency
{ }

PdfExporter服务可以通过注入IPdfExporterIExporter接口来使用,也可以直接注入PdfExporter类引用来使用。但是,您不能使用ICanExport接口注入它,因为名称PdfExporter不以CanExport为后缀。

一旦您使用该ExposeServices属性来暴露服务,如以下代码块所示:

[ExposeServices(typeof(IPdfExporter))]
public class PdfExporter: IExporter, IPdfExporter, ICanExport, ITransientDependency
{ }

现在,您只能通过注入IPdfExporter接口来使用PdfExporter类。

我应该为每个服务定义接口吗?

ABP 不会强迫你这么做,但是通用接口来定义是最佳实践:如果你想松散地耦合你的服务。比如,在单元测试中可以轻松模拟测试数据。

这就是为什么我们将接口与实现物理分离(例如,我们在项目中定义Application.Contracts接口,并在Application项目中实现它们,或者在领域层中定义存储库接口,在基础设施层中实现它们)。

我们已经了解了如何注册和消费服务。另外,某些服务具有选项配置,您需要在使用它们之前对其进行配置。接下来的两节将展开介绍。

待续

文章有点长,下篇将继续介绍ABP的配置和选项模式,感谢你的阅读。

探索ABP基础架构的更多相关文章

  1. 探索ABP基础架构-下

    配置应用程序 ASP.NET Core 的配置系统提供了一个基于键值对的配置方法.它是一个可扩展的系统,可以从各种资源中读取键值对,例如 JSON 设置文件.环境变量.命令行参数等等. 设置配置值 默 ...

  2. ABP框架之——数据访问基础架构

    大家好,我是张飞洪,感谢您的阅读,我会不定期和你分享阅读心得,希望我的文章能成为你成长路上的一块垫脚石,我们一起精进. 几乎所有的业务应用程序都要适用一种数据库基础架构,用来实现数据访问逻辑,以便从数 ...

  3. 一、项目基础架构(附GitHub地址)——以ABP为基础架构的一个中等规模的OA开发日志

    前言: 最近园子里ABP炒的火热.看了几篇对于ABP的介绍后,深感其设计精巧,实现优雅.个人感觉,ABP或ABP衍生品的架构设计,未来会成为中型Net项目的首选架构模式.如果您还不了解ABP是什么,有 ...

  4. ABP(现代ASP.NET样板开发框架)系列之3、ABP分层架构

    点这里进入ABP系列文章总目录 基于DDD的现代ASP.NET开发框架--ABP系列之3.ABP分层架构 ABP是“ASP.NET Boilerplate Project (ASP.NET样板项目)” ...

  5. MVP社区巡讲-云端基础架构:12月5日北京站 12月12日上海站

    紧跟当今的技术发展趋势还远远不够,我们要引领变革!加入本地技术专家社区,获取真实案例.实况培训演示以及探讨新一代解决方案.在此活动中,您将: 了解如何运用开源(OSS)技术.Microsoft 技术及 ...

  6. 云计算服务模型,第 1 部分: 基础架构即服务(IaaS)

    英文原文:Cloud computing service models, Part 1: Infrastructure as a Service 本文介绍三个云类别中的第一个:基础架构即服务(infr ...

  7. 基于DDD的现代ASP.NET开发框架--ABP系列之3、ABP分层架构

    基于DDD的现代ASP.NET开发框架--ABP系列之3.ABP分层架构 ABP是“ASP.NET Boilerplate Project (ASP.NET样板项目)”的简称. ABP的官方网站:ht ...

  8. 如何使用 Docker、ECS、Terraform 重建基础架构?

    早期 Segment 基础架构普遍组合在一起.我们通过 AWS 界面设定实例,使用许多闲散的 AMI,并且采用三种不同的部署方式. 然而随着商业的飞速发展,工程师团队的规模不断扩大,基础架构的复杂度也 ...

  9. ABP分层架构

    ABP分层架构 基于DDD的现代ASP.NET开发框架--ABP系列之3.ABP分层架构 ABP是“ASP.NET Boilerplate Project (ASP.NET样板项目)”的简称. ABP ...

随机推荐

  1. 处理器映射器(HandlerMapping)及处理器适配器(HandlerAdapter)详解(二)

    注解的 处理器映射器 和 处理器适配器 介绍 注解的映射器: 在 Spring3.1 之前使用 DefaultAnnotationHandlerMapping 注解映射器(根据 DispatcherS ...

  2. java弹框

    Java弹窗操作 1.任务简介 本博客介绍两种Java弹窗操作的方法,第一个种是通过弹出对话框显示用户输入的信息,第二种是通过用户输入数字的不同打开不同的程序. 2.弹出对话框的操作 1)任务内容编程 ...

  3. PCB中加入任意LOGO图文说明 精心制作

    防静电图 首先我们要对下载下来的图片进行处理否则Altium designer6.9会提示装载的图片不是单色的,用Photoshop CS打开开始下载的图片 选择 图像→模式→灰度 在选择 图像→模式 ...

  4. 在微信小程序中绘制图表(part2)

    本期大纲 1.确定纵坐标的范围并绘制 2.根据真实数据绘制折线 相关阅读:在微信小程序中绘制图表(part1)在微信小程序中绘制图表(part3) 关注我的 github 项目 查看完整代码. 确定纵 ...

  5. 安卓性能测试之Lint测试

    pre { direction: ltr; color: rgba(0, 0, 10, 1); text-align: left } pre.western { font-family: " ...

  6. AMS分析 -- 启动过程

    一. AMS简介 AmS可以说是Android上层系统最核心的模块之一,其主要完成管理应用进程的生命周期以及进程的Activity,Service,Broadcast和Provider等. 从系统运行 ...

  7. Android 预置APK

    1.   预置apk,使其不可卸载   第一步:      在 "/vendor/huawei/packages/apps" 目录下创建一个对应名称的文件夹.   第二步:   将 ...

  8. spring security简介与使用

    目录 spring security 新建一个springboot项目 添加spring security 登录 使用默认用户和随机生成的密码登录 使用yaml文件定义的用户名.密码登录 使用代码中指 ...

  9. 升级DLL plugin 到AutoDllPlugin

    为了使打包构建速度加快使用的DLLPlugin,但是我们还是需要手动把dll文件引入文件, HTMLwebpackplugin 结合autoDLLplugin可以自动引入打包文件, 十份地方便

  10. 【代码大全2 学习笔记】ADT 抽象与封装

    ADT abstract data type 抽象数据类型 要理解面向对象编程,就要先理解ADT这个概念.不懂ADT的程序员开发出来的类只是名义上的"类"而已--只是单纯的把一些相 ...