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

я написал этот комментарий на 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 со всеми общими методами ad:

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

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

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
         */
    }
}

если я сделаю выше, то мои контроллеры Ad могут наследовать от него, и я не нужно повторять один и тот же метод отображения в каждом из них... но тогда мне нужно сделать мой UnitOfWork generic... или иметь 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">$@Model.Price</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">$@Model.Price</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)

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

3 ответов


технически это возможно. Для подобных сущностей вы можете ввести перечисление и использовать его, чтобы указать, с каким типом сущности вы имеете дело в 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 отдельные, даже если они имеют код очень близко друг к другу. Вы можете начать optimiziong "неоднократные кодекса" использование ниже своего Ди сервисы (ака business logic) или похожие слоя.

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


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

у меня есть полное решение, как генерировать контроллеры из определения модели 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 server и т. д., один-многим, многие-ко-многим, необработанное expceptions поддержки)

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

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


Простите меня, если я неправильно понял, но при условии, что вы добавили 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>();
    }
}