Business Extensions

Business Extensions

Since I write so many NUGET packages, I also tend to write more than a few extension methods containing startup / construction logic. Each library typically has at least two methods: (1) a AddWhatever method, which usually registers the types from my library, and (2) a UseWhatever method, which usually runs any kind of startup that is required by my code. The ‘Whatever” part of those methods is just a descriptive moniker for, well, whatever library we’re talking about. So, for instance, the CG.Email NUGET package has a AddSmtpRepositories method, and the CG.Sms.Twilio NUGET package has a AddTwilioRepositories method. Make sense?

After writing a few dozen iterations of AddWhatever / UseWhatever, in a few dozen NUGET packages, I eventually discovered that quite a bit of what I was doing was either repetitive, or almost repetitive. Because of that, I stepped back and pulled all the common code together, into a central abstraction. I left the uncommon code for external extension methods and I tied everything together with a bit of .NET reflection. The result is, now I’m writing less repetitive code, in order to pull one of my NUGET packages into a project and get everything wire up correctly.

To be specific, I came up with an extension method named AddRepositories, and another called UseRepositories, as part of the CG.Business NUGET package, that I use to dynamically construct a concrete repository, at runtime, based on configuration settings.

I’ll discuss both methods in this blog post. Both are part of the CODEGATOR CG.Business NUGET package. The source code for that package is available HERE.


I’ll start with a few quick snippets, to demonstrate how I typically use these methods:

Assume I have a project with a configuration like this:

{
  "Foo": {
    "Name": "Test",
    "AssemblyNameOrPath": "MyAssemblyName",
    "ServiceLifetime": "Scoped",
    "Test": {
      "A": "1",
      "B":  "2"
    }
  }
}

Assume further that I have a project with startup code, like this:

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddScoped<MyService>();

        services.AddRepositories(
            Configuration.GetSection("Foo")
            );
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        app.UseRepositories(
            env,
            "Foo"
            );
    }
}

Finally, assume that I have a library with a service, and a repository class, like this:

class MyService 
{
   public MyService(MyRepository repo) {}
}

class MyRepository 
{
   public MyRepsitory() { }
}

The trick is, to wire up the MyService and MyRepository types, without actually hard coding the MyRepository type, in the Startup class. That’s because almost all my services typically defer to a repository for any technology specific I/O code. Since I’ve gone to the trouble of separating my business logic from my I/O logic, to maintain a reasonable level of flexibility, I certainly want to follow through and ensure that my startup / construction logic is equally as flexible.

If we look at the example above, we see that the call to AddRepositories is passed an IConfiguration instance. That object is pointing to the configuration section that corresponds with the repository I’m using. So, for instance, in the example we see that the AddRepositories method is passed the Foo configuration section.

Looking at the configuration, we see that Foo contains a Name property, containing the value “Test”. That value works well enough for this simple demo. Name can have whatever value you like, but for a real project, I would probably use a string that identifies the underlying technology. For instance, “SqlServer”, or “CosmoDb”. For now though, “Test” is fine. Farther down, we see a section named Test, in the same configuration section as the Name property. That is where we would typically put configuration parameters for whatever that repository needs, at runtime. So, for instance, if this were a SQL Server repository, we might include a connection string here.

One piece that’s still missing is an extension method to perform any non-common registration or construction logic, for the repository. For instance, for a SQL Server repository, we’ll certainly want to construct a data-context and pass that into our repository. Here is what that extension method might look like:

public static partial class ServiceCollectionExtensions
{
    public static IServiceCollection AddTestRepositories(
        this IServiceCollection serviceCollection,
        IConfiguration configuration,
        ServiceLifetime serviceLifetime
        )
    {
        // This method is an example of how to hook into the startup
        //   pipeline and run logic to register your specific repository
        //   types, and, of course, any types those repositories also 
        //   rely on. For instance, here is a great place to register 
        //   you data-context type(s), if you're using EFCore.

        return serviceCollection;
    }
}

The only thing about this method that I want to point out, is the name. Notice that it complies with this structure: Add[label]Repositories, where the [label] part matches whatever you chose to use in the Name property, of the configuration.

Put your repository specific code here. My AddRepositories method will, in conjunction with the configuration, take care of locating and calling this method, at startup.

The next thing to cover is the UseRepositories method. I wrote this because I realized that so many database libraries require some sort of startup code to run against the underlying database – often before any application code runs. For instance, EF core often requires migrations to be run, at startup. In addition, it’s pretty typical to do things like create, or drop, or seed a database, at startup – at least in a development environment.

I’ve seen many, many examples of this sort of code getting added either directly to the Startup class itself, or called from an extension method, in the Startup class, or even (shudder) called directly from the HostBuilder code, in the Program.cs file. While it can be argued that the Startup class might need to know about the database technology underlying one or more repository types, I don’t buy for a second that the HostBuilder ought to ever have to know anything about databases, or repositories, or really much of anything other that HostBuilder related stuff.

That’s the biggest reason I added UseRepositories – to give everyone a convenient, and location appropriate place to perform startup related “stuff” for repository types.

So how does UseRepositories work, exactly? Almost exactly like AddRepositories, except that instead of registering types, we’re running startup code. That means, we’re reading the exact same configuration section, and using it to locate and call a similar extension method. In this case though, the extension method will follow this naming pattern: User[label]Repositories, where [label], once again, matches whatever you chose to use in the Name property, of the configuration.

Here is an example of the extension method UseRepositories will locate and call:

public static partial class ApplicationBuilderExtensions
{
    public static IApplicationBuilder UseTestRepositories(
        this IApplicationBuilder applicationBuilder,
        IWebHostEnvironment hostEnvironment,
        string configurationSection
        )
    {
        // This method is an example of how to hook into the startup
        //   pipeline and run logic specific to your repository types,
        //   and, of course, whatever tech those repository types rely
        //   on.  For instance, here is a great place to call logic
        //   to perform migrations, or seed an empty database.

        return applicationBuilder;
    }
}

This method provides access to the application builder, and all it’s registered services. It also provides access to the host environment, which is crucial of your logic should only run in certain environments. Finally, the method also provides a hint as to what part of the configuration is associated with this method. It’s unusual to need the configuration, at this point, but at least the hint is there, if you need it.

Put your repository specific startup code here. My UseRepositories method will, in conjunction with the configuration, take care of locating and calling this method, at startup.


I’ve covered what the AddRepositories and UseRepositories methods are, and what they do. Now let’s go look at the source code and break down what’s going on. Since the implementation for AddRepositories is so similar to UseRepositories, I’ll only break down AddRepositories here.

Here is the listing for the AddRepositories method:

public static IServiceCollection AddRepositories(
    this IServiceCollection serviceCollection,
    IConfiguration configuration,
    string assemblyWhiteList = "", 
    string assemblyBlackList = "Microsoft*, System*, mscorlib, netstandard"
    )
{
    Guard.Instance().ThrowIfNull(serviceCollection, nameof(serviceCollection))
        .ThrowIfNull(configuration, nameof(configuration));

    var loaderOptions = new LoaderOptions();
    configuration.Bind(loaderOptions);

    if (false == loaderOptions.IsValid())
    {
        throw new ConfigurationException(
            message: string.Format(
                Resources.InvalidLoaderSection,
                nameof(AddRepositories),
                configuration.GetPath()
                )
            );
    }

    var strategyName = loaderOptions.Name.Trim();
    if (string.IsNullOrEmpty(strategyName))
    {
        throw new ConfigurationException(
            message: string.Format(
                Resources.EmptyStrategyName,
                nameof(AddRepositories)
                )
            );
    }

    try
    {
        if (false == string.IsNullOrEmpty(loaderOptions.AssemblyNameOrPath))
        {
            if (loaderOptions.AssemblyNameOrPath.EndsWith(".dll"))
            {
                _ = Assembly.LoadFrom(
                    loaderOptions.AssemblyNameOrPath
                    );

                assemblyWhiteList = assemblyWhiteList.Length > 0
                    ? $"{assemblyWhiteList}, {Path.GetFileNameWithoutExtension(loaderOptions.AssemblyNameOrPath)}"
                    : $"{Path.GetFileNameWithoutExtension(loaderOptions.AssemblyNameOrPath)}";
            }
            else
            {
                _ = Assembly.Load(
                    loaderOptions.AssemblyNameOrPath
                    );

                assemblyWhiteList = assemblyWhiteList.Length > 0
                    ? $"{assemblyWhiteList}, {loaderOptions.AssemblyNameOrPath}"
                    : $"{loaderOptions.AssemblyNameOrPath}";
            }
        }
    }
    catch (Exception ex)
    {
        throw new BusinessException(
            message: string.Format(
                Resources.NoLoadAssembly,
                loaderOptions.AssemblyNameOrPath
                ),
            innerException: ex
            );
    }

    var methodName = $"Add{strategyName}Repositories";
            
    var methods = AppDomain.CurrentDomain.ExtensionMethods(
        typeof(IServiceCollection),
        methodName,
        new Type[] { typeof(IConfiguration), typeof(ServiceLifetime) },
        assemblyWhiteList,
        assemblyBlackList
        );

    if (methods.Any())
    {
        var subSection = configuration.GetSection(
            strategyName
            );

        var method = methods.First();

        method.Invoke(
            null,
            new object[] 
            { 
                serviceCollection, 
                subSection, 
                loaderOptions.ServiceLifetime 
            });
    }
    else
    {
        throw new BusinessException(
            message: string.Format(
                Resources.MethodNotFound,
                nameof(AddRepositories),
                methodName,
                $"{nameof(IServiceCollection)},{nameof(IConfiguration)}, {nameof(ServiceLifetime)}"
                )
            );
    }
    return serviceCollection;
}

The first thing the method does is validate the incoming parameters. Next, it creates an instance of LoaderOptions. That class contains three pieces of information: (1) The name of the section we’ll eventually need to read from, (2) an optional assembly name (or path), in case we need to load an external assembly, and (3) an optional service lifetime to use when registering the repository types with the DI container. The LoaderOptions are bound directly to the incoming IConfiguration object, which means the configuration section used for the method can be named anything you want, but, it must have the Name property in that section …

After binding the LoaderOptions, the method validates the options, to ensure all required properties were present, and valid, in the configuration. Assuming everything validates, the next thing the method does is trim any whitespace from the Name property of the LoaderOptions object. The trimmed value is then validated again, just in case it was all spaces. The resulting strategyName variable contains the value we’ll use to locate the external extension method with.

Before we try to locate the external extension method, we need to check to make sure we have the assembly loaded. If the AssemblyNameOrPath property of the LoaderOptions object contains a value, we check to see whether it ends with ‘.dll’, or not. If it does, we try to load the assembly using the AssemblyNameOrPath property as a path. If not, we try to load the assembly using the AssemblyNameOrPath property as an assembly name.

If an assembly was specified, we also add the resulting assembly name to the white-list, for the upcoming extension method search. Doing that greatly narrows the range of that query and speeds up the resulting search operation.

Once we know we have the assembly loaded, we then move on to format the actual name of the external extension method. After that, we use the ExtensionMethod extension method, to locate the method we’re looking for, by searching through the assemblies loaded into the current AppDomain. That’s why we added the assembly name to the white list, to make this operation more efficient.

If we don’t find a matching extension method, we throw an exception.

If we do find a matching extension method, we then locate the appropriate sub-section, in the configuration, and pass that along, with the service collection, and the service lifetime hint, to the external extension method.

At this point, we’ve located and called the method. Nothing left to do but return.


As I said before, the UseRepositories method works almost exactly like the AddRepositories method, except that it searches for a difference external extension method, and passes a different set of arguments to that method, at runtime. Everything else is almost identical between the two methods – implementation wise.


Repositories aren’t the only abstractions that I use regularly, I also tend to use concrete strategy types regularly. Because of that, there are also AddStrategies and UseStrategies methods, in the CG.Business NUGET package. Once you understand how to use AddRepositories and UseRepositories, you’ll also understand how to use AddStrategies and UseStrategies.

Inside CG.Business, there are also a number of different base types for repositories, strategies, stores, services, and models. A number of other CODEGATOR packages derive from these types. They also use the Add/Use Strategies and Add/Use Repositories methods, to wire themselves into a calling application. The result are components that work in a dynamic fashion, using the configuration, but also allowing for a great deal of flexibility in how your startup logic actually gets called, in your application.


The source for the CODEGATOR CG.Business NUGET package can be found HERE.

The CODEGATOR CG.Business NUGET package itself can be found HERE.

Photo by Pedro Lastra on Unsplash