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

时间:2018-09-11 22:18:00

标签: asp.net-mvc oop inheritance repository unit-of-work

前一段时间我在codereview.stackexchange.com上发布了this review ...我觉得它可能更适合stackoverflow,因为它比代码审查更成问题。

这需要一些解释,请忍受我


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

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

我有不同的广告类型: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; }
}

现在其他广告都从该基类派生:

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

同样,我觉得我在每个视图中都重复了很多代码。我试图通过将它们放在公共的局部视图中来减少重复代码的数量。 我不确定是否有更好的方法?

3 个答案:

答案 0 :(得分:4)

从技术上讲是可能的。对于相似的实体,您可以引入枚举并使用它来表示您在d = {'Value': [1, 2, 3, 4], 'Currency': ['CAD', 'USD', 'EUR', 'USD'], 'CAD': [1,1,1,1], 'USD': [1.3, 1.2, 1.4, 1.1], 'EUR': [1.4, 1.5, 1.4, 1.4]} df = pd.DataFrame(d) 中处理的实体类型。您可以创建通用视图来处理类似的广告(但是,根据模型广告类型,您当然需要显示/隐藏相应的UI元素)。这是controller的伪代码来说明这个想法:

controller

但是我应该指出,与UI相关的代码中的继承最终导致的问题多于收益。代码变得更加复杂,难以维护和保持整洁。因此,将所有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 } } Views分开是更有意义的,即使它们的代码彼此非常接近也是如此。您可以在DI服务(aka Controllers)或类似层下面开始优化“重复代码”的用法。

UI级别的business logic问题应通过提取组件(aka repeated codecontrolspartial views)来解决。控制器继承是可能的,但使代码难以维护。

答案 1 :(得分:4)

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

我有完整的解决方案,如何使用扩展树根据EF模型定义生成控制器

检查一下,删除所有“重复代码”后控制器的代码如何:

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

这些启动时的块会配置具有许多功能的完整控制器(例如,行版本支持,sql server约束错误解析器等,一对多,多对多,未处理的接收支持)

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参数绑定魔术呢?

答案 2 :(得分:0)

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