The implementation of AddSingleton<T1, T2>
looks like this:
public static IServiceCollection AddSingleton<TService, TImplementation>(this IServiceCollection services)
where TService : class
where TImplementation : class, TService
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}
return services.AddSingleton(typeof(TService), typeof(TImplementation));
}
As you can see, it literally calls the other overload of AddSingleton
that passes two types instead. So semantically, it makes no difference whether you do AddSingleton<IDb, Db>()
or AddSingleton(typeof(IDb), typeof(Db))
. Both calls will result in the exact same result.
The reason that there is a generic overload is that it feels a lot nicer. Generic methods are preferred over passing types because you can just write it easier. So you are more likely to see the generic usage.
In addition, there is the benefit that you can add constraints to the the generic type arguments which may add some compile time checks. In this particular case, the constraints look like this:
where TService : class
where TImplementation : class, TService
Apart from the fact that both types are required to be reference types, there is the additional requirement that TImplementation
inherits from TService
. This makes sure that you can actually use instances of type TImplementation
in places where a TService
is expected. That is also the idea behind the Liskov Substitution Principle. By having a type constraint, this check is being verfied at compile time, so you can be sure that this will work at runtime where as this is not guaranteed if you use the other overload.
Needless to say, AddTransient<>
and AddScoped<>
work in the same way over their respective non-generic overloads.