taking advantage of inheritance in Controllers and Views
Technically it is possible. For similar entities you can introduce enumeration and use it to indicate what entity type you deal with in controller
. You can create generic view to handle similar ads (but of course you will need to show/hide corresponding UI elements depending on the model ad type). this is the pseudo code for controller
to illustrate the idea:
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
}
}
But I should note, that inheritance in UI related code eventually results into more problems than benefits. The code becomes more complex to maintain and keep it clean. So it makes more sense to keep all your Views
and Controllers
separate, even if they have the code very close to each other. You could start optimiziong the "repeated code" usage below the your DI services (aka business logic
) or similar layer.
The repeated code
problem for UI level should be solved via extracting components (aka controls
, partial views
, view components
). Controller inheritance is possible but make code harder to maintain.
More abstraction -> more abstraction leaks.
I have complete solution how to generate controllers from EF model definition using exression trees
Check this, how the controller's code looks after all "duplicated code" is removed:
https://github.com/DashboardCode/Routines/blob/master/AdminkaV1/Injected.AspCore.MvcApp/Controllers/UsersController.cs
or this ("Roles" can be created when "Users" was imported from AD )
https://github.com/DashboardCode/Routines/blob/master/AdminkaV1/Injected.AspCore.MvcApp/Controllers/RolesController.cs
Those blocks on start configures full controller with a lot of features (e.g. rowversion support, sql server constraints error parsers and etc., one-to many, many-to-many, unhandled expceptions support)
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
But those definitions actually is a kind of new Internal DSL. In fact you are asking "how to write new DSL that defines controllers/pages in bigger bricks". Answer is - it is easy, but there is a reason why people stick to general purpose languages. It is because it is "general".
P.S. One detail: if you want that "full controller" could be contracted/configured at run time, therefore you are forced to parse http requests by youself - and ignore MS parameters binding model - it is because BindAttribute
- important binding modificator - can't be "set up" run time simple way. For many people - even when they loose "int id" in parameters list - is too high price. Even if refusing of MS parameters binding is very logical: why would you need to keep MS parameters binding magic when you are going to configure whole controller magically?