Reputation: 7325
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?
Upvotes: 10
Views: 9953
Reputation: 7613
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;
Upvotes: 14
Reputation: 1880
As we know, a Class Library cannot execute own its own. You have to reference it from a console or an ASP.NET Core project—let’s call those executables—which will then call your library.
Your Dependency Injection configurations runs before the actual code in your class library. In a library, we don't have any entry point to configure the dependency injection container; we have to make one and then call it from our executable’s Startup
or Main
.
For example, EF Core is just a library, but it has an extension method (serving as an entry point) which allows you to configure it with DI:
public static IServiceCollection AddDbContext<TContext>(this IServiceCollection serviceCollection, Action<DbContextOptionsBuilder> optionsAction = null, ServiceLifetime contextLifetime = ServiceLifetime.Scoped, ServiceLifetime optionsLifetime = ServiceLifetime.Scoped) where TContext : DbContext
{
library configurations
}
You can take this same approach in your code.
Upvotes: 1
Reputation: 11554
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.
Upvotes: 3