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.