Explore ABP Infrastructure

To understand how an application is configured and initialized, this article will explore the most basic building blocks of ASP.NET Core and the ABP framework. We'll start with ASP.NET Core's Startup class to understand why we need a modular system and how ABP provides a modular way to configure and initialize an application. Then we'll explore ASP.NET Core's dependency injection and how ABP automates dependency injection using predefined rules. Finally, we'll look at ASP.NET Core's configuration and options framework, as well as other class libraries.

Here are all the topics in this article:

  • Learn about modularity
  • Use a dependency injection system
  • Configure the application
  • Implement options pattern
  • log system

1. Understanding Modularity

Modularity is the function of breaking down large software into smaller parts and allowing each part to communicate through standardized interfaces. Modularity has the following main benefits:

  • After the modules are isolated according to the rules, the system complexity is greatly reduced.
  • Modules are loosely coupled, providing greater flexibility. Because modules are assembleable and replaceable.
  • Because the module is self-contained, it allows reuse across applications.

Most enterprise software is designed to be modular, but achieving modularity is not easy. One of the main goals of the ABP framework is to provide the infrastructure and tools for modularity. We will introduce modular development in detail later, this section only introduces the basics of ABP modules.

Startup class

Before defining the module of ABP, it is recommended to be familiar with the StartUp class in ASP.NET Core. Let's take a look at the Startup class of ASP.NET Core:

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();
        });
    }
}

The ConfigureServices method is used to configure services and register new services with the dependency injection system. On the other hand, the Configure method is used to configure the ASP.NET Core pipeline middleware for handling HTTP requests.
Before the application starts, we need to configure the Startup class in Program.cs:

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>();
            });
}

This Startup class is unique, we have only one point to configure and initialize all services. However, in a modular application, we want each module to independently configure and initialize the services associated with that module. In addition, a module often needs to use or depend on other modules, so the module configuration order and initialization are very important. Let's take a look at how ABP's modules are defined

module definition

An ABP module is a set of types (such as classes or interfaces) that were developed and delivered together. It's an assembly (generally a project in Visual Studio) that derives from AbpModule, and the module class is responsible for configuration and initialization, and configuration of dependent modules if necessary.

The following is a simple definition of a SMS sending module:

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

Each module can override the ConfigureServices method in order to register its services with the dependency injection system. The SmsService service in this example is registered with a transient lifecycle. This example is similar to the Startup above. However, most of the time, you don't need to register services manually, thanks to the ABP framework's registration system by convention.

The OnApplicationInitialization method is used after the service is registered and executed after the application is ready. With this method, you can do anything when the app starts. For example, you can initialize a service:

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

Here, we use context.ServiceProvider to request and initialize the service from the dependency injection system. It can be seen that the service has been registered at this time.

You can also equate the OnApplicationInitialization method with the Configure method of the Startup class.

You can build your ASP.NET Core request pipeline here. However, typically we configure the request pipeline in the startup module, as described in the next section.

Module dependencies and startup modules

A business application usually consists of multiple modules, and the ABP framework allows you to declare dependencies between modules. An application must have a startup module. Startup modules can depend on other modules, other modules can depend on other modules, and so on.

The following figure is a simple module dependency diagram:

If shown, if module A depends on module B, then module B is always initialized before module A. This allows module A to use, set, change or override the configurations and services defined by module B.

For the example diagram, the order of module initialization should be: G, F, E, D, B, C, A.

You don't have to know the exact initialization order; just know that if your module depends on module xx, then module xx is initialized before your module.

ABP uses the [DependsOn] (property declaration) method to define module dependencies:

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

Here, ModuleA depends on ModuleB and ModuleC through [DependsOn].
In this example, start the module ModuleA Responsible for setting up ASP.NET Core's request pipeline:

[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();
        });
    }
}

[The code block is the same as the previous ASP.NET Core Startup class to create the request pipeline. context.GetApplicationBuilder() and context.GetEnvironment() are used to obtain IApplicationBuilder and IWebHostEnvironment services from dependency injection.

Finally, we integrate ASP.NET Core and ABP framework in Startup:

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

The services.AddApplication() method is defined by the ABP framework for module configuration of ABP. It executes the ConfigureServices methods of all modules in order. The app.InitializeApplication() method is also defined by the ABP framework, and it also executes the OnApplicationInitialization method of all modules in the order of module dependencies.

The ConfigureServices and OnApplicationInitialization methods are the most commonly used methods in module classes.

module life cycle

The life cycle methods defined in AbpModule, in addition to ConfigureServices and OnApplicationInitialization seen above, are listed below for other life cycle related methods:

  • PreConfigureServices: This method is called before the ConfigureServices method. It allows you to configure the code to be executed before the service.
  • ConfigureServices: This is the main method for configuring modules and registering services.
  • PostConfigureServices: This method is called after ConfigureServices (including modules that depend on your module), here you can configure the code executed after the service.
  • OnPreApplicationInitialization: This method is called before OnApplicationInitialization. At this stage, you can resolve the service from dependency injection because the service has already been initialized.
  • OnApplicationInitialization: This method is used to configure the ASP.NET Core request pipeline and initialize your service.
  • OnPostApplicationInitialization: This method is called after the initialization phase.
  • OnApplicationShutdown: You can implement the shutdown logic of the module yourself according to your needs.
    Methods prefixed with Pre... and Post... have the same purpose as the original method. They provide a kind of configuration/initialization code that is executed before or after the module, which we rarely use in general.

Asynchronous lifecycle methods

The lifecycle methods described in this section are synchronous. At the time of this writing, the ABP Framework team is working to introduce asynchronous lifecycle methods in version 5.1 of the framework.

As mentioned earlier, the module class mainly contains the code to register and configure the services related to the module. In the next section, we will describe how to register services using the ABP framework.

2. Use Dependency Injection System

.NET native dependency injection

Dependency injection is a technique to obtain the dependencies of a class, which separates the creation of the class from the use of the class.

Suppose we have a UserRegistrationService class that calls the SmsService class to send verification SMS messages as follows:

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"
        );
    }
}

Here the SmsService uses constructor injection to get the instance. That is, the dependency injection system will automatically instantiate the class dependencies for us and assign them to our _smsService.

Note: ABP uses the native dependency injection framework of ASP.NET Core, and he did not invent the dependency injection framework himself.

There is another important thing to consider when designing a service: the service life cycle.
ASP.NET Core provides three lifecycle options for service registration:

  • Transient: Every time you request/inject the service, a new instance is created.
  • Scoped: Usually this is evaluated by the request lifecycle, you can only share the same instance within the same scope.
  • Singleton (singleton): There is only one instance in the application. All requests use the same instance. The object is created on the first request.
    The following module registers two services, one transient and one singleton:
public class MyModule : AbpModule
{
    public override void ConfigureServices(ServiceConfigurationContext context)
    {
        context.Services.AddTransient<ISmsService, SmsService>();
        context.Services.AddSingleton<OtherService>();
    }
}

The type of context.Services is IServiceCollection, which is an extension method.

In the first example it is registered with an interface, and the second example is registered as a singleton with a reference class.

Dependency Injection of ABP

When using ABP Framework, you don't have to think about service registration, thanks to ABP Framework's unique service registration system.

1. Conventional registration

In ASP.NET Core, all services need to be explicitly registered with the IServiceCollection, as shown in the previous section. Most of these registrations are repetitive and can be fully automated.

ABP employs automatic registration for the following types:

  • MVC controllers
  • Razor page models
  • View components
  • Razor components
  • SignalR hubs
  • Application services
  • Domain services
  • Repositories
    All of the above types are automatically registered with a transient lifecycle. If you have other types, consider interface registration.

2. Interface registration

You can implement the following three interfaces to register:

  • ITransientDependency
  • IScopedDependency
  • ISingletonDependency

For example, in the following code block, we register the service as a singleton:

public class UserPermissionCache : ISingletonDependency
{ }

Interface registration is easy and the recommended way, but it has some limitations compared to property registration below.

3. Property registration

Attribute registration is more refined, the following are configuration parameters related to attribute registration

  • Lifetime(enum): The life cycle of the service, including Singleton,Transient and Scoped
  • TryRegister(bool): Register only if the service is not already registered
  • ReplaceServices(bool): If the service is already registered, replace the previous registration

Sample code:

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

4. Interface property mixed registration

used with the property interface. If an attribute defines an attribute, the attribute takes precedence over the interface.

If a class may be injected into a different class or interface, depending on the type exposed.

Expose service

When a class does not implement an interface, it can only be injected by class reference. The UserPermissionCache class in the previous section is used by injecting a class reference.

Suppose we have an interface that abstracts SMS sending:

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

Suppose you want ISmsService to implement an Azure service:

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

The AzureSmsService here implements two interfaces, ISmsService and ITransientDependency. The ITransientDependency interface is used to automatically register with dependency injection. The injection here is mostly done through naming conventions, as AzureSmsService ends with SmsService as a suffix.
Let's take another example by naming conventions, suppose we have a class that implements multiple interfaces:

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

The PdfExporter service can be used by injecting the IPdfExporter and IExporter interfaces, or by directly injecting the PdfExporter class reference. However, you cannot inject it using the ICanExport interface because the name PdfExporter is not suffixed with CanExport.

Once you use the ExposeServices property to expose the service, as shown in the following code block:

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

Now, you can only use the PdfExporter class by injecting the IPdfExporter interface.

Should I define an interface for each service?

ABP doesn't force you to do this, but defining generic interfaces is best practice: if you want to loosely couple your services. For example, test data can be easily mocked in unit tests.

This is why we physically separate the interface from the implementation (for example, we define Application.Contracts interfaces in our project and implement them in the Application project, or we define repository interfaces in the domain layer and implement them in the infrastructure layer).

We've seen how to register and consume services. Also, some services have options configuration that you need to configure before using them. The next two sections will expand on this.

to be continued

The article is a bit long, the next part will continue to introduce the configuration and option mode of ABP, thank you for reading.

Posted by kingnutter on Sun, 15 May 2022 07:21:57 +0300