DEV Community

Cover image for Why manually registering component when Auto-Discovery exist?
Majdi Zlitni
Majdi Zlitni

Posted on

Why manually registering component when Auto-Discovery exist?

For production grade applications SOLID are a must principles, but when adding a new feature open for extension close for modification principle could be easily violated.


Hardcoding filters

Let's take an example of an image processing application where we need an image filter plugin system.
Image Taker

The core interface is simple, it must be implemented by classes as a best practice.
IFilter file Name represents the filter that we will apply to a given image.


The common approach

The common way is to manually register every known filter

public class FilterManager
{    
    public IFilter GetFilter(string filterName)
    {
        switch (filterName.ToLower())
        {
            case "grayscale":
                return new GrayscaleFilter();

            case "sepia":
                return new SepiaFilter();

            // PROBLEM: To add a new filter, we must come back here and modify this file.
            /*
            case "invert":
                return new InvertColorsFilter();
            */

            default:
                throw new NotSupportedException($"Filter '{filterName}' is not recognized.");
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Main application logic:

Mais.cs file


Why this is a problem?

This approach is technical debt and time consuming because it will introduce:

  • Open/Close principle violation: The FilterManager is not 'closed for modification.' To add a new InvertColorsFilter, we are forced to open and change the FilterManager code which is time consuming, overwhelming and could be risky for a production grade level.

  • Error prone: Whenever a new filter is added we should always add it to code to previous switch statement and this creates a tightly coupled application.

  • Merge conflict: For faster problems solving and project success, team play is needed but as the team grow a single file can become the hotspot where multiple developers edit it simultaneously, which introduces frustrating merge conflicts.


Assembly scanning for discovery as a solution

Refactoring the code for a production-grade level by adding a generic engine that builds classes automatically would be a better approach.

public class FilterManager
{
    private readonly IReadOnlyDictionary<string, Type> _discoveredFilters;

    public FilterManager(IReadOnlyDictionary<string, Type> discoveredFilters)
    {
        _discoveredFilters = discoveredFilters;
    }

    public IFilter GetFilter(string filterName)
    {
        if (!_discoveredFilters.TryGetValue(filterName.ToLower(), out var filterType))
        {
            throw new NotSupportedException($"Filter '{filterName}' is not recognized.");
        }        
        return (IFilter)Activator.CreateInstance(filterType)!;
    }
}

private static IReadOnlyDictionary<string, Type> BuildFilterCatalog()
{
    var catalog = new Dictionary<string, Type>();    
    var pluginAssembly = typeof(Program).Assembly;
    var filterTypes = pluginAssembly.GetTypes()
        .Where(t => t.IsClass && !t.IsAbstract && typeof(IFilter).IsAssignableFrom(t));

    foreach (var type in filterTypes)
    {
        var tempFilter = (IFilter)Activator.CreateInstance(type)!;
        Console.WriteLine($"[Plugin Discovery] Found filter: '{tempFilter.Name}'");
        catalog[tempFilter.Name.ToLower()] = type;
    }

    return catalog;
}
Enter fullscreen mode Exit fullscreen mode

How it works

  • discoveredFilters.TryGetValue(...): Tries to find an entry in the dictionary using a key.

  • pluginAssembly.GetTypes(): The pluginAssembly have references to .dll and .exe files.

    • GetTypes(): will return every single:
    • class
    • interface
    • struct
    • enum
  • .Where(...): Is a Language Integrated Query, used to keep data that satisfy given conditions.

    • t.IsClass: checks if the current type is a class.
    • !t.IsAbstract: filters out abstract classes.
    • typeof(IFilter).IsAssignableFrom(t): determines if the type can be assigned to a variable of type IFilter.

foreach loop takes a list of objects, each representing a filter class discovered by reflection and for each type in the list, an instance of the class is created using Activator.CreateInstance().


What is reflection?

In .NET an Assembly is a compiled output of code that contains .dll and .exe files and by using System.Reflection.Assembly we can load and inspect our solution files programmatically


Combining dynamic discovery via reflection with builder pattern we achieved not only flexible but also an extensible architecture.
Main file

We have used reflection and dependency injection to load filters dynamically without hard coding.


Conclusion

Using this shifting mindset we can create robust and scalable system but scanning every assembly can lead to slowness therefore we should be specific for best practices.

Top comments (0)