ASP.NET Core comes with thin but rich and powerful built-in dependency injection mechanism we can use to inject instances to controllers and some other parts of web applications. Although constructor injection is the most famous dependency injection method for controllers there is also dependency injection to controller actions available. This post introduces controller action injection and shows how to benefit from it.
Constuctor injection with controllers
Controller action injection is great feature to use with controllers where most of actions need some specific service that others don’t need. In ASP.NET Core it’s traditional to use constructor injection. Let’s take a look at the following controller.
public class HomeController : Controller
{
private readonly ICustomerDAO _customerDao;
private readonly ISmsService _smsService;
private readonly IVehicleTelemetryService _telemetryService;
private readonly IMailService _mailService;
public HomeController(ICustomerDAO customerDao,
ISmsService smsService,
IMailService mailService,
IVehicleTelemetryService telemetryService)
{
_customerDao = customerDao;
_smsService = smsService;
_mailService = mailService;
_telemetryService = telemetryService;
}
public IActionResult Index(int page = 1)
{
var customers = _customerDao.ListCustomers(page: 1, pageSize: 25);
return View(customers);
}
public IActionResult SendSms(int id, string text)
{
var customer = _customerDao.GetById(id);
_smsService.Send(customer.Phone, text);
}
public IActionResult SendMail(int id, string subject, string body)
{
var customer = _customerDao.GetById(id);
_mailService.Send(customer.Email, subject, body);
}
public IActionResult GetCoordinates(int id)
{
var coordinates = _telemetryService.GetCoordinates(id);
return Json(coordinates);
}
}
It seems all okay but when we think few steps further we can see that part of this class can easily grow messy as over time we probably inject more services to controller.
public class HomeController : Controller
{
private readonly ICustomerDAO _customerDao;
private readonly ISmsService _smsService;
private readonly IVehicleTelemetryService _telemetryService;
private readonly IMailService _mailService;
/*
More injected services here
*/
public HomeController(ICustomerDAO customerDao,
ISmsService smsService,
IMailService mailService,
/* More injected services here */
IVehicleTelemetryService telemetryService)
{
_customerDao = customerDao;
_smsService = smsService;
_mailService = mailService;
_telemetryService = telemetryService;
/*
More injected services here
*/
}
// Controller actions follow
}
But at same time we have services used only by one controller action and there is no reason to have these services available in class scope. Action scope would be enough for us.
Injecting services to controller action
Solution to our problem is called controller action injection. It means we can inject services to controller action instead of controller constructor. When we add some service to action parameters list we confuse MVC as it doesn’t know by default that we want something from services. We have to apply FromServicesAttribute to action parameters to tell MVC that this parameter is coming from dependency-injection. As a sample let’s use controller action injection with SendSms() method of sample controller.
public IActionResult SendSms(int id, string text, [FromServices]ISmsService smsService)
{
var customer = _customerDao.GetById(id);
smsService.Send(customer.Phone, text);
}
But SendSms() is not the only action that needs instance of some service that doesn’t actions don’t use. We have also SendMail() and GetCoordinates() actions. After moving to controller action injection our class looks smaller and cleaner.
public class HomeController : Controller
{
private readonly ICustomerDAO _customerDao;
public HomeController(ICustomerDAO customerDao)
{
_customerDao = customerDao;
}
public IActionResult Index(int page = 1)
{
var customers = _customerDao.ListCustomers(page: 1, pageSize: 25);
return View(customers);
}
public IActionResult SendSms(int id, string text, [FromServices]ISmsService smsService)
{
var customer = _customerDao.GetById(id);
smsService.Send(customer.Phone, text);
}
public IActionResult SendMail(int id, string subject, string body, [FromServices]IMailService mailService)
{
var customer = _customerDao.GetById(id);
mailService.Send(customer.Email, subject, body);
}
public IActionResult GetCoordinates(int id, [FromServices]IVehicleTelemetryService telemetryService)
{
var coordinates = telemetryService.GetCoordinates(id);
return Json(coordinates);
}
}
We leave ICustomerDao to class level and inject it using controller constructor injection because this class is used my multiple controller action. Our class is now cleaner and we don’t have growing pile of contructor arguments anymore. It also makes it easier to write tests for controllers as we don’t have to initialize controller with all dependencies it is possibly using.
Wrapping up
Controller action injection is useful feature that helps us keep services used by one action away from class scope. This way we don’t pile injected services to class level and our controller classes are smaller. If service is used by more than one or few actions it is good idea to have reference to it at class level. Otherwise we can keep controller classes smaller by using controller action injection.
View Comments (6)
Good article. Controller action injection is great when a service is only required by one action. One point to note however is if the reasoning behind using controller action injection is to neaten up "messy" dependencies because there are too many, it often is because that controller may be doing too much and breaking SRP. Consider breaking the controller into separate ones that handle certain types of user interactions. i.e. GetCoordinates could belong in a different controller
I think KevDev has a point. This is solving a problem that could be better solved by breaking the controller down.
Controller doesn't have to be too big. It may have short actions but still need load of instances to be injected.
It's and interesting idea, that could help some controllers loose weight, but do you have any more detailed examples?
How would you deal with error/ exception handling on these actions? Taking the SendSms as an example what happens when the customer doesn't exist or phone number isn't valid, text is to long and so on.
Should these issues be dealt with by the smsService and if so whats a typical return value the the controller can deal with?
It comes down to design of services classes. I prefer services to be able to handle expected error situations on their own and if needed then communicate problems using status codes or return values. If it's exception - something that shouldn't happen in the case of normal code flow - then it should be taken care by controller (or base controller).
You helped me yet again!