Dependency injection into controllers
By Steve Smith
ASP.NET Core MVC controllers should request their dependencies explicitly via their constructors. In some instances, individual controller actions may require a service, and it may not make sense to request at the controller level. In this case, you can also choose to inject a service as a parameter on the action method.
View or download sample code ((xref:)how to download)
Dependency Injection
Dependency injection is a technique that follows the Dependency Inversion Principle, allowing for applications to be composed of loosely coupled modules. ASP.NET Core has built-in support for dependency injection, which makes applications easier to test and maintain.
Constructor Injection
ASP.NET Core’s built-in support for constructor-based dependency injection extends to MVC controllers. By simply adding a service type to your controller as a constructor parameter, ASP.NET Core will attempt to resolve that type using its built in service container. Services are typically, but not always, defined using interfaces. For example, if your application has business logic that depends on the current time, you can inject a service that retrieves the time (rather than hard-coding it), which would allow your tests to pass in implementations that use a set time.
[!code-csharpMain]
1: using System;
2:
3: namespace ControllerDI.Interfaces
4: {
5: public interface IDateTime
6: {
7: DateTime Now { get; }
8: }
9: }
Implementing an interface like this one so that it uses the system clock at runtime is trivial:
[!code-csharpMain]
1: using System;
2: using ControllerDI.Interfaces;
3:
4: namespace ControllerDI.Services
5: {
6: public class SystemDateTime : IDateTime
7: {
8: public DateTime Now
9: {
10: get { return DateTime.Now; }
11: }
12: }
13: }
With this in place, we can use the service in our controller. In this case, we have added some logic to the HomeController
Index
method to display a greeting to the user based on the time of day.
[!code-csharpMain]
1: using ControllerDI.Interfaces;
2: using Microsoft.AspNetCore.Mvc;
3:
4: namespace ControllerDI.Controllers
5: {
6: public class HomeController : Controller
7: {
8: private readonly IDateTime _dateTime;
9:
10: public HomeController(IDateTime dateTime)
11: {
12: _dateTime = dateTime;
13: }
14:
15: public IActionResult Index()
16: {
17: var serverTime = _dateTime.Now;
18: if (serverTime.Hour < 12)
19: {
20: ViewData["Message"] = "It's morning here - Good Morning!";
21: }
22: else if (serverTime.Hour < 17)
23: {
24: ViewData["Message"] = "It's afternoon here - Good Afternoon!";
25: }
26: else
27: {
28: ViewData["Message"] = "It's evening here - Good Evening!";
29: }
30: return View();
31: }
32:
33: public IActionResult About([FromServices] IDateTime dateTime)
34: {
35: ViewData["Message"] = "Currently on the server the time is " + dateTime.Now;
36:
37: return View();
38: }
39:
40: public IActionResult Contact()
41: {
42: ViewData["Message"] = "Your contact page.";
43:
44: return View();
45: }
46:
47: public IActionResult Error()
48: {
49: return View();
50: }
51: }
52: }
If we run the application now, we will most likely encounter an error:
An unhandled exception occurred while processing the request.
InvalidOperationException: Unable to resolve service for type 'ControllerDI.Interfaces.IDateTime' while attempting to activate 'ControllerDI.Controllers.HomeController'.
Microsoft.Extensions.DependencyInjection.ActivatorUtilities.GetService(IServiceProvider sp, Type type, Type requiredBy, Boolean isDefaultParameterRequired)
This error occurs when we have not configured a service in the ConfigureServices
method in our Startup
class. To specify that requests for IDateTime
should be resolved using an instance of SystemDateTime
, add the highlighted line in the listing below to your ConfigureServices
method:
[!code-csharpMain]
1: using System.IO;
2: using ControllerDI.Interfaces;
3: using ControllerDI.Model;
4: using ControllerDI.Services;
5: using Microsoft.AspNetCore.Builder;
6: using Microsoft.AspNetCore.Hosting;
7: using Microsoft.Extensions.Configuration;
8: using Microsoft.Extensions.DependencyInjection;
9:
10: namespace ControllerDI
11: {
12: public class Startup
13: {
14: public Startup(IHostingEnvironment env)
15: {
16: var builder = new ConfigurationBuilder()
17: .SetBasePath(env.ContentRootPath)
18: .AddJsonFile("samplewebsettings.json");
19: Configuration = builder.Build();
20: }
21:
22: public IConfigurationRoot Configuration { get; set; }
23:
24: // This method gets called by the runtime. Use this method to add services to the container.
25: // For more information on how to configure your application, visit http://go.microsoft.com/fwlink/?LinkID=398940
26: public void ConfigureServices(IServiceCollection services)
27: {
28: // Required to use the Options<T> pattern
29: services.AddOptions();
30:
31: // Add settings from configuration
32: services.Configure<SampleWebSettings>(Configuration);
33:
34: // Uncomment to add settings from code
35: //services.Configure<SampleWebSettings>(settings =>
36: //{
37: // settings.Updates = 17;
38: //});
39:
40: services.AddMvc();
41:
42: // Add application services.
43: services.AddTransient<IDateTime, SystemDateTime>();
44: }
45:
46: // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
47: public void Configure(IApplicationBuilder app)
48: {
49: app.UseMvc(routes =>
50: {
51: routes.MapRoute(
52: name: "default",
53: template: "{controller=Home}/{action=Index}/{id?}");
54: });
55: }
56:
57: // Entry point for the application.
58: public static void Main(string[] args)
59: {
60: var host = new WebHostBuilder()
61: .UseKestrel()
62: .UseContentRoot(Directory.GetCurrentDirectory())
63: .UseIISIntegration()
64: .UseStartup<Startup>()
65: .Build();
66:
67: host.Run();
68: }
69: }
70: }
[!NOTE] This particular service could be implemented using any of several different lifetime options (
Transient
,Scoped
, orSingleton
). See Dependency Injection to understand how each of these scope options will affect the behavior of your service.
Once the service has been configured, running the application and navigating to the home page should display the time-based message as expected:
[!TIP] See Testing Controller Logic to learn how to explicitly request dependencies http://deviq.com/explicit-dependencies-principle/ in controllers makes code easier to test.
ASP.NET Core’s built-in dependency injection supports having only a single constructor for classes requesting services. If you have more than one constructor, you may get an exception stating:
An unhandled exception occurred while processing the request.
InvalidOperationException: Multiple constructors accepting all given argument types have been found in type 'ControllerDI.Controllers.HomeController'. There should only be one applicable constructor.
Microsoft.Extensions.DependencyInjection.ActivatorUtilities.FindApplicableConstructor(Type instanceType, Type[] argumentTypes, ConstructorInfo& matchingConstructor, Nullable`1[]& parameterMap)
As the error message states, you can correct this problem having just a single constructor. You can also replace the default dependency injection support with a third party implementation, many of which support multiple constructors.
Action Injection with FromServices
Sometimes you don’t need a service for more than one action within your controller. In this case, it may make sense to inject the service as a parameter to the action method. This is done by marking the parameter with the attribute [FromServices]
as shown here:
[!code-csharpMain]
1: using ControllerDI.Interfaces;
2: using Microsoft.AspNetCore.Mvc;
3:
4: namespace ControllerDI.Controllers
5: {
6: public class HomeController : Controller
7: {
8: private readonly IDateTime _dateTime;
9:
10: public HomeController(IDateTime dateTime)
11: {
12: _dateTime = dateTime;
13: }
14:
15: public IActionResult Index()
16: {
17: var serverTime = _dateTime.Now;
18: if (serverTime.Hour < 12)
19: {
20: ViewData["Message"] = "It's morning here - Good Morning!";
21: }
22: else if (serverTime.Hour < 17)
23: {
24: ViewData["Message"] = "It's afternoon here - Good Afternoon!";
25: }
26: else
27: {
28: ViewData["Message"] = "It's evening here - Good Evening!";
29: }
30: return View();
31: }
32:
33: public IActionResult About([FromServices] IDateTime dateTime)
34: {
35: ViewData["Message"] = "Currently on the server the time is " + dateTime.Now;
36:
37: return View();
38: }
39:
40: public IActionResult Contact()
41: {
42: ViewData["Message"] = "Your contact page.";
43:
44: return View();
45: }
46:
47: public IActionResult Error()
48: {
49: return View();
50: }
51: }
52: }
Accessing Settings from a Controller
Accessing application or configuration settings from within a controller is a common pattern. This access should use the Options pattern described in (xref:)configuration. You generally should not request settings directly from your controller using dependency injection. A better approach is to request an IOptions<T>
instance, where T
is the configuration class you need.
To work with the options pattern, you need to create a class that represents the options, such as this one:
[!code-csharpMain]
1: namespace ControllerDI.Model
2: {
3: public class SampleWebSettings
4: {
5: public string Title { get; set; }
6: public int Updates { get; set; }
7: }
8: }
Then you need to configure the application to use the options model and add your configuration class to the services collection in ConfigureServices
:
[!code-csharpMain]
1: using System.IO;
2: using ControllerDI.Interfaces;
3: using ControllerDI.Model;
4: using ControllerDI.Services;
5: using Microsoft.AspNetCore.Builder;
6: using Microsoft.AspNetCore.Hosting;
7: using Microsoft.Extensions.Configuration;
8: using Microsoft.Extensions.DependencyInjection;
9:
10: namespace ControllerDI
11: {
12: public class Startup
13: {
14: public Startup(IHostingEnvironment env)
15: {
16: var builder = new ConfigurationBuilder()
17: .SetBasePath(env.ContentRootPath)
18: .AddJsonFile("samplewebsettings.json");
19: Configuration = builder.Build();
20: }
21:
22: public IConfigurationRoot Configuration { get; set; }
23:
24: // This method gets called by the runtime. Use this method to add services to the container.
25: // For more information on how to configure your application, visit http://go.microsoft.com/fwlink/?LinkID=398940
26: public void ConfigureServices(IServiceCollection services)
27: {
28: // Required to use the Options<T> pattern
29: services.AddOptions();
30:
31: // Add settings from configuration
32: services.Configure<SampleWebSettings>(Configuration);
33:
34: // Uncomment to add settings from code
35: //services.Configure<SampleWebSettings>(settings =>
36: //{
37: // settings.Updates = 17;
38: //});
39:
40: services.AddMvc();
41:
42: // Add application services.
43: services.AddTransient<IDateTime, SystemDateTime>();
44: }
45:
46: // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
47: public void Configure(IApplicationBuilder app)
48: {
49: app.UseMvc(routes =>
50: {
51: routes.MapRoute(
52: name: "default",
53: template: "{controller=Home}/{action=Index}/{id?}");
54: });
55: }
56:
57: // Entry point for the application.
58: public static void Main(string[] args)
59: {
60: var host = new WebHostBuilder()
61: .UseKestrel()
62: .UseContentRoot(Directory.GetCurrentDirectory())
63: .UseIISIntegration()
64: .UseStartup<Startup>()
65: .Build();
66:
67: host.Run();
68: }
69: }
70: }
[!NOTE] In the above listing, we are configuring the application to read the settings from a JSON-formatted file. You can also configure the settings entirely in code, as is shown in the commented code above. See (xref:)Configuration for further configuration options.
Once you’ve specified a strongly-typed configuration object (in this case, SampleWebSettings
) and added it to the services collection, you can request it from any Controller or Action method by requesting an instance of IOptions<T>
(in this case, IOptions<SampleWebSettings>
). The following code shows how one would request the settings from a controller:
[!code-csharpMain]
1: using ControllerDI.Model;
2: using Microsoft.AspNetCore.Mvc;
3: using Microsoft.Extensions.Options;
4:
5: namespace ControllerDI.Controllers
6: {
7: public class SettingsController : Controller
8: {
9: private readonly SampleWebSettings _settings;
10:
11: public SettingsController(IOptions<SampleWebSettings> settingsOptions)
12: {
13: _settings = settingsOptions.Value;
14: }
15:
16: public IActionResult Index()
17: {
18: ViewData["Title"] = _settings.Title;
19: ViewData["Updates"] = _settings.Updates;
20: return View();
21: }
22: }
23: }
Following the Options pattern allows settings and configuration to be decoupled from one another, and ensures the controller is following separation of concerns, since it doesn’t need to know how or where to find the settings information. It also makes the controller easier to unit test Testing Controller Logic, since there is no static cling or direct instantiation of settings classes within the controller class.
|