Is it possible to use dependency injection in a standalone .NET Standard project without an ASP.NET Core web application project?
Since there is no startup.cs
file, I am curious to find out if this is possible, and if it is how it can be done?
NET supports the dependency injection (DI) software design pattern, which is a technique for achieving Inversion of Control (IoC) between classes and their dependencies. Dependency injection in . NET is a built-in part of the framework, along with configuration, logging, and the options pattern.
Dependency injection (also known as DI) is a design pattern in which a class or object has its dependent classes injected (passed to it by another class or object) rather than create them directly. Dependency injection facilitates loose coupling and promotes testability and maintenance.
Dependency Injection is done by supplying the DEPENDENCY through the class's constructor when creating the instance of that class. The injected component can be used anywhere within the class. Recommended to use when the injected dependency, you are using across the class methods.
Dependency Injection (DI) is a software design pattern. It allows us to develop loosely-coupled code. The intent of Dependency Injection is to make code maintainable. Dependency Injection helps to reduce the tight coupling among software components.
Yes, you absolutely can do this. In fact, even if your class libraries were in the same assembly as an ASP.NET Core implementation, it’s preferred to make your libraries entirely unaware of both your specific dependencies and any dependency injection container you’re using.
TL;DR: Both your dependencies and any dependency injection container you’re using should be configured in your application’s composition root (e.g.,
Startup
), not in your library. Read on for details.
First of all, it's useful to remember that, fundamentally, dependency injection is a set of patterns and practices for achieving loosely coupled code, and not a specific dependency injection container. That's important here as you don't want your libraries to become dependent upon whatever dependency injection container you may choose to use, such as the one built-in to ASP.NET Core. The container is simply a tool that helps automate the construction, management, and injection of dependencies into your libraries.
Given that, your .NET Standard Class Library should be written to allow dependencies to be injected from whatever application it is being implemented in, while being completely agnostic to what dependencies or even container is being used. Typically that is done through constructor injection, in which dependencies are exposed as parameters in a class’s constructor. In that case, you just want to expose dependencies in your constructors through their abstractions—namely through an interface that describes the implementation.
These concepts are easier to understand with an example.
Let’s say you need access to some type of data persistence layer (such as a database) for users, but you don't want your class library to be tightly coupled to any single implementation (such as a SQL Server database).
In that case, you might create an IUserRepository
abstraction, which your concrete implementations can implement. Here’s a barebones example, which exposes a single GetUser()
method:
public interface IUserRepository
{
User GetUser(int userId);
}
Then, for any classes that depend on that service, you will implement a constructor which allows the IUserRepository
abstraction to be injected into it:
public class MyClass
{
private readonly IUserRepository _userRepository;
public MyClass(IUserRepository userRepository)
{
_userRepository = userRepository?? throw new ArgumentNullException(nameof(userRepository));
}
public string GetUserName(int userId) => _userRepository.GetUser(userId).Name;
}
Now, you can create a concrete implementation of the IUserRepository
—let us say a SqlUserRepository
.
public class SqlUserRepository: IUserRepository
{
private readonly string _connectionString;
public SqlUserRepository(string connectionString)
{
_connectionString = connectionString?? throw new ArgumentNullException(nameof(connectionString));
}
public GetUser(int userId)
{
//Implementation
}
}
Critically, this implementation could be in an entirely separate assembly; the assembly which contains IUserRepository
and MyClass
needn't be aware of it at all.
So where does the actual dependency injection happen? In whatever application that implements your .NET Standard Class Library. So, for instance, if you have an ASP.NET Core application, you might configure your dependency injection container via your Startup
class to inject an instance of SqlUserRepository
for any classes which depend upon an IUserRepository
:
public void ConfigureServices(IServiceCollection services)
{
//Register dependencies
services.AddScoped<IUserRepository>(c => new SqlRepository("my connection string"));
}
In that regard, your front-end application provides the glue between your .NET Standard Class Library and whatever services it depends upon.
Important: This could just as easily be a console application instead. It could use a third-party dependency injection container, such as Ninject. Or you could manually wire up your dependency graph in your composition root. What’s important here is that your .NET Standard class library doesn’t know or care, so long as something gives it its dependencies.
In the above example, your assembly structure might look something like this:
My.Library.dll
IUserRepository
MyClass
My.Library.Sql.dll
(references My.Library.dll
)
SqlUserRepository
My.Web.dll
(references My.Library.dll
,My.Library.Sql.dll
)My.Console.dll
(references My.Library.dll
,My.Library.Sql.dll
)Notice that My.Library
is completely unaware of either the concrete implementations (e.g., My.Library.Sql.dll
) or the applications which will implement it (e.g., My.Web.dll
, My.Console.dll
). All it aware of is that an IUserRepository
will be injected into MyClass
when it is constructed.
Important: These could all be in the same assembly. But even if they are, it’s useful to think of them as separate in order to maintain loose coupling between your dependency injection container, your core business logic, and your concrete implementations.
While not strictly required, a best practice is for your dependencies to be required parameters which are treated as read-only by your class. You can create a lot problems for yourself if you expose either optional dependencies, or dependencies which can be replaced during the lifetime of an object.
The above class structure demonstrates the ideal implementation by requiring the parameter:
public MyClass(IUserRepository userRepository)
Adding a guard clause to prevent null values:
_userRepository = userRepository?? throw new ArgumentNullException(nameof(userRepository));
And, finally, assigning it to a readonly
field so it can’t be replaced with a different implementation later:
private readonly IUserRepository _userRepository;
Let's assume you are writing a log reader library that fetches data from several sources:
public interface IDataProvider
{
Task<(IEnumerable<LogModel>, int)> FetchDataAsync(
int page,
int count,
string level = null,
string searchCriteria = null
);
}
And you have multiple implementations for the above interface:
public class SqlServerDataProvider : IDataProvider
{
private readonly RelationalDbOptions _options;
public SqlServerDataProvider(RelationalDbOptions options)
{
_options = options;
}
public async Task<(IEnumerable<LogModel>, int)> FetchDataAsync(
int page,
int count,
string logLevel = null,
string searchCriteria = null
)
{ ... }
And here is RelationalDbOptions
class
public class RelationalDbOptions
{
public string ConnectionString { get; set; }
public string TableName { get; set; }
public string Schema { get; set; }
}
Let's create a ServiceCollection
extension method to register dependencies:
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddSqlServerLogReader(
this IServiceCollection services,
Action<RelationalDbOptions> options
)
{
if (services == null)
throw new ArgumentNullException(nameof(services));
if (optionsBuilder == null)
throw new ArgumentNullException(nameof(optionsBuilder));
var relationalDbOptions = new RelationalDbOptions();
options.Invoke(relationalDbOptions );
services.AddSingleton(relationalDbOptions);
Services.AddScoped<IDataProvider, SqlServerDataProvider>();
return services;
}
}
Now if you want to use your log reader library with ASP.NET Core or Console application, you can register log reader dependencies ba calling AddSqlServerLogReader
in ConfigureServices
method or anywhere that you are creating ServiceCollection
:
public void ConfigureServices(IServiceCollection services)
{
...
services.AddSqlServerLogReader(options => {
options.ConnectionString = "";
options.LogTableName = ""
});
...
}
It's a common pattern to register library dependencies. Checkout real implementation here.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With