So I'm currently looking at some code and I'm trying to figure out how this line manages to resolve the "factory" which apparently is nothing but a delegate that takes a "Type" and returns an object. It's hard for me to form this question because I don't fully understand what's going on. Could someone break it down?
It all starts in the App.cs
public App()
{
IServiceCollection _services = new ServiceCollection();
_services.AddSingleton<MainViewModel>();
_services.AddSingleton<HomeViewModel>();
/* This is the part I don't understand */
_services.AddSingleton<INavigationService, NavService>(sp =>
{
return new NavService(type => sp.GetRequiredService(type));
});
...
_serviceProvider = _services.BuildServiceProvider();
}
This essentially builds the singleton instance that we're registering
_services.AddSingleton<INavigationService, NavService>(sp =>
{
return new NavService(type => sp.GetRequiredService(type));
});
And we're passing in whatever sp.GetRequiredService(type)
returns as a parameter to the NavService
constructor.
Which seems to be a Func<Type, object>
? Why?
And what is type
that we're using when we're using the lambda statement type => sp.GetRequiredService(type)
How do we resolve a Func<Type, object>
from type
?
Inside the NavService
we're utilizing that delegate by invoking it with a type, which I believe resolves the singleton instance of whatever type we're using when calling NavigateTo<T>
public class NavService : ObservableObject, INavigationService
{
private readonly Func<Type, object> factory;
private object _currentView;
public NavService(Func<Type, object> factory)
{
this.factory = factory;
}
public object CurrentView
{
get => _currentView;
private set
{
_currentView = value;
OnPropertyChanged();
}
}
public void NavigateTo<T>() where T : ViewModel
{
object viewModel = factory.Invoke(typeof(T)) ?? throw new InvalidOperationException("Could not locate VM.");
CurrentView = viewModel;
}
}
So my best guess is that, what we pass in through the constructor is the actual "factory" behind the Microsoft.Extensions.DependencyInjection
package that I'm using which is responsible for newing up instances of dependencies that we're registering.
But that still doesn't answer my question of how we resolve a Func<Type, object>
from type
which is just of type Type
?
CodePudding user response:
And we're passing in whatever sp.GetRequiredService(type) returns as a parameter to the NavService constructor.
No you aren't.
You are passing in a lambda type => sp.GetRequiredService(type)
to the constructor. This lambda is the factory method.
The dotnet runtime doesn't have any direct support for lambda's. Instead the C# compiler will translate your code into IL that is equivalent to;
public class Captures{
private IServiceProvider sp;
public Captures(IServiceProvider sp){
this.sp = sp;
}
public object Factory(Type type){
return sp.GetRequiredService(type);
}
}
_services.AddSingleton<INavigationService, NavService>(sp =>
{
var captures = new Captures(sp);
return new NavService(new Func<Type,object>(captures.Factory));
});
So when NavService
executes factory.Invoke
, the generated method Captures.Factory
will be called, which will in turn call the extension method sp.GetRequiredService
to obtain the instance of the requested Type
.
CodePudding user response:
You are creating an inline lambda as delegate. The delegate is the used as a factory to create instances dynamically.
When you have the following constructor...
class NavigationService : INavigationService
{
public NavigatioService(Func<Type, object> factory)
{}
}
...then you must register a related factory delegate in order to be able to construct it with a DI container. The factory delegate in the above example takes a parameter of type Type
and returns an instance of type object
.
The factory
delegate translates to the following lambda expression:
typeParameter => factory_implementation_that_returns_object;
Instead of instantiating the type manually you can use the service provider to instantiate it for you. This will automatically create all required dependencies:
// Create a delegate that takes a Type as parameter and returns an object using the ServiceProvider
type => sp.GetRequiredService(type)
Now for some reason you have decided to create the NavigationService
instance explicitly (new NavigationService()
). As a drawback you also have to explicitly build all the constructor dependencies, which is the Funcy<Type, object>
delegate:
// Register the INavigationService and tell the IoC container how to build this type
_services.AddSingleton<INavigationService, NavService>(sp =>
{
// Imagine NavService had more constructor dependencies!
// Imagine even this single dependency had 10 dependencies itself,
// where each has n dependencies, where each has...
// You would find yourself to take care to satisfy all of them explicitly at this point...
return new NavService(type => sp.GetRequiredService(type));
});
Instead of registering the Func<Type, object>
as a regular dependency, you decided to create this delegate manually inline. This is not a good decision. You always want he IoC container to wire up the dependencies for you.
I don't recommend to do it this way (inline). It only works when the constructor has a single parameter and this parameter doesn't requests its own dependencies. Otherwise you would have to construct all constructor dependencies manually (inline) - which is definitely not what you want.
Additionally, it's also better (more robust) to return an explicit type instance from the factory instead of object
.
Because you have defined the Navigate
method as generic with a type constraint that T
must be of type ViewModel
...
// The type of T is used as the parameter for the factory delegate
public void NavigateTo<T>() where T : ViewModel
...you should use ViewModel
as the explicit return type of your factory.
To improve your code, the goal is to let the IoC container do all the work. That's what it is for. As a bonus, the code becomes simpler and the configuration feels more intuitive.
The key is to design your code that you don't have to create the NavService
instance manually. For this reason you should try to use a parameterless factory delegate. See this answer (solution #2) to get an example on how to design the Navigate
method. There are good design reasons to not allow every page to navigate to any page. The idea is to let the caller pass in the reference instead of the Type
(and instantiate this Type
in the NavService
). Just do it like the Frame
does it.
However, to fix and improve your example you simply have to register the factory delegate as a service. This way you don't have to create it inline and you don't have to create the receiving type manually too:
class NavigationService : INavigationService
{
private Func<Type, ViewModel> Factory { get; }
public ViewModel CurrentView { get; private set; }
// Improve the robustness and use a strongly typed result (of type ViewModel).
// The Type parameter of the delegate is later provided by the caller, which is the Navigate method.
public NavigatioService(Func<Type, ViewModel> viewModelFactory)
{
this.Factory = factory;
}
// The type of generic type parameter 'TViewModel'
// is the parameter for the factory delegate
public void NavigateTo<TViewModel>() where TViewModel : ViewModel
{
ViewModel viewModel = this.Factory.Invoke(typeof(TViewModel));
this.CurrentView = viewModel;
}
}
public App()
{
IServiceCollection _services = new ServiceCollection();
_services.AddSingleton<MainViewModel>()
.AddSingleton<HomeViewModel>()
// Let the IoC contaiiner construct the NavigatioService
// and all its dependencies for you
.AddSingleton<INavigationService, NavigatioService>()
// Register the factory delegate as normal dependency,
// so that the IoC container can resolve it
.AddSingleton<Func<Type, ViewModel>(serviceProvider => viewModelType => serviceProvider.GetRequiredService(viewModelType));
_serviceProvider = _services.BuildServiceProvider();
}