core bootstrap modules - grecosoft/NetFusion GitHub Wiki

IMAGE Plugin Modules

This topic will discuss adding modules to plugins. A Plugin can have one or more registered Modules called during the bootstrap process. All code to be executed by a Plugin is contained within a Module. Multiple modules are used to organize a plugin's implementation allowing each module to implement a specific concern.

For example, the NetFusion.Integration.ServiceBus plugin contains the following two modules:

  • NamespaceModule: This module is responsible for loading the microservice's configuration and storing the connection metadata required to establish the connections when the module is started.
  • NamespaceEntityModule: This module is responsible for searching for all message routing classes. A routing class specifies the Service Bus entity to which commands and domain-events are delivered. Once the module is started, this data is used to create the needed queues, topics, and subscriptions.

The following will be discussed:

  • Declaring a Plugin Module class
  • Adding Modules to a Plugin
  • Overriding the Initialize and Configure methods
  • Accessing the PluginContext from within a module
  • Defining an Plugin Configuration and accessing it from within a module
  • Changing the default values of a Plugin Configuration when bootstrapping the host

Module Declaration

Modules are easy to create and add to a plugin. This section will add modules to the following Plugin projects:

Project Plugin Type Module Name
Examples.Bootstrapping.WebApi HostPlugin HostModuleOne
Examples.Bootstrapping.App AppPlugin AppModuleOne
Examples.Bootstrapping.CrossCut CorePlugin CoreModuleOne
CoreModuleTwo

HostPlugin Module

Add a module named HostModuleOne to the Examples.Bootstrapping.WebApi project within the following directory:

./src/Examples.Bootstrapping.WebApi/Plugin/Modules

using NetFusion.Core.Bootstrap.Plugins;

namespace Examples.Bootstrapping.WebApi.Plugin.Modules;

public class HostModuleOne : PluginModule
{
    
}

Then add the following line of code to the HostPlugin class contained in the parent directory:

using Examples.Bootstrapping.WebApi.Plugin.Modules;
using NetFusion.Core.Bootstrap.Plugins;

namespace Examples.Bootstrapping.WebApi.Plugin;

public class WebApiPlugin : PluginBase
{
    public const string HostId = "F30D55C9-D06D-481F-860F-8E9524BC73B8";
    public const string HostName = "examples-bootstrapping";

    public override PluginTypes PluginType => PluginTypes.HostPlugin;
    public override string PluginId => HostId;
    public override string Name => HostName;
        
    public WebApiPlugin()
    {
        AddModule<HostModuleOne>(); 	// <-- Add this line
        
        Description = "WebApi host exposing REST/HAL based Web API.";
    }
}

AppPlugin Module

Add a module named AppModuleOne to the Examples.Bootstrapping.App project within the following directory:

./src/Examples.Bootstrapping.App/Plugin/Modules

using NetFusion.Core.Bootstrap.Plugins;

namespace Examples.Bootstrapping.App.Plugin.Modules;

public class AppModuleOne : PluginModule
{
    
}

Then add the following line of code to the AppPlugin class contained in the parent directory:

using NetFusion.Core.Bootstrap.Plugins;
using Examples.Bootstrapping.App.Plugin.Modules;

namespace Examples.Bootstrapping.App.Plugin;

public class AppPlugin : PluginBase
{
    public override string PluginId => "FA2A1F82-471C-44D0-A56C-01A7163D71C2";
    public override PluginTypes PluginType => PluginTypes.AppPlugin;
    public override string Name => "Application Services Component";

    public AppPlugin()
    {
        AddModule<ServiceModule>();
        AddModule<AppModuleOne>();		// <-- Add this line

        Description = "Plugin component containing the Microservice's application services.";
    }   
}

CorePlugin Module

Add a module named CoreModuleOne to the Examples.Bootstrapping.CrossCut project within the following directory:

./src/Examples.Bootstrapping.CrossCut/Plugin/Modules

using NetFusion.Core.Bootstrap.Plugins;

namespace Examples.Bootstrapping.CrossCut.Plugin.Modules;

public class CoreModuleOne : PluginModule
{
    
}

Add a module named CoreModuleTwo to the Examples.Bootstrapping.CrossCut project within the following directory:

./src/Examples.Bootstrapping.CrossCut/Plugin/Modules

using NetFusion.Core.Bootstrap.Plugins;

namespace Examples.Bootstrapping.CrossCut.Plugin.Modules;

public class CoreModuleTwo : PluginModule
{
    
}

Then add the following two lines of code to the CorePlugin class contained in the parent directory:

using Examples.Bootstrapping.CrossCut.Plugin.Modules;
using NetFusion.Core.Bootstrap.Plugins;

namespace Examples.Bootstrapping.CrossCut.Plugin;

public class CrossCutPlugin : PluginBase
{
    public override string PluginId => "7332C55B-E64C-430C-8836-488787CAC875";
    public override PluginTypes PluginType => PluginTypes.CorePlugin;
    public override string Name => "Cross-Cut Component";

    public CrossCutPlugin()
    {
        AddModule<CoreModuleOne>();		// <-- Add this line
        AddModule<CoreModuleTwo>();		// <-- Add this lien
        
        Description = "Example of a core plugin";
    }   
}

Note the following:

  • Multiple modules have been declared within plugins that are of different types (HostPlugin, AppPlugin, and CorePlugin)
  • The plugin definitions are exactly the same regardless of the type of plugin
  • Future topics will discuss how the type of plugin a module is located impacts the bootstrap process
  • In real production code, the modules would be clearly named based on their responsibilities
  • Future topics explaining different bootstrapping concepts will add additional code to these create plugins

Module Logs

A dedicated topic will discuss how modules can add details to the log. However, running the example microservice at this point will show how each module declared above exists in the log. Before running the microservice, make sure that SEQ is running within Docker.

Execute the following to start the microservice:

cd ./src/Examples.Bootstrapping.WebApi/
dotnet run

IMAGE

Above shows the log entry associated with the Cross-Cut plugin expanded. Within the expanded log, there is an entry for each added module added to the plugin.

Initialization and Configuration

When each Plugin Module is bootstrapped, the base PluginModule class has several methods that can be overridden to execute code at specific points within the process. This section will discuss the Initialize and Configure methods and subsequent sections will discuss those pertaining to their specific topic. For example, the Service Registration topic will discuss the methods called where services can be added to the dependency-injection container.


IMAGEThe order in which the Modules are added to the Plug-in determines the order in which they have their methods called. This can be important for the Start and Stop methods. For example, a module responsible for establishing connections will often need to be started before another dependent module and would be added first. Also, when the Stop method is called, the order they are called is reversed. This way, dependent objects can first be disposed before their associated connection.


This section will simply add log messages to each of these methods. For each of the four modules, add the following code:

public override void Initialize()
{
    NfExtensions.Logger.Log<PluginModule>(
        LogLevel.Information, $"Initializing: {GetType().Name}");
}

public override void Configure()
{
    NfExtensions.Logger.Log<PluginModule>(
        LogLevel.Information, $"Configuring: {GetType().Name}");
}

The following shows the order in which these methods are called:

IMAGE

Initialization and Configuration Order

Plugin Modules are initialized and configured in the following order:

  • All modules are Initialized in order of their plugin's type: Core, Application, Host
  • All modules are Configured in order of their plugin's type: Core, Application, Host

Module Dependencies

Sometimes, the initialization and configuration order needs to be taken into account when designing PlugIns. If a module is dependent on another, it should access the state of the module on which it depends from within its Configure method override. Likewise, the state of the module on which other modules depend should be set within the Initialize method override.


IMAGECalls to external resources such as databases should not be preformed within Initialize or Configure base method overrides. External resources should be accessed asynchronously from within the module's OnStartModuleAsync method and will be discussed in an upcoming topic.

The purpose of the Initialize and Configure method overrides are to initialize and cache any state specified within code. For example, the NetFusion.Services.Mapping plugin allows mapping between to types of classes. Within the MappingModule of this plugin, all IMappingStartegy implementations are discovered and cached in a ILookup structure where the source type is the key. This allows the possible mappings for a given source type to be quickly determined at runtime. Also, any needed plugin validations can take place within the initialize method.


Accessing PluginContext and its Uses

Each derived Plugin Module can reference the property named Context on the based ModuleContext class. This class contains several properties that can be accessed within the methods invoked on a module. Such as the Initialize and Configure methods discussed above. Additional properties will be discussed within future topics when appropriate.

Property Usage
AppHost Reference to the host plugin used for reading associated metadata. This can be helpful, for example, if the unique Id of the host's plugin is needed for recording associated information. For example, message broker queues can be tagged with the Id of a microservice's host so all running instances can subscribe the the same queue belonging to its microservice.
Plugin Reference to the plugin containing the module.
Configuration Reference to Microsoft's IConfiguration instance. Can be used to read application settings pertaining to the plugin module stored within the settings of the host process.
LoggerFactory Reference to Microsoft's ILoggerFactory instance. Can be used for creating loggers for use within a module. Note that this can only be used once the underlying IServiceProvider has been created. If the property is accessed prior to this, an exception will be thrown. Therefore, when logs need to be written before this time, the NfExtensions.Logger property should be used. This property is a reference to the IExtendedLogger instance passed into the CompositeContainer extension method during bootstrapping.
Logger Like the LoggerFactory property, can only be access after the IServiceProvider has been created. This property differs from the LoggerFactory since it is an already created logger with the type of the Plugin Module set as its Category. The Category for Microsoft's logger is used to tag log messages so the source of the log can be determined.

Some of these properties will be demonstrated in upcoming topic examples.


IMAGEMentioned above was that the LoggerFactory and Logger properties can't be accessed until the IServiceProvider has been created. After the composite-container is composed, the IServiceProvider will have been created. Once the composite-application is started, the IServiceProvider is available and passed to the Start, Run, and stop methods. At this time, the two logger properties can be accessed.


The below example shows accessing a property from the context. Add the following code to the Initialize method override within the AppOneModule class to log the PlugId associated with the HostPlugin:

using Microsoft.Extensions.Logging;
using NetFusion.Common.Base;
using NetFusion.Core.Bootstrap.Plugins;

namespace Examples.Bootstrapping.App.Plugin.Modules;

public class AppModuleOne : PluginModule
{
    public override void Initialize()
    {
        NfExtensions.Logger.Log<PluginModule>(
            LogLevel.Information, $"Initializing: {GetType().Name}");
        
        NfExtensions.Logger.Log<PluginModule>(
            LogLevel.Information, $"Host PluginId: {Context.AppHost.PluginId}");
    }

    public override void Configure()
    {
        NfExtensions.Logger.Log<PluginModule>(
            LogLevel.Information, $"Configuring: {GetType().Name}");
    }
}

If you run the microservice again, the identity of the host plugin will be written to the console or can be seen within SEQ.

cd ./src/Examples.Bootstrapping.WebApi
dotnet run

IMAGE

Plugin Configurations

Some plugins require configuration options specified by the host application when the plugin is added within the host's Program class. This can be any information used by Modules within the plugin to vary its initialization and runtime behavior.

For example, the core messaging pipeline implemented by NetFusion implements the enricher pattern. When a message is published, it passes through zero or more enrichers that can add information to the message. The core messaging plugin, for example, provides an enricher that will add a Correlation Guid to all messages. The host application can access the messaging plugin's configuration and register additional enrichers after optionally clearing any default registrations. A Plugin Configuration allows the host application to easily provide such settings to the modules contained with a plugin.


IMAGEPlug-in configurations should not be confused with application settings. The NetFusion.Settings plugin provides light weight extensions on top of the already feature rich .NET configuration implementation. This will be discussed within the Settings topic. On the other hand, Plugin Configurations are specified in code when the host is bootstrapped to control options of added plugins.


The following will define a simple configuration containing a message that will be logged by the application plugin modules. All configuration classes implement the IPluginConfig marker interface.

Add the below class to the Examples.Bootstrapping.App project at the following location: Examples.Bootstrapping.App/Plugin/Configs

using NetFusion.Core.Bootstrap.Plugins;

namespace Examples.Bootstrapping.App.Plugin.Configs;

public class HelloWorldConfig : IPluginConfig
{
    public string Message { get; private set; } = "World";

    public void SetMessage(string message) => Message = message;
}

The only remaining steps are as follows:

  • Register the configuration type within the project's Plugin definition
  • Reference the configuration within a Plugin Module.

Add a call to the AddConfig method within the constructor of the AppPlugin class: Examples.Bootstrapping.App/Plugin/AppPlugin.cs

public class AppPlugin : PluginBase
{
    public override string PluginId => "FA2A1F82-471C-44D0-A56C-01A7163D71C2";
    public override PluginTypes PluginType => PluginTypes.AppPlugin;
    public override string Name => "Application Services Component";

    public AppPlugin()
    {
        AddConfig<HelloWorldConfig>();  // <-- Add this line
        
        AddModule<ServiceModule>();
        AddModule<AppModuleOne>();

        Description = "Plugin component containing the Microservice's application services.";
    }   
}

Next, reference the Configuration within the AppModuleOne class and log the configured message: Examples.Bootstrapping.App/Plugin/Modules/AppModuleOne

Within the Initialize method, add the following code to read the configuration:

public override void Initialize()
    {
        NfExtensions.Logger.Log<PluginModule>(
            LogLevel.Information, $"Initializing: {GetType().Name}");
        
        NfExtensions.Logger.Log<PluginModule>(
            LogLevel.Information, $"Host PluginId: {Context.AppHost.PluginId}");
        
        var config = Context.Plugin.GetConfig<HelloWorldConfig>();  // <-- Add the following lines
        if (!string.IsNullOrEmpty(config.Message))
        {
            NfExtensions.Logger.Log<PluginModule>(
                LogLevel.Information, 
                $"The host application with the name of: {Context.AppHost.Name} says Hello {config.Message}");
        }
    }

Next, run the WebApi host and observe the message written to the log:

cd ./src/Examples.Bootstrapping.WebApi
dotnet run

IMAGE

The output shows that a default settings of a Plugin Configuration are used with default values if the host application does not override and configuration settings. Next, override the message by adding the following code within the Program class of Examples.Bootstrapping.WebApi:

// ..

// Add Plugins to the Composite-Container:
builder.Services.CompositeContainer(builder.Configuration, new SerilogExtendedLogger())
    .AddSettings()

    .AddPlugin<CrossCutPlugin>()

    .AddPlugin<InfraPlugin>()
    .AddPlugin<AppPlugin>()
    .AddPlugin<DomainPlugin>()
    .AddPlugin<WebApiPlugin>()

    .InitPluginConfig((HelloWorldConfig config) => config.SetMessage("is anyone home?")) // <-- Add this line
    .Compose();

// ..

The above code calls the InitPluginConfig method passing a delegate of the Plug-in Configuration to be initialized. Run the microservice service again and verify that the message has changed.

cd ./src/Examples.Bootstrapping.WebApi
dotnet run

IMAGE

Plugin Extending

Often one plugin will extend another by setting information on a configuration of the plugin being extended. For example, the NetFusion.Integration.ServiceBus plugin extends the base pipeline provided by the NetFusion.Messaging plugin by adding a IMessagePublisher implementation to the MessageDispatchConfig as follows:

using NetFusion.Core.Bootstrap.Container;
using NetFusion.Core.Settings.Plugin;
using NetFusion.Integration.ServiceBus.Internal;
using NetFusion.Integration.ServiceBus.Plugin.Configs;
using NetFusion.Integration.ServiceBus.Plugin.Modules;
using NetFusion.Messaging.Plugin;
using NetFusion.Messaging.Plugin.Configs;

namespace NetFusion.Integration.ServiceBus.Plugin;

public class ServiceBusPlugin : PluginBase
{
    public override string PluginId => "2E8CE828-146B-4383-9A02-DB838A72B6A5";
    public override PluginTypes PluginType => PluginTypes.CorePlugin;
    public override string Name => "NetFusion: Azure Service Bus";
        
    public ServiceBusPlugin()
    {
        AddConfig<ServiceBusConfig>();
            
        AddModule<NamespaceModule>();
        AddModule<NamespaceEntityModule>();
    }
}
    
public static class CompositeBuilderExtensions
{
    // Adds the Azure Service Bus plugin to the composite application.
    public static ICompositeContainerBuilder AddAzureServiceBus(this ICompositeContainerBuilder composite)
    {
        // Adds the Azure Service Bus and dependent plugins to the composite application.
        return composite
            .AddSettings()
            .AddMessaging()
            .AddPlugin<ServiceBusPlugin>()
                
            // Extend the base messaging pipeline by adding the ServiceBusPublisher.
            .InitPluginConfig<MessageDispatchConfig>(config => 
                config.AddPublisher<ServiceBusPublisher>());
    }
}

Note that in the above code, the Service Bus Plugin first adds any dependent plugins (Settings and Messaging) to the composite-container. This is followed by adding the ServiceBus plug-in (ServiceBusPlugin). Lastly, the plugin obtains the MessageDispatchConfig of the messaging plugin and adds the ServiceBusPublisher to the messaging pipeline. This is how the ServiceBus plugin integrates and extends the base messaging pipeline.

⚠️ **GitHub.com Fallback** ⚠️