ASP.Net Core Вызов контроллера с другого контроллера

В моем решении ASP.NET Core MVC 6 у меня есть два набора контроллеров. Один набор содержит веб-страницы с их обычными видами. Другой набор содержит контроллеры API.

Чтобы избежать дублирования логики db, веб-контроллеры используют контроллеры API. В настоящее время я создаю экземпляр требуемого контроллера вручную, передавая ему DbContext в качестве аргумента конструктора. Это DbContext, заданный веб-контроллеру путем инъекции зависимостей.

Но всякий раз, когда я добавляю другой конструктор в контроллер API, мне нужно изменить все веб-контроллеры, которые используют этот контроллер API.

Как я могу использовать систему впрыска зависимостей, встроенную в ASP.Net 5, для создания экземпляра требуемого контроллера API для меня? Затем он автоматически заполнит требуемые параметры конструктора.

Одним из решений может быть перемещение логики db из контроллеров API на отдельный уровень и вызов из API и веб-контроллеров. Это не решило бы мою проблему, так как новый слой по-прежнему будет иметь одни и те же параметры, и я не поклонник ненужной проводки.

Другим решением было бы, чтобы веб-контроллеры обращались к API через веб-вызов, но это просто добавляет сложности в приложение.

Сегодня я делаю это:

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);
    }
}

И я надеюсь на что-то вроде этого: (Этот пример не работает.)

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);
}

Ответ 1

Чтобы иметь возможность использовать контроллер с другого контроллера, вам необходимо:

  1. Зарегистрируйте контроллер в Startup.cs ConfigureServices: services.AddTransient <Areas.Api.Controllers.FoobarController, Areas.Api.Controllers.FoobarController>();
  2. Вы должны передать контроллер, к которому хотите получить доступ, в качестве параметра ctor в главный контроллер.

Если вам нужен доступ к локальным свойствам в контроллере, таких как User или Url есть два способа сделать это.

Первый способ - использовать DI для получения экземпляра IHttpContextAccessor для доступа к User и IUrlHelper для доступа к объектам Url:

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;
    }
}

Второй способ - установить его в вызывающем контроллере:

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);
    }
}

Исходный код для контроллера ASP.Net Core MVC6 RC1 можно найти здесь. Тем не менее, он подвергается серьезной перезаписи для RC2, и вместе с этим свойства, которые должны быть скопированы, чтобы получить доступ к User и Url, изменятся.

Ответ 2

Как я могу использовать встроенную в ASP.Net 5 систему внедрения зависимостей для создания экземпляра необходимого мне контроллера API?

В вашем Startup.cs можете указать MVC зарегистрировать все ваши контроллеры как сервисы.

services.AddMvc().AddControllersAsServices();

Затем вы можете просто вставить нужный контроллер в ваш другой контроллер через механизм DI и вызвать его метод действия.

Ответ 3

Не делай этого. Переместите эту логику в другой компонент, который используется двумя контроллерами. Контроллер отправляется платформой в результате HTTP-вызова, это не ваша публичная поверхность API. В целом, ваши контроллеры должны использоваться как место, где HTTP-запрос преобразуется в бизнес-объекты. Операции над этими объектами следует делегировать другому уровню (особенно, если его необходимо использовать из более чем одного места в вашем приложении).

Ответ 4

Зачем нужен новый слой? Почему бы не взять объект в оба контроллера и вызвать метод для этого объекта. Контейнер DI мог разрешить зависимости этого нового объекта без дублирования проводки, не мог ли он?

то есть вы могли бы это сделать:

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);   
    }
}

//Повторите это для контроллера API

public class SharedComponent
{
    public SharedComponent(DBContext dbContext, AppEnvironment appEnvironment, UserManager userManager, RoleManager roleManager, 
        EmailSender emailSender, SmsSender smsSender)
    {
       ...Store in fields for later usage
    }
}

Ответ 5

Я должен согласиться с другими, что введение контроллера может быть не лучшим маршрутом. Главным образом потому, что он объединяет бизнес-логику с ASP.Net вместо того, чтобы рассматривать ее как устройство ввода-вывода, как, по моему мнению, должно быть.

Допустим, у нас есть интерфейс, который выглядит следующим образом:

public interface ICalculator {
    int Add(int left, int right);
}

и у нас есть реализация, которая хранит бизнес-логику:

public class MyCalculator : ICalculator {
    public int Add(int left, int right) => left + right;
}

Эту реализацию можно использовать в качестве фоновой службы, в том же процессе, что и приложение WPF, или в качестве контроллера ASP.NET WebAPI. Это будет выглядеть примерно так:

[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);
}

Если у этого контроллера есть зависимость от репозитория, вы также можете добавить этот интерфейс. Лично мне нравится определять коллекцию репозиториев (например, IUserRepository) и вставлять только то, что нужно, вместо всего DbContext.

public CalculatorController(ICalculator calculator, IDbContext db) { }

Нет ничего плохого в контроллере, который зависит не только от того, что он украшает. Просто убедитесь, что у вас есть набор тестов, которые утверждают различные вещи. Например, вы можете утверждать, что когда вызывается определенный метод контроллера, также вызывается конкретный метод на другом интерфейсе.

Лично я считаю этот подход более подходящим. Можно использовать определенные технологии, но они должны быть на расстоянии вытянутой руки от бизнес-правил. Разработчик должен уметь принимать бизнес-правила, управляющие определенной частью кода, и тривиально переходить от службы WCF к ASP.NET WebAPI.

Я лично участвовал в нескольких проектах, в которых нам приходилось переключаться с одной технологии базы данных на другую (с SQL Server на CouchDB), и в которой наши микро-сервисы должны были работать как отдыхающие сервисы веб-API вместо сервисов Windows. Если вы проектируете вещи таким образом, эти типы проектов становятся относительно тривиальными по сравнению с тем, как все обычно составляется.