EF Core load data for subclasses

174 Views Asked by At

I have managed to load data for the subtypes ImageCell and TextCell of Cell.

public abstract class Cell : IAggregateRoot
{
    public int CellId { get; set; }
    public string CellType { get; set; }
    public int RowIndex { get; set; }
    public int ColIndex { get; set; }
    public int RowSpan { get; set; }
    public int ColSpan { get; set; }
    public int PageId { get; set; }
    public Page Page { get; set; }
}

public class TextCell : Cell
{
    public String Text { get; set; }
}

public class ImageCell : Cell
{
    public String Url { get; set; }
}

I added the discriminator value of CellType based on an Enum.

public enum CellEnum
{
    Cell,
    TextCell,
    ImageCell
}

In the DbContext.OnModelCreating() I configured it like this:

builder.Entity<Cell>()
    .HasDiscriminator(c => c.CellType)
    .IsComplete(false);

From then I think I went into bad design by creating a service per CellType.

Program.cs

builder.Services.AddScoped<IService<Cell>, GenericService<Cell>>();
builder.Services.AddScoped<IService<ImageCell>, GenericService<ImageCell>>();
builder.Services.AddScoped<IService<TextCell>, GenericService<TextCell>>();

IService.cs

namespace Core.Interfaces
{
    public interface IService<T> where T : class, IAggregateRoot
    {
        Task<T> GetByIdAsync(int id);
        Task<IEnumerable<T>> ListAsync();
        // Adapt to use specifications
        Task<IEnumerable<T>> ListAsyncWithSpec(Specification<T> spec);
        Task DeleteByIdAsync(int id);
        Task AddAsync(T t);
        Task UpdateAsyc(T t);
    }
}

GenericService is just an implementation for the repository using IService<T>.

I then created a DTO.

namespace API.DTOs
{
    public class CellDTO : DTO
    {        
        public int CellId { get; set; }
        public string CellType { get; set; }
        public int RowIndex { get; set; }
        public int ColIndex { get; set; }
        public int RowSpan { get; set; }
        public int ColSpan { get; set; }
        public int PageId { get; set; }
        // CellType specific properties
        public string Url { get; set; }
        public string Text { get; set; }
    }
}

And created a MappingProfile.

CreateMap<Cell, CellDTO>()
    .ForMember(dto => dto.Text, options => options.MapFrom(src => src.CellType.Equals(CellEnum.TextCell.ToString()) ? ((TextCell)src).Text : null))
    .ForMember(dto => dto.Url, options => options.MapFrom(src => src.CellType.Equals(CellEnum.ImageCell.ToString()) ? ((ImageCell)src).Url : null));

The Controller seems really messy now:

namespace API.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class CellsController : BaseController<Cell, CellDTO>
    {
        private readonly IService<ImageCell> _imageCellService;
        private readonly IService<TextCell> _textCellService;

        public CellsController(IService<Cell> service, IService<ImageCell> imageCellService, IService<TextCell> textCellService, IMapper mapper) : base(service, mapper)
        {
            _imageCellService = imageCellService;
            _textCellService = textCellService;
        }

        [HttpGet]
        public override async Task<ICollection<CellDTO>> Get()
        {
            var result = new List<CellDTO>();

            // Add ImageCells
            ICollection<ImageCell> imageCells = (ICollection<ImageCell>)await _imageCellService.ListAsync();
            result.AddRange(_mapper.Map<ICollection<ImageCell>, ICollection<CellDTO>>(imageCells));

            // Add TextCells
            ICollection<TextCell> textCells = (ICollection<TextCell>)await _textCellService.ListAsync();
            result.AddRange(_mapper.Map<ICollection<TextCell>, ICollection<CellDTO>>(textCells));

            return result;
        }
    }
}

How can I go about loading all the subclasses without duplicating this much code and using multiple IService<T> for each dedicated type?

Edit #1

I'm using Ardalis.Specification, but couldn't find anything usefull on that.

using Ardalis.Specification;
using Core.Entities.Cells;

namespace Core.Specifications
{
    public class CellSpecification : Specification<Cell>
    {
        public CellSpecification()
        {
            
        }
    }
}

Edit #2

Example output

[
  {
    "cellId": 1,
    "cellType": "ImageCell",
    "rowIndex": 0,
    "colIndex": 0,
    "rowSpan": 1,
    "colSpan": 1,
    "pageId": 1,
    "url": "http://bild0.png",
    "text": null
  },
  {
    "cellId": 2,
    "cellType": "ImageCell",
    "rowIndex": 0,
    "colIndex": 0,
    "rowSpan": 1,
    "colSpan": 1,
    "pageId": 2,
    "url": "http://bild1.png",
    "text": null
  },
  {
    "cellId": 3,
    "cellType": "ImageCell",
    "rowIndex": 0,
    "colIndex": 0,
    "rowSpan": 1,
    "colSpan": 1,
    "pageId": 3,
    "url": "http://bild2.png",
    "text": null
  },
  {
    "cellId": 4,
    "cellType": "ImageCell",
    "rowIndex": 0,
    "colIndex": 0,
    "rowSpan": 1,
    "colSpan": 1,
    "pageId": 4,
    "url": "http://bild3.png",
    "text": null
  },
  {
    "cellId": 5,
    "cellType": "ImageCell",
    "rowIndex": 0,
    "colIndex": 0,
    "rowSpan": 1,
    "colSpan": 1,
    "pageId": 5,
    "url": "http://bild4.png",
    "text": null
  },
  {
    "cellId": 6,
    "cellType": "TextCell",
    "rowIndex": 0,
    "colIndex": 1,
    "rowSpan": 1,
    "colSpan": 2,
    "pageId": 1,
    "url": null,
    "text": "Es ist..."
  },
  {
    "cellId": 7,
    "cellType": "TextCell",
    "rowIndex": 0,
    "colIndex": 1,
    "rowSpan": 1,
    "colSpan": 2,
    "pageId": 2,
    "url": null,
    "text": "Borja hockt..."
  },
  {
    "cellId": 8,
    "cellType": "TextCell",
    "rowIndex": 0,
    "colIndex": 1,
    "rowSpan": 1,
    "colSpan": 2,
    "pageId": 3,
    "url": null,
    "text": "..."
  },
  {
    "cellId": 9,
    "cellType": "TextCell",
    "rowIndex": 0,
    "colIndex": 1,
    "rowSpan": 1,
    "colSpan": 2,
    "pageId": 4,
    "url": null,
    "text": "«Das Versteck ..."
  },
  {
    "cellId": 10,
    "cellType": "TextCell",
    "rowIndex": 0,
    "colIndex": 1,
    "rowSpan": 1,
    "colSpan": 2,
    "pageId": 5,
    "url": null,
    "text": "Die Ameise...:"
  }
]
1

There are 1 best solutions below

0
On

Cell is an entity, so just run

var cells = await db.Set<Cell>().ToListAsync();

And remove or fix and simplify IService like this:

public interface IService<T> where T : class, IAggregateRoot
{
    Task<T> GetByIdAsync(int id);
    IQueryable<T> All();
    Task DeleteByIdAsync(int id);
    Task AddAsync(T t);
    Task UpdateAsyc(T t);
}

Which you can use like this

var cells = await cellService.All().ToListAsync();