Reputation: 12326
In my ASP.Net Core MVC 6 solution I have two sets of controllers. One set contains the webpages with their regular views. Another set contains the API controllers.
To avoid duplicating db logic the web controllers are using the API controllers. Currently I am creating an instance of the required controller manually by handing it a DbContext as constructor argument. This is the DbContext given to web controller by dependency injection.
But whenever I add another constructor parameter to the API controller I need to modify all web controllers that use this API controller.
How can I use the dependency injection system builtin to ASP.Net 5 to create an instance of the required API controller for me? Then it would fill in the required constructor parameters automatically.
One solution could be to move the db logic from the API controllers to a separate layer and call that from both API and web controllers. This would not solve my problem since the new layer would still need the same parameters and I'm not fan of the unnecessary wiring.
Another solution would be to have the web controllers access the API through a web call, but that just adds complexity to the app.
Today I am doing this:
public IActionResult Index()
{
using (var foobarController = new Areas.Api.Controllers.FoobarController(
// All of these has to be in the constructor of this controller so they can be passed on to the ctor of api controller
_dbContext, _appEnvironment,
_userManager, _roleManager,
_emailSender, _smsSender))
{
var model = new IndexViewModel();
model.Foo = foobarController.List(new FoobarRequest() { Foo = true, Bar = false });
model.Bar = foobarController.List(new FoobarRequest() { Foo = false, Bar = true });
return View(model);
}
}
And I am hoping for something like this: (This example does not work.)
using (var foobarController = CallContextServiceLocator.Locator.ServiceProvider.GetService<Areas.Api.Controllers.FoobarController>())
{
var model = new IndexViewModel();
model.Foo = foobarController.List(new FoobarRequest() { Foo = true, Bar = false });
model.Bar = foobarController.List(new FoobarRequest() { Foo = false, Bar = true });
return View(model);
}
Upvotes: 63
Views: 97747
Reputation: 70314
@B12Toaster is correct for MVC but if you only use ApiController
you should do it like this:
services.AddControllers().AddControllersAsServices();
Upvotes: 2
Reputation: 496
I'd have to agree with others that injecting the controller may not be the best route. Mostly because it marries the business logic with ASP.Net instead of treating it like an IO device like, in my opinion, it should be.
Let's say we have an interface that looks like this:
public interface ICalculator {
int Add(int left, int right);
}
and we have an implementation that stores the business logic:
public class MyCalculator : ICalculator {
public int Add(int left, int right) => left + right;
}
This implementation can be used as a background service, within the same process as a WPF application, or as an ASP.NET WebAPI controller. It would look something like this:
[ApiController]
[Route("api/{controller}")]
public void CalculatorController : Controller, ICalculator {
private readonly ICalculator _calculator;
public CalculatorController(ICalculator calc) => _calculator = calc;
[Route("Add")]
public int Add(int left, int right) => _calculator.Add(left, right);
}
If that controller has a dependency on a repository you can inject that interface too. Personally I like defining a collection of repositories (like IUserRepository for example) and injecting only what is needed instead of the entire DbContext.
public CalculatorController(ICalculator calculator, IDbContext db) { }
There's nothing wrong with a controller depending on more than just the thing it is decorating. Just make sure you have a set of tests that assert various things. For example you could assert that when a particular controller method is called the particular method on the other interface is also called.
Personally I find this approach a better fit. It's okay to use certain technologies but they should be kept at arm's length from the business rules. A developer should be able to take the business rules that govern a particular part of the code and switch from a WCF service to ASP.NET WebAPI trivially.
I've personally been a part of a couple projects where we had to switch from one database technology to another (SQL Server to CouchDB) and one where our micro-services needed to be running as restful Web API services instead of Windows services. If you architect things this way those types of projects become relatively trivial compared to how things are normally composed.
Upvotes: 0
Reputation: 38885
Don't do it. Move that logic to another component that gets shared between the 2 controllers. The controller is dispatched to by the framework as a result of an HTTP call, its not your public API surface. In general, your controllers should be used as a the place where the HTTP request is transformed into business objects. Operations on those objects should be delegate to another layer (especially if it needs to be used from more than one place in your application).
Upvotes: 40
Reputation: 15723
How can I use the dependency injection system builtin to ASP.Net 5 to create an instance of the required API controller for me?
In your Startup.cs
can tell the MVC to register all your controllers as services.
services.AddMvc().AddControllersAsServices();
Then you can simply inject the desired controller in your other controller via the DI mechanism and invoke its action method.
Upvotes: 49
Reputation: 32954
Why would your new layer need wiring up? Why not take in an object into both controllers and call a method on that object. The DI container could resolve the dependencies of this new object without duplicated wiring couldn't it?
ie you could have this:
public class MvcController
{
SharedComponent sharedComponent;
public MvcController(SharedComponent sharedComponent)
{
this.sharedComponent = sharedComponent;
}
public IActionResult Index()
{
var model = new IndexViewModel();
model.Foo = shredComponent.List(new FoobarRequest() { Foo = true, Bar = false });
model.Bar = shredComponent.List(new FoobarRequest() { Foo = false, Bar = true });
return View(model);
}
}
//Repeat this for the API controller
public class SharedComponent
{
public SharedComponent(DBContext dbContext, AppEnvironment appEnvironment, UserManager userManager, RoleManager roleManager,
EmailSender emailSender, SmsSender smsSender)
{
...Store in fields for later usage
}
}
Upvotes: 0
Reputation: 12326
To be able to use a controller from another controller you need to:
services.AddTransient <Areas.Api.Controllers.FoobarController, Areas.Api.Controllers.FoobarController>();
If you need to access local properties in the controller such as User
or Url
there are two ways to do this.
The first way is to use DI to get an instance of IHttpContextAccessor
to access User
and IUrlHelper
to access Url
objects:
public class FoobarController : Controller
{
private readonly ApplicationDbContext _dbContext;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IUrlHelper _urlHelper;
public FoobarController(ApplicationDbContext dbContext, IHttpContextAccessor httpContextAccessor, IUrlHelper _urlHelper, [...])
{
_dbContext = dbContext;
_httpContextAccessor = httpContextAccessor;
_urlHelper = urlHelper;
}
public FoobarResponse List(FoobarRequest request)
{
var userId = _httpContextAccessor.HttpContext.User.GetUserId();
var response = new FoobarResponse();
response.List = _dbContext.Foobars.Where(f => f.UserId == userId).ToList();
response.Thumb =
return response;
}
}
The second way is to set it in the calling controller:
public class HomeController : Controller
{
private Areas.Api.Controllers.FoobarController _foobarController;
public HomeController(Areas.Api.Controllers.FoobarController foobarController)
{
_foobarController = foobarController;
}
private void InitControllers()
{
// We can't set this at Ctor because we don't have our local copy yet
// Access to Url
_foobarController.Url = Url;
// Access to User
_foobarController.ActionContext = ActionContext;
// For more references see https://github.com/aspnet/Mvc/blob/6.0.0-rc1/src/Microsoft.AspNet.Mvc.ViewFeatures/Controller.cs
// Note: This will change in RC2
}
public IActionResult Index()
{
InitControllers();
var model = new IndexViewModel();
model.Foo = _foobarController.List(new FoobarRequest() { Foo = true, Bar = false });
model.Bar = _foobarController.List(new FoobarRequest() { Foo = false, Bar = true });
return View(model);
}
}
The source code for ASP.Net Core MVC6 RC1 Controller can be found here. It is however undergoing heavy rewrite for RC2 and with it the properties that has to be copied to get access to User and Url will change.
Upvotes: 19