利用控制器和视图中的继承

Taking advantage of inheritance in Controllers and Views

我刚才在 codereview.stackexchange.com 上 posted this review...我觉得它可能更适合 Whosebug,因为它更像是一个问题而不是代码审查。

需要稍微解释一下,请多多包涵


我正在 ASP.NET MVC 中开发电子商务网站。用户可以在网站上 post 不同类型的广告。

我正在使用继承来定义我的广告类型,这个问题是关于利用层次结构删除控制器和视图中的重复代码。

我有不同的广告类型:SimpleAdCarRealEstateRental

每个广告都派生自具有所有共同属性的 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; }
}

现在其他广告都来自这个基础class:

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

其他广告存储库继承自上述 class:

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 个控制器:SimpleAdControllerCarControllerRealEstateRentalController

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

CarControllerRealEstateRentalController 有相同的 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">$@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)

再次,我觉得我在每个视图中重复了很多代码。我试图通过将它们放在公共部分视图中来减少重复代码的数量。 不知道有没有更好的方法?

如果我误解了请原谅我,但如果你添加了一个通用的 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>();
    }
}

从技术上讲这是可能的。对于类似的实体你可以引入枚举并用它来表示你在controller中处理的实体类型。您可以创建通用视图来处理类似的广告(当然,您需要 show/hide 相应的 UI 元素,具体取决于模型广告类型。这是 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 相关代码中的继承最终会导致问题多于收益。代码变得更加复杂,难以维护和保持整洁。因此,将所有 ViewsControllers 分开更有意义,即使它们的代码彼此非常接近。您可以在 DI 服务(又名 business logic)或类似层下开始优化 "repeated code" 用法。

UI 级别的 repeated code 问题应该通过提取组件来解决(又名 controlspartial viewsview components)。控制器继承是可能的,但会使代码更难维护。

更多抽象 -> 更多抽象泄漏。

我有完整的解决方案如何使用表达式树从 EF 模型定义生成控制器

检查这个,控制器的代码在所有 "duplicated code" 被删除后的样子:

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

或者这个("Roles"可以在从AD导入"Users"时创建)

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

启动时的那些块配置了具有很多功能的完整控制器(例如行版本支持、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。事实上你问的是"how to write new DSL that defines controllers/pages in bigger bricks"。答案是——这很容易,但人们坚持使用通用语言是有原因的。因为是"general".

P.S。一个细节:如果您希望 "full controller" 在 运行 时可能是 contracted/configured,那么您将被迫自己解析 http 请求 - 并忽略 MS 参数绑定模型 - 这是因为 BindAttribute - 重要的绑定修饰符 - 不能是 "set up" 运行 时间简单的方法。对于许多人来说——即使他们在参数列表中放了 "int id"——也是太高的价格。即使拒绝 MS 参数绑定是非常合乎逻辑的:当你要神奇地配置整个控制器时,为什么还需要保持 MS 参数绑定魔法?