Skip to content

Auto Discovery Pattern

When developing Framework Modules, sometimes we provide interfaces for consumers to implement. Or we are wrapping a third party library which has some useful types like AutoMapper.Profile, etc.

This pattern is quite similar to the Provider Pattern. Actually, the Provider Pattern can be implemented using Auto Discovery instead of DI. The Auto Discovery pattern's advantage is that it allows you to modify or inspect the types before registering them into DI.

The goal is always to automatically discover all these types from all registered modules at runtime, instead of having consumers having to call a method to add these implementations. Again, AutoMapper.Profile is a good example: you just implement a class which extends from Profile and it gets automatically discovered, if you depend on the AutoMapperModule, of course.

In order to implement this, we have two ground rules:

  1. Keep ModuleOptions as the source of truth
  2. Use the discovered types as late as possible.

Auto Discovery Module Options

The Auto Discovery pattern consist of a Module Options which includes the list of types that were discovered or manually added.

Module Options should include a, usually internal, ICollection<Type> and public convenience methods for adding types, like using generics.

They should also include a bool for toggling each auto discovery of types (if multiples are provided), and a method, DisableAllAutoDiscovery(), to disable them all. This is quite useful for testing purposes, where you only want a set of types to be included in the test case.

When disabling auto discovery, the convenience methods for adding types are used.

Let's see an example. Say we have this interface that we want to discover all its implementors:

C#
public interface IWorker {}

Our module options may look like this:

C#
public class MyModuleOptions {
    internal ICollection<Type> WorkerTypes { get; } = new HashSet<Type>();

    public bool IsWorkerAutoDiscoveryEnabled { get; set; } = true;

    public MyModuleOptions DisableAllAutoDiscovery() {
        this.IsWorkerAutoDiscoveryEnabled = false;
        return this;
    }

    public MyModuleOptions AddWorker(Type workerType) {
        this.WorkerTypes.Add(workerType);
        return this;
    }

    public MyModuleOptions AddWorker<TWorker>()
        where TWorker: IWorker
    {
        return this.AddWorker(typeof(TWorker));
    }

    public MyModuleOptions AddWorkers(IEnumerable<Type> workerTypes) {
        this.WorkerTypes.AddRange(workerTypes);
        return this;
    }
}

Then, in our module we would use this like so:

C#
public class MyModule : SuiteModule {
    public override void ConfigureServices(
        IServiceCollection services,
        ModuleConfigurationContext context)
    {
        var opts = context.GetModuleOptions<MyModuleOptions>().Value;

        // Auto discover workers
        if (opts.IsWorkerAutoDiscoveryEnabled) {
            var autoDiscoveredWorkerTypes = context.FindTypesFromAllModules<IWorker>();
            opts.AddWorkers(autoDiscoveredWorkerTypes);
        }

        // Register workers in DI
        foreach(var workerType in opts.WorkerTypes) {
            // Wrap the worker but register as IWorker
            var finalType = typeof(IWorkerWrapper<>).MakeGenericType(workerType);
            services.AddTransient(typeof(IWorker), finalType);
        }
    }
}