пользуясь наследованием в контроллерах и представлениях

Я разместил этот обзор на codereview.stackexchange.com некоторое время назад... Я чувствую, что он может быть более подходящим для stackoverflow, так как это больше вопрос, чем обзор кода.

Это займет немного объяснений, пожалуйста, потерпите меня.


Я занимаюсь разработкой сайта электронной коммерции в ASP.NET MVC. Пользователи могут размещать на сайте рекламу разных типов.

Я использую наследование для определения типов моих объявлений, и этот вопрос касается использования преимуществ иерархической структуры для удаления повторяющегося кода в контроллерах и представлениях.

У меня разные типы объявлений: SimpleAd, Car и RealEstateRental.

Каждое объявление происходит от AdBase, который имеет все общие свойства:

public abstract class AdBase
{
    public long AdBaseId { get; set; }
    public bool IsActive { get; set; }
    public long UserId { get; set; }
    public string Title { get; set; }
    public short AdDurationInDays { get; set; }
    public string PhotosFolder { get; set; }
}

Теперь другие объявления получены из этого базового класса:

public class SimpleAd : AdBase
{
    public decimal Price { get; set; }
}

public class Car : AdBase
{
    public decimal Price { get; set; }
    public string Make { get; set; }
}

public class RealEstateRental : AdBase
{
    public decimal WeeklyRent { get; set; }
    public DateTime AvailableFrom { get; set; }
    public short NoOfBedrooms { get; set; }
    public short NoOfBathrooms { get; set; }
}

Я использую Entity Framework для взаимодействия с базой данных и использую шаблоны единиц работы и репозитория:

У меня есть общий AdBaseRepository со всеми распространенными методами рекламы:

public abstract class AdBaseRepository<TEntity> where TEntity : AdBase
{
    protected readonly ApplicationDbContext Context;

    public AdBaseRepository(ApplicationDbContext context)
    {
       Context = context; 
    }

    public TEntity Get(long adBaseId)
    {
        return Context.AdBase.OfType<TEntity>()
                  .Where(r => r.IsActive == true && r.AdBaseId == adBaseId)
                  .FirstOrDefault();
    }

    // more common methods here...
}

Другие рекламные репозитории наследуются от вышеуказанного класса:

public class SimpleAdRepository : AdBaseRepository<SimpleAd>
{
    public SimpleAdRepository(ApplicationDbContext context) : base(context)
    {
    }
}

public class CarRepository : AdBaseRepository<Car>
{
    public CarRepository(ApplicationDbContext context) : base(context)
    {
    }

    // methods which apply only to car here...
}

И это моя единица работы:

public class UnitOfWork
{
    protected readonly ApplicationDbContext Context;

    public UnitOfWork(ApplicationDbContext context)
    {
        Context = context;
        SimpleAd = new SimpleAdRepository(Context);
        RealEstateRental = new RealEstateRentalRepository(Context);
        Car = new CarRepository(Context);
    }

    public SimpleAdRepository SimpleAd { get; private set; }
    public RealEstateRentalRepository RealEstateRental { get; private set; }
    public CarRepository Car { get; private set; }

    public int SaveChanges()
    {
        return Context.SaveChanges();
    }

    public void Dispose()
    {
        Context.Dispose();
    }
}

Я доволен всем до сих пор... но проблема в том, что я не знаю, как я могу использовать эту иерархию наследования в моих контроллерах и представлениях.

На данный момент у меня есть 3 контроллера: SimpleAdController, CarController и RealEstateRentalController:

public class SimpleAdController : ControllerBase
{
    private UnitOfWork _unitOfWork;

    public SimpleAdController(UnitOfWork unitOfWork)
    {
        _unitOfWork = unitOfWork;
    }

    [HttpGet]
    // display specific ad
    public ActionResult Display(long id)
    {
        SimpleAd simpleAd = _unitOfWork.SimpleAd.Get(id);
        /* 
         * I have not included my ViewModel Classes in this question to keep
         * it small, but the ViewModels follow the same inheritance pattern
         */
        var simpleAdDetailsViewModel = Mapper.Map<SimpleAdDetailsViewModel>(simpleAd);
        return View(simpleAdDetailsViewModel);
    }
}

CarController и RealEstateRentalController имеют одинаковый метод Display, за исключением того, что тип объявления отличается (например, в CarController меня есть):

    public ActionResult Display(long id)
    {
        Car car = _unitOfWork.Car.Get(id);
        var carViewModel = Mapper.Map<CarViewModel>(car);
        return View(car);
    }

Чего я хотел добиться, так это создать AdBaseController чтобы в него помещались все распространенные методы, что-то вроде этого:

public class AdBaseController : ControllerBase
{
    private UnitOfWork _unitOfWork;

    public AdBaseController(UnitOfWork unitOfWork)
    {
        _unitOfWork = unitOfWork;
    }

    // Display for generic ad type
    [HttpGet]
    public ActionResult Display(long id)
    {
        // SimpleAd simpleAd = _unitOfWork.SimpleAd.Get(id);
        /* 
         * I need to replace the above line with a generic ad type... 
         * something like: _unitOfWork<TAd>.GenericAdRepository.Get(id)
         */

        // var simpleAdDetailsViewModel = Mapper.Map<SimpleAdDetailsViewModel>(simpleAd);
        // return View(simpleAdDetailsViewModel);
        /* 
         * similarly I have to replace the above 2 lines with a generic type
         */
    }
}

Если я сделаю вышеизложенное, то мои контроллеры рекламы могут наследовать его, и мне не нужно повторять один и тот же метод отображения для каждого из них... но тогда мне нужно сделать свой UnitOfWork универсальным... или иметь 2 UoW (общий и не общий)... который я не уверен, если это хорошая идея? Любая рекомендация о наличии AdBaseController?


Точно так же я повторяю много кода в моих представлениях. Например, это дисплей SimpleAdView:

<div class="row">
    <div class="col-l">
        @*this partial view shows Ad photos and is common code for all ad types*@
        @Html.Partial("DisplayAd/_Photos", Model)
    </div>
    <div class="col-r">
        <div class="form-row">
            @*Common in all ads*@
            <h5>@Model.Title</h5>
        </div>

        @*showing ad specific fields here*@
        <div class="form-row">
            <h5 class="price">[email protected]</h5>
        </div>

        @*Ad heading is common among all ad types*@
        @Html.Partial("DisplayAd/_AdBaseHeading", Model)
    </div>
</div>
@*Ad Description is common among all ad types*@
@Html.Partial("DisplayAd/_Description", Model)

И это мой дисплей CarView:

<div class="row">
    <div class="col-l">
        @*Common in all ads*@
        @Html.Partial("DisplayAd/_Photos", Model)
    </div>
    <div class="col-r">
        <div class="form-row">
            @*Common in all ads*@
            <h5>@Model.Title</h5>
        </div>

       @*Price and Make are specific to Car*@ 
        <div class="form-row">
            <h5 class="price">[email protected]</h5>
        </div>
        <div class="form-row">
            <h5 class="make">@Model.Make</h5>
        </div>

        @*Common in all ads*@ 
        @Html.Partial("DisplayAd/_AdBaseHeading", Model)
    </div>
</div>
@*Common in all ads*@
@Html.Partial("DisplayAd/_Description", Model)

Опять же, я чувствую, что повторяю много кода в каждом представлении. Я попытался уменьшить количество повторяющихся кодов, поместив их в общие частичные представления. Я не уверен, есть ли лучший способ сделать это?

Ответ 1

Технически это возможно. Для подобных объектов вы можете ввести перечисление и использовать его для указания того, с каким типом сущности вы имеете дело с controller. Вы можете создать общий вид для обработки похожих объявлений (но, конечно, вам нужно будет отображать/скрывать соответствующие элементы пользовательского интерфейса в зависимости от типа модели объявления). это псевдо-код для controller чтобы проиллюстрировать идею:

using System.Threading.Tasks;
using AutoMapper;
using MyNamespace.Data;
using Microsoft.AspNetCore.Mvc;
using MyNamespace.ViewModels;

namespace MyNamespace
{
    public enum AdType
    {
        [Description("Simple Ad")]
        SimpleAd = 0,

        [Description("Car")]
        Car = 1,

        [Description("Real Estate Rental")]
        RealEstateRental = 2
    }

    public class AdController : Controller
    {
        private readonly ApplicationDbContext _context;
        private readonly IMapper _mapper;

        public AdController(
            ApplicationDbContext context,
            IMapper mapper)
        {
            _context = context;
            _mapper = mapper;
        }

        [HttpGet("Ad/{type}")]
        public IActionResult Index(AdType? type = AdType.SimpleAd)
        {
            switch (type)
            {
                case AdType.RealEstateRental:
                    return RedirectToAction("RealEstateRental");
                case AdType.Car:
                    return RedirectToAction("Car");
                case AdType.SimpleAd:
                default:
                    return RedirectToAction("SimpleAd");
            }
        }

        [HttpGet("Ad/Car")]
        public IActionResult Car()
        {
            return View("Index", AdType.Car);
        }

        [HttpGet("Ad/RealEstateRental")]
        public IActionResult RealEstateRental()
        {
            return View("Index", AdType.RealEstateRental);
        }

        [HttpGet("Ad/SimpleAd")]
        public IActionResult SimpleAd()
        {
            return View("Index", AdType.SimpleAd);
        }

        [HttpGet("Ad/List/{type}")]
        public async Task<IActionResult> List(AdType type)
        {
            // var list = ... switch to retrieve list of ads via switch and generic data access methods 
            return list;
        }

        [HttpGet("Ad/{type}/Details/{id}")]
        public async Task<IActionResult> Details(AdType type, int id)
        {
            var ad = // ... switch by type to retrieve list of ads via switch and generic data access methods
            if (ad == null) return NotFound($"Ad not found.");

            // for instance - configure mappings via Automapper from DB entity to model views
            var model = _mapper.Map<AdViewModel>(ad);

            // Note: view will have to detect the exact ad instance type and show/hide corresponding UI fields
            return View(model);
        }

        [HttpGet("Ad/{type}/Add/")]
        public IActionResult Add(AdType type)
        {
            var ad = // ... switch by type to validate/add new entity  

            return View(_mapper.Map<AdEditModel>(ad));
        }

        [HttpPost("Ad/{type}/Add/")]
        public async Task<IActionResult> Add(AdEditModel model)
        {
            // detect ad type and save 
            return View(model);
        }

        [HttpGet("Ad/{type}/Edit/{id}")]
        public async Task<IActionResult> Edit(AdType type, int id)
        {
            // similar to Add
            return View(model);
        }

        [HttpPost("Ad/{type}/Edit/{id}")]
        public async Task<IActionResult> Edit(AdEditModel model)
        {
            // similar to Add
            return View(model);
        }

        // And so on
    }
}

Но я должен отметить, что наследование кода, связанного с UI, в конечном итоге приводит к большим проблемам, чем к преимуществам. Код становится более сложным для поддержания и поддержания его в чистоте. Поэтому имеет смысл хранить все ваши Views и Controllers отдельно, даже если они имеют код очень близко друг к другу. Вы можете начать оптимизацию использования "повторного кода" ниже ваших услуг DI (например, business logic) или аналогичного уровня.

repeated code проблема repeated code для уровня пользовательского интерфейса должна быть решена путем извлечения компонентов (например, controls, partial views, view components). Наследование контроллера возможно, но сделать код сложнее в обслуживании.

Ответ 2

Больше абстракции → больше утечек абстракции.

У меня есть полное решение, как генерировать контроллеры из определения модели EF с использованием деревьев exression

Проверьте, как выглядит код контроллера после удаления всего "дублированного кода":

https://github.com/DashboardCode/Routines/blob/master/AdminkaV1/Injected.AspCore.MvcApp/Controllers/UsersController.cs

или это ("Роли" могут быть созданы, когда "Пользователи" были импортированы из AD)

https://github.com/DashboardCode/Routines/blob/master/AdminkaV1/Injected.AspCore.MvcApp/Controllers/RolesController.cs

Эти блоки при запуске настраивают полный контроллер с множеством функций (например, поддержка rowversion, парсеры с ограничениями на SQL-серверы и т.д., Поддержка от одного до многих, многие-ко-многим, необработанные разрешения)

static ControllerMeta<User, int> meta = new ControllerMeta<User, int>(
            // how to find entity by "id"      
            findByIdExpression: id => e => e.UserId == id,
            // how to extract "id" from http responce      
            keyConverter: Converters.TryParseInt,
            // configure EF includes for Index page
            indexIncludes: chain => chain
                       .IncludeAll(e => e.UserPrivilegeMap)
            // ... and so on, try to read it

Но эти определения на самом деле являются своего рода новой внутренней DSL. На самом деле вы спрашиваете "как написать новый DSL, который определяет контроллеры/страницы в больших кирпичах". Ответ - это легко, но есть причина, почему люди придерживаются языков общего назначения. Это потому, что оно "общее".

PS Одна деталь: если вы хотите, чтобы "полный контроллер" мог быть сжат/сконфигурирован во время выполнения, поэтому вы вынуждены самостоятельно анализировать HTTP-запросы самостоятельно и игнорировать модель привязки параметров MS - это потому, что BindAttribute - важный связующий модификатор - t "установить" время работы простым способом. Для многих людей - даже когда они теряют "int id" в списке параметров - это слишком высокая цена. Даже если отказ в привязке параметров MS очень логичен: зачем вам нужно, чтобы параметры MS привязывали магию, когда вы собираетесь настраивать весь контроллер магически?

Ответ 3

Простите меня, если я неправильно понял, но если вы добавили genric UOW, мне кажется, что вы можете сделать что-то вроде этого: я не понимаю, почему было бы плохо делать это

public class AdBaseController : ControllerBase
{
    private IUnitOfWork _unitOfWork;

    public AdBaseController(IUnitOfWork unitOfWork)
    {
        _unitOfWork = unitOfWork;
    }

    public ActionResult GetDisplayAction<TAd, TViewModel>(long id)
    {
        SimpleAd simpleAd = _unitOfWork<TAd>.GenericAdRepository.Get(id)
        var viewModel = Mapper.Map<TViewModel>(simpleAd);         
        return View(viewModel);
    }
}

public class SimpleAdController : ControllerBase
{    
    public SimpleAdController(IUnitOfWork unitOfWork) : base(unitOfWork)
    {
    }

    [HttpGet]
    public ActionResult Display(long id)
    {
        return GetDisplayAction<AdType, ViewModelType>();
    }
}