I'm working on a fairly mundane ASP.NET MVC app that allows users to do basic CRUD operations on a number of different domain objects. I'm trying to minimize duplicate code and boring, monkey code as much as possible, so I'm using AutoMapper
to map types and MVC's scaffolding feature to spit out forms. I have a standard template for Index
, Details
, Create
, Edit
, Delete
, and DeleteConfirmed
views. Finally, I use specific view models to support these views (IndexModel
, RowModel
, DisplayModel
, and EditModel
).
I've written an abstract base class called StandardService
, which works with AutoMapper to provide support for mapping from entities to view models and back again. It has generic type parameters for the entity and each type of view model.
I began a refactoring of this class based on concern that it violated SRP (and had "too many" generic parameters), but I quickly realized this just meant a lot more classes and a lot more code. Yes, the classes would be more tightly focused, and there would no doubt be greater flexibility, but it didn't seem to be flexibility I needed at this point.
So, I reverted to the single base class. I'd like feedback on whether this is a sensible/practical use of inheritance or an anti-pattern/code smell. Is there a better solution that doesn't result in an explosion of classes?
StandardService`5
public class StandardService<TEntity, TIndexModel, TRowModel, TDisplayModel, TEditorModel> : ServiceBase
where TEntity : class, IEntity
where TIndexModel : IIndexModel<TRowModel>
where TDisplayModel : IDisplayModel
where TEditorModel : IEditorModel, new()
{
public StandardService(MainDbContext dbContext) : base(dbContext) { }
public virtual TIndexModel GetIndexModel(GridSortOptions sortOptions)
{
var processedEntities = GetProcessedEntities();
var rowModels = MapProcessedEntitiesToRowModels(processedEntities);
var processedRowModels = ProcessRowModels(rowModels);
processedRowModels = SortRowModels(processedRowModels, sortOptions);
var model = MapProcessedRowModelsToIndexModel(processedRowModels);
model.SortOptions = sortOptions;
return model;
}
protected virtual IEnumerable<TEntity> GetProcessedEntities()
{
return GetDbSet();
}
protected virtual IEnumerable<TRowModel> MapProcessedEntitiesToRowModels(IEnumerable<TEntity> processedEntities)
{
return Mapper.Map<IEnumerable<TRowModel>>(processedEntities);
}
protected virtual IEnumerable<TRowModel> ProcessRowModels(IEnumerable<TRowModel> rowModels)
{
return rowModels;
}
protected virtual IEnumerable<TRowModel> SortRowModels(IEnumerable<TRowModel> rowModels, GridSortOptions sortOptions)
{
if (sortOptions == null || string.IsNullOrWhiteSpace(sortOptions.Column)) return rowModels;
return rowModels.OrderBy(sortOptions.Column, sortOptions.Direction);
}
protected virtual TIndexModel MapProcessedRowModelsToIndexModel(IEnumerable<TRowModel> processedRowModels)
{
return Mapper.Map<TIndexModel>(processedRowModels);
}
public virtual TDisplayModel GetDisplayModel(int id)
{
var entity = FindOrThrow(id);
return MapEntityToDisplayModel(entity);
}
protected virtual TDisplayModel MapEntityToDisplayModel(TEntity entity)
{
return Mapper.Map<TDisplayModel>(entity);
}
public virtual TEditorModel GetEditorModel()
{
return new TEditorModel();
}
public virtual TEditorModel GetEditorModel(int id)
{
var entity = FindOrThrow(id);
return MapEntityToEditorModel(entity);
}
public virtual void UpdateModel(TEditorModel model) { }
public virtual TEntity AddAndSave(TEditorModel model)
{
var entity = MapEditorModelToEntity(model);
GetDbSet().Add(entity);
SaveChanges();
return entity;
}
protected virtual TEntity MapEditorModelToEntity(TEditorModel model)
{
return Mapper.Map<TEntity>(model);
}
public virtual TEntity EditAndSave(TEditorModel model)
{
var entity = FindOrThrow(model.Id);
MapEditorModelToEntity(model, entity);
SaveChanges();
return entity;
}
protected virtual TEditorModel MapEntityToEditorModel(TEntity entity)
{
return Mapper.Map<TEditorModel>(entity);
}
protected virtual void MapEditorModelToEntity(TEditorModel model, TEntity entity)
{
Mapper.Map(model, entity);
}
public virtual bool CanDelete(int id)
{
var entity = FindOrThrow(id);
return CanDelete(entity);
}
public virtual bool CanDelete(IEnumerable<TEntity> entities)
{
foreach (var entity in entities)
if (CanDelete(entity)) return false;
return true;
}
protected virtual bool CanDelete(TEntity entity)
{
return true;
}
public virtual void DeleteAndSave(int id)
{
var entity = FindOrThrow(id);
DeleteAndSave(entity);
}
public virtual void DeleteAndSave(TEntity entity)
{
Delete(entity);
SaveChanges();
}
public virtual void Delete(TEntity entity)
{
GetDbSet().Remove(entity);
}
public virtual TEntity Find(int id)
{
return GetDbSet().Find(id);
}
protected virtual TEntity FindOrThrow(int id)
{
return GetDbSet().FindOrThrow(id);
}
protected DbSet<TEntity> GetDbSet()
{
return DbContext.Set<TEntity>();
}
}
ServiceBase
public abstract class ServiceBase
{
private readonly MainDbContext _dbContext;
protected MainDbContext DbContext
{
get { return _dbContext; }
}
public ServiceBase(MainDbContext dbContext)
{
_dbContext = dbContext;
}
protected void SaveChanges()
{
_dbContext.SaveChanges();
}
}
LadderMatchService (example base class)
public class LadderMatchService : StandardService<LadderMatch, IndexModel, RowModel, DisplayModel, EditorModel>
{
private readonly NameFinderService _nameFinderService;
public LadderMatchService(MainDbContext dbContext, NameFinderService nameFinderService)
: base(dbContext)
{
_nameFinderService = nameFinderService;
}
protected override IEnumerable<RowModel> ProcessRowModels(IEnumerable<RowModel> rowModels)
{
return rowModels.OrderBy(model => model.ReportedOn);
}
public override void UpdateModel(EditorModel model)
{
model.ChallengerNames = _nameFinderService.GetExactNameOrSimilarNames(model.ChallengerNames.Single(), false);
model.ChallengeeNames = _nameFinderService.GetExactNameOrSimilarNames(model.ChallengeeNames.Single(), true);
}
}