Dependency injection (DI) is heavily used in ASP.NET Core and this post tries to explain how this technique is used. Dependency injection is a design pattern that makes code easier to maintain.
Dependency injection aims at making classes independent of their dependencies, a class is dependent on interfaces but the client decides on the version of interfaces that is sent to the class. The client has the control of instantiating an class (Inversion of Control), the class uses an interface and does not have to instantiate interfaces it depends on.
Dependency injection relies on interfaces rather than classes and this makes it easier to change the implementation of these interfaces just by plugging in another class that implements that interface. Dependencies is normally registered in a service container at startup and the framework takes care of creating new instances of these dependencies.
When you create a class with DI in ASP.NET Core, you add interfaces that your class needs in the constructor of the class. You can say that these interfaces is injected into the class. You can use these dependencies in your class without the need to create new instances of them, the framework handles the creation of dependencies.
Create a class with dependencies
We want to create a class that handles static pages on our website and this class depends on other classes in our project. We start by creating an interface for our class.
public interface IStaticPageRepository
{
Int32 Add(StaticPage post);
void Update(StaticPage post);
Int32 GetCountBySearch(string[] keywords);
StaticPage GetOneById(Int32 id);
StaticPage GetOneByConnectionId(byte connectionId);
StaticPage GetOneByPageName(string pageName);
IList<StaticPage> GetAll(string sortField, string sortOrder);
IList<StaticPage> GetAllActive(string sortField, string sortOrder);
IList<StaticPage> GetAllActiveLinks(string sortField, string sortOrder);
IList<StaticPage> GetBySearch(string[] keywords, Int32 pageSize, Int32 pageNumber, string sortField, string sortOrder);
Task<IList<NewsItem>> GetNewsFromRSS(string searchString);
Int32 DeleteOnId(Int32 id);
string GetValidSortField(string sortField);
string GetValidSortOrder(string sortOrder);
} // End of the interface
This interface defines the methods that our class needs to implement. In our class for static pages we will inject dependencies in the constructor. You can inject dependencies that comes from the ASP.NET framework and Interfaces that you have created in your project.
public class StaticPageRepository : IStaticPageRepository
{
#region Variables
private readonly IDatabaseRepository database_repository;
private readonly IHttpClientFactory client_factory;
#endregion
#region Constructors
/// <summary>
/// Create a new repository
/// </summary>
public StaticPageRepository(IDatabaseRepository database_repository, IHttpClientFactory client_factory)
{
// Set values for instance variables
this.database_repository = database_repository;
this.client_factory = client_factory;
} // End of the constructor
#endregion
#region Insert methods
public Int32 Add(StaticPage post)
{
// Create the int to return
Int32 idOfInsert = 0;
// Create the sql statement
string sql = "INSERT INTO dbo.static_pages (connected_to_page, link_name, title, main_content, meta_description, meta_keywords, "
+ "meta_robots, page_name, inactive, news_search_string, sort_value) "
+ "VALUES (@connected_to_page, @link_name, @title, @main_content, @meta_description, @meta_keywords, @meta_robots, "
+ "@page_name, @inactive, @news_search_string, @sort_value);SELECT CAST(SCOPE_IDENTITY() AS INT);";
// Create parameters
IDictionary<string, object> parameters = new Dictionary<string, object>();
parameters.Add("@connected_to_page", post.connected_to_page);
parameters.Add("@link_name", post.link_name);
parameters.Add("@title", post.title);
parameters.Add("@main_content", post.main_content);
parameters.Add("@meta_description", post.meta_description);
parameters.Add("@meta_keywords", post.meta_keywords);
parameters.Add("@meta_robots", post.meta_robots);
parameters.Add("@page_name", post.page_name);
parameters.Add("@inactive", post.inactive);
parameters.Add("@news_search_string", post.news_search_string);
parameters.Add("@sort_value", post.sort_value);
// Insert the post
this.database_repository.Insert<Int32>(sql, parameters, out idOfInsert);
// Return the id of the inserted item
return idOfInsert;
} // End of the Add method
#endregion
} // End of the class
IDatabaseRepository database_repository can be a MS SQL repository or a MySql repository that implements this interface, the client decides on the class to use. Our MsSqlRepository class depends on IOptions and these options need to be created by the client.
public class MsSqlRepository : IDatabaseRepository
{
#region Variables
private readonly DatabaseOptions options;
#endregion
#region Constructors
public MsSqlRepository(IOptions<DatabaseOptions> options)
{
// Set values for instance variables
this.options = options.Value;
} // End of the constructor
#endregion
} // End of the class
Register dependencies
We register our dependencies in a service container in the ConfigureServices method in the StartUp class. We do not need to register all dependencies that we need in our project, the framework register dependencies like IHostingEnvironment for example.
public void ConfigureServices(IServiceCollection services)
{
// Create database options (Injected in MsSqlRepository)
services.Configure<DatabaseOptions>(options =>
{
options.connection_string = configuration.GetSection("AppSettings")["ConnectionString"];
options.sql_retry_count = 1;
});
// Add clients (Registers IHttpClientFactory)
services.AddHttpClient();
// Register dependencies
services.AddSingleton<IDatabaseRepository, MsSqlRepository>();
services.AddTransient<IOperationTransient, Operation>();
services.AddScoped<IOperationScoped, Operation>();
// More code ...
} // End of the ConfigureServices method
Dependencies can be registered with 3 different lifetimes (Singleton, Scoped, Transient). I use the singleton lifetime for most of my repository classes, model classes is created when they are needed.
Service lifetimes
- Transient lifetime services are created each time they’re requested.
- Scoped lifetime services are created once per request.
- Singleton lifetime services are created once the first time they’re requested.
You can inject a singleton dependency in a scoped or a transient service and you can inject a scoped service in a transient service. It is not good practice to inject a scoped or transient dependency in a singleton service and it is not good practice to inject a transient dependency in a scoped service.
A singleton service should not have state variables that can change between requests, variables in a singleton service will always be static.