Wednesday, May 31, 2023

Micro API Design Pattern

 Hello there,

From the architectural/design point of view these are the common API design patterns:

  • monolith - all endpoints in one project (or dependencies)
  • micro services - separated by domain (and with specific components such as service registry or API gateway).

These have their own pros and cons and I would like to propose a hybrid approach - Micro APIs.

Technically, all micro APIs are hosted in a monolith but are not loaded as a normal dependencies - they are discovered at runtime. This approach is using the plugin-based design but applied to APIs. Basically, one can have all the benefits from the micro-services architectural/design pattern (as the micro APIs/plugins can be developed per domain and with different technologies, as long as the host can handle them) and still be manageable as a monolith (of course, this will not stop the host to be scaled and I think that sometimes a smart load balancer may redirect the traffic to the nodes that are domain specific, hence micro service).

Each micro API will have it's own API endpoints handlers (e.g. controllers), services and data models (aka DTO, ViewModels, etc.). Using patterns like dependency injection each micro API can use services from the host, if needed (e.g. configuration, logging, etc.).

A micro API project might look like this:


An ASP.NET Core/6/7 implementation might look like this:

1/ At Startup.cs the host will try to discover the plugins from some location (this can be changed if needed).

RegisterPlugins(services);
private void RegisterPlugins(IServiceCollection serviceCollection)
{
    var pluginsPath = Path.Combine(AppContext.BaseDirectory@".\Plugins");
    string[] pluginPaths = Directory.GetFiles(pluginsPath"*.Plugin.dll"SearchOption.AllDirectories);
    var pluginAssemblies = pluginPaths.Select(pluginPath => pluginPath.LoadAssembly());
 
    foreach (var assembly in pluginAssemblies)
    {
        assembly.LoadBaseServices(serviceCollection);
        serviceCollection.AddAutoMapper(assembly);
 
        serviceCollection.AddControllers()
            .PartManager.ApplicationParts.Add(new AssemblyPart(assembly));
    }
 
    // init plugins
    var serviceProvider = serviceCollection.BuildServiceProvider();
    var registrars = serviceProvider.GetServices<IServiceRegistrar>();
 
    foreach (var registrar in registrars)
    {
        registrar.Register(serviceCollection);
    }
}

This will try to load all plugins and add the controllers as an application part.

2/ In order to use DI, a micro API would have to implement an IServiceRegistrar interface where all the DI services from that micro API will be registered in the services collection.

public interface IServiceRegistrar
{
    void Register(IServiceCollection services); 

}

public sealed class ServiceRegistrar : IServiceRegistrar
 {
     public void Register(IServiceCollection services)
     {
         services.AddScoped<IMaptServiceMaptService>();
     } 

 } 

3/ The micro API controllers will be the normal ASP.NET Core/6/7 controllers - no changes here.

[ApiController]
[Route("api/[controller]")]
[Authorize]
public class MaptController : ControllerBase
{
    readonly IMapper _mapper;
    readonly IMaptService _maptService;
 
    public MaptController(IMapper mapperIMaptService maptService)
    {
        _mapper = mapper;
        _maptService = maptService;
    }
 
    [HttpGet]
    public IActionResult Get()
    {
        return StatusCode(200new MaptViewModel { Result = "OK" });
    }
}

4/ If you now run the WebAPI host and enable Swagger (e.g. Swashbuckle.AspNetCore) you will notice that even if the micro API project is not statically linked/referred by the WebAPI host, the micro APIs endpoints will appear in the Swagger documentation and will be available to be consumed.

If an existing micro API is removed then the impact on the host is minimal or non-existing (the host is auto-cleaning). Same for adding a micro API, the host will load it in it's memory context and expose any endpoints of the micro API.

Hope this will help you in designing new ways of consuming APIs!

Happy Coding!


The helper functions (which can be part of a common project/assembly):

public class PluginLoadContext : AssemblyLoadContext
    {
        private AssemblyDependencyResolver _resolver;
 
        public PluginLoadContext(string pluginPath)
        {
            _resolver = new AssemblyDependencyResolver(pluginPath);
        }
 
        protected override Assembly Load(AssemblyName assemblyName)
        {
            string assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName);
            if (assemblyPath != null)
            {
                return LoadFromAssemblyPath(assemblyPath);
            }
 
            return null;
        }
 
        protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
        {
            string libraryPath = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName);
            if (libraryPath != null)
            {
                return LoadUnmanagedDllFromPath(libraryPath);
            }
 
            return IntPtr.Zero;
        }
    }


public static class PluginHelpers
{
    public static Assembly LoadAssembly(this string path)
    {
        string pluginLocation = Path.GetFullPath(path);
        return new PluginLoadContext(pluginLocation).LoadFromAssemblyName(AssemblyName.GetAssemblyName(pluginLocation));
    }
 
    public static void LoadBaseServices(this Assembly assemblyIServiceCollection services)
    {
        foreach (Type type in assembly.GetTypes())
        {
            if (typeof(ICommand).IsAssignableFrom(type))
            {
                services.AddSingleton(typeof(ICommand), type);
            }
            if (typeof(IServiceRegistrar).IsAssignableFrom(type))
            {
                services.AddSingleton(typeof(IServiceRegistrar), type);
            }
        }
    } } 

No comments: