1

Hope this is the right place to post an opinion-based question like this. I'm in the process of converting an aging API monolith to a set of .NET 5/C#-based microservices and working on the code reuse strategy. Of course, this is a hot topic, and there are half a dozen or more different ways of approaching it, but what I'd like to be able to do is have a set of shared libraries that can be updated dynamically without having to recompile/redeploy (or, ideally, even stop/restart) the microservices that leverage them. Maybe I'm old-fashioned, but the idea of duplicating and having to maintain the same code in dozens of microservices just doesn't seem practical, and for performance reasons, setting all this functionality up as individual microservices, themselves, isn't practical. All that by way of explaining why I'm trying to do this. The solution I've been exploring is to use the overload of Assembly.Load() that takes byte arrays for the assembly and its PDB file. This way the file is not locked on disk and can be updated as needed. A FileSystemWatcher alerts the service of a change to the assembly, and DI -- using AddScoped() and constructor injection -- handles the plumbing. Here's a simplified example:

Startup.cs:

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers();
        services.AddMvc(options => options.EnableEndpointRouting = false);

        foreach (string path in Program.GetAppSettings("IOCAssemblies"))
            LoadAssembly(path, ref services);
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        app.UseRouting();
        app.UseMvc();
    }

    private static void LoadAssembly(string pathToAssemblyFile, ref IServiceCollection services)
    {
        FileInfo dll = new(pathToAssemblyFile);
        FileInfo pdb = new(pathToAssemblyFile.Replace(".dll", ".pdb"));
        Assembly assm = Assembly.Load(File.ReadAllBytes(dll.FullName), pdb.Exists ? File.ReadAllBytes(pdb.FullName) : null);
        FileSystemWatcher fsw = new(dll.DirectoryName, dll.Name)
        {
            EnableRaisingEvents = true,
            NotifyFilter = NotifyFilters.CreationTime | NotifyFilters.LastWrite | NotifyFilters.Size
        };
        FileSystemEventHandler handler = (s, e) => 
        {
            byte[] dllBytes = File.ReadAllBytes(pathToAssemblyFile);
            byte[] pdbBytes = pdb.Exists ? File.ReadAllBytes(pdb.FullName) : null;
            assm = Assembly.Load(dllBytes, pdbBytes);
        };

        fsw.Created += handler;
        fsw.Changed += handler;

        foreach (Type t in assm.GetExportedTypes())
            services.AddScoped(t.GetInterfaces()[0], isp => Activator.CreateInstance(assm.GetType(t.FullName)));
    }
}

Sample controller:

[ApiController]
[Route("[controller]")]
public class TestController : ControllerBase
{
    public TestController(IUtilities util)
    {
        _util = util;
    }

    private readonly IUtilities _util;

    [HttpGet]
    public IActionResult Get() =>
        Ok(_util.Test());
}

And this is the simple Utilities class and its interface that I'm using only for illustration purposes:

public class Utilities : IUtilities
{
    public string Test() =>
        "Test 1";
}

public interface IUtilities
{
    string Test();
}

This all works beautifully for instance classes. The problem comes with static classes, specifically those used to define extension methods. Obviously, standard DI doesn't work for static classes, and I've really been struggling to find a solution.

Full disclosure: I've been told that I'm trying to negate what little practical meaning the term "microservices" has, that this approach is, "a coupled distributed and brittle mess that is easily broken," and that these sorts of services, "should be self contained, part of a CDCI pipeline, be independent of any other service, and should be able to be developed and deployed without conflict." While I agree with that last statement, I don't think the approach I'm pursuing is in conflict with it. Services architected this way would be no more dependent on each other than they would if using a shared DLL. (Or is even that an anti-pattern when it comes to microservices?) They would also be able to be independently developed and deployed via a CI/CD pipeline. Basically, I'm just trying to create a shared library that can be updated dynamically.

Like I alluded to above, maybe I'm a dinosaur and have just had DRY and similar principles beaten into me for so long that I can't comprehend anything else, but it's really hard to believe that the best approach to this sort of thing is to completely duplicate all shared code in every service (likely 60+ of them), then change it in multiple places anytime it needs to be modified. From my research, though, there only seem to be a few alternatives, like the ones mentioned here: The Dilemma of Code Reuse in Microservices. Heck, maybe I just need to abandon the term "microservices" and call these "miniservices" or something. (j/k) I just want to make maintenance and support of them as easy as possible.

Does anyone have an approach to this that I haven't come across? Any opinions are welcome.

20
  • 5
    Why would you go through all of this effort rather than using blue/green deployments or other approaches to zero-downtime releases?
    – Telastyn
    Commented Nov 17, 2021 at 3:58
  • 2
    You have to deploy 60+ assemblies every time the shared code changes anyways? And I’m pretty sure any company with a change management process will promptly fire you if they find out that you’re circumventing it…
    – Telastyn
    Commented Nov 17, 2021 at 5:01
  • 1
    I think Telasnyn has a point. Hot-updating a shared lib on which 60+ microservices depend has a certain risk of breaking all those 60 microservices in production at once if something goes wrong. And the DRY principle does not say that all your microservices must us the same version of the lib at all times in production, you can be perfectly DRY with one lib which is developed iteratively in one place, but deployed in specific versions together with the related microservice deliberately, only when the change in the lib will also change the visible behaviour of the service,...
    – Doc Brown
    Commented Nov 17, 2021 at 6:33
  • 2
    I suspect you are dividing your microservices wrongly. If 60 services use the same logic, and it is paramount that they always use exactly the same version of that logic, then that logic is a bounded context of its own and can be made into its own microservice. If that is not worth the bother, then your code is likely still a monolith, just one that is now being deployed in chunks not because it made logical sense to do so but because you could get away with doing it. Telastyn and Doc Brown are on the right track here, you should consider staying away from hot reloading.
    – Flater
    Commented Nov 17, 2021 at 9:31
  • 1
    Something has got to give here. If you can't redeploy services with relative ease, don't want to redeploy many services, at the same time want to have 60+ services up and running, cannot bear the overhead cost of one service calling another service, and are squeezing for performance to the point that you just stated you don't even want any out-of-process services; then it just doesn't make any sense that you're using microservices at all. [..]
    – Flater
    Commented Nov 17, 2021 at 21:55

0