Edited for a more concrete example...
This all relates to Microsoft.Extentions.DependencyInjection...
I have found the information available (both documentation and blogs) for dependency injection to be extremely lacking in the details I want to know. It brings to mind all those memes about unit tests working but integration tests failing - all the documentation/blogs I've found are the unit tests, and no-one has written about integrating different scopes. I've been doing some trial and error, but got to a point where I've realised I would need to make huge changes to my app just to test something out, so I'm hoping someone can provide this info (take this also as a nudge that a blog on this topic would be most welcome).
I have 2 services (might increase to more later, but 2 for now). Let's say they're Chinese Restauraunts with the same menu (beef in black bean sauce, spring rolls, etc.) but different deals on different days or other differences, so some days you order from one place, other days you order from the other place, but you're never ordering from both places, so loading both into memory is wasting memory (especially if you add more restaurants). So we have a common interface IMenu, and 2 services RestaurantA and RestaurantB. I want to be able to select which one to use (from user settings), and ONLY have that one using any memory. Every example I have seen has both RestaurantA and RestaurantB defined as singletons and tells you how to switch between them, but then both of them are taking up memory when I only need one at a time (and even more of a memory issue if/when I add more services).
So, the way I do this without DI is...
MenuPage(IMenu menu)
{
string restaurant=Preferences.get("CurrentChoice",default);
switch (restaurant)
{
case "RestaurantA":{menu=new RestaurantA();}
case "RestaurantB":{menu=new RestaurantB();}
}
}
This works fine without DI because if you switch from using A to B, A goes out of scope and gets garbage collected. But...
Further to that, I found that the page you inject has to also be registered (was only mentioned on 1 blog - nowhere else did I ever see this mentioned), so in this case MenuPage itself is registered in DI (either as a singleton, or a scoped service itself, but even if it's scoped, somewhere upstream there's a singleton page, because this is all inside of a MAUI Flyout page, which never goes out of scope).
i.e. in the builder there would be something like...
MauiAppBuilder builder=MauiApp.CreateBuilder();
builder.Services
.AddSingleton<MAUIFlyoutPage>()
.AddScoped<MenuPage>()
.AddScoped<IMenu,RestaurantA>()
.AddScoped<IMenu,RestaurantB>();
So, one thing I did find, but is the fly in the ointment, is if you inject a Scoped service into a Singleton, it effectively becomes a Singleton, because the Singleton never goes out of scope, so it never goes out of scope (logically this makes sense). So whether MenuPage is a singleton or scoped, either way it's inside a singleton FlyoutPage, so it appears to me that the scope gets trapped so no garbage collection happens?
Qualifier, this is all with constructor injection. I saw mentioned somewhere property injection, and maybe this is a way to do it? Asking before I go down another potentially fruitless rabbit hole of singletons and trapped scopes.
Basically I just want to know if there's a way in Dependency Injection to switch between 2 (or more) services, and only have 1 at a time loaded in memory (as opposed to all of them loaded in memory and switching)? As far as I can see it can't be done (not when you have a root page that never goes out of scope, and all pages that you want to be injected into also have to be registered... which eventually bring you back down to the root page).
Here's one way to solve the problem:
public sealed class MenuProxy : IMenu
{
// This class depends on the Contianer and should, therefore, be part of your
// application's Composition Root (see https://mng.bz/K1qZ).
private readonly IServiceProvider container;
public MenuProxy(IServiceProvider container) => this.container = container;
private IMenu GetMenu()
{
string restaurant=Preferences.get("CurrentChoice",default);
switch (restaurant)
{
case "RestaurantA": return this.GetRestaurant<RestaurantA>();
case "RestaurantB": return this.GetRestaurant<RestaurantB>();
}
}
private T GetRestaurant<T>() => this.container.GetRequiredService<T>();
// Map all IMenu methods here to the GetMenu() method.
public Appetizer[] GetAppetizers() => this.GetMenu().GetAppetizers();
}
Registration:
MauiAppBuilder builder=MauiApp.CreateBuilder();
builder.Services
.AddSingleton<MAUIFlyoutPage>()
.AddScoped<MenuPage>()
.AddScoped<RestaurantA>()
.AddScoped<RestaurantB>()
.AddScoped<IMenu, MenuProxy>();
This ensures the following thing:
MenuProxy
can be injected into consumers as IMenu
. This allows consumers to stay ignorant of the fact that multiple IMenu
implementations exist in the backgroundMenuProxy
will resolve the required restaurant implementation based on the user choice, preventing the other from being loaded when not neededRestaurantA
and RestaurantB
are registered using AddScoped
, only one instance of the restaurant will exist in mememory for the duration of a single scope, independently of how often IMenu
's and MenuProxy
's methods are called.MenuProxy
inside the application's Composition Root you prevent the application from applying the Service Locator anti-pattern.