Search code examples
c#asp.net-coreasp.net-core-webapidtotable-per-hierarchy

Best advice to Implement REST API for entities with Table Per Hierarchy model design (MVCCore + EF 2.2)


I have a couple of derived classes Shippable and Downloadable inheriting from the same base class Product and each of them have own properties:

    public abstract class Product : Entity<Guid>
    {
        public ProductType ProductType{ get; set; }

        public string Name { get; set; }
    }

    public class Downloadable : Product
    {
        public Guid DownloadId { get; set; }
    }

    public class Shippable : Product
    {
        public Guid ShippingInfo { get; set; }
    }

I used EF Core TBH design for my DataTable so all properties of this entities stored in a table:

            builder.Entity<Product>(
                b =>
                {
                    b.ToTable(
                        options.TablePrefix + "Products",
                        options.Schema
                    );

                    b.HasKey(
                        x => x.Id
                    );

                    b.HasDiscriminator<ProductType>(
                         nameof(Product.ProductType)
                     )
                     .HasValue<Downloadable>(
                         ProductType.Downloadable
                     )
                     .HasValue<Shippable>(
                         ProductType.Shippable
                     );
                }

My project is NLayered DDD so I have a application layer which produce business logics for UI/REST API methods and contains application services and DTOs.

My question is whats the best practice for apply this. What you think about this options:

1- Business services per derived class with derived DTOs

2- Only one service that can serve all with shared DTO (contains nullable properties of derived classes

3- Your suggestions

Thanks in advance.


Solution

  • I'm not entirely sure what you're looking for, but from an API view, I'd use an abstract product controller and then derive a controller for each specific product type from that:

    [Route("api/[controller]")]
    [ApiController]
    public abstract class ProductController<TProduct, TProductDTO> : ControllerBase
        where TProduct : Product, new()
        where TProductDTO : class, new()
    {
    }
    
    public class DownloadableController : ProductController<Downloadable, DownloadableDTO>
    {
    }
    
    public class ShippableController : ProductController<Shippable, ShippableDTO>
    {
    }
    

    Then, you build all the endpoints on your abstract product controller, utilizing the generic types TProduct and TProductDTO. For example, you might have something like:

    [HttpPost("")]
    public async Task<ActionResult<TProductResource>> Create(TProductDTO dto)
    {
        var product = _mapper.Map<TProduct>(dto);
        _context.Add(product);
        await _context.SaveChangesAsync();
        return CreatedAt("Get", new { product.Id }, _mapper.Map<TProductDTO>(product));
    }
    

    Because of the generic types, this same method works for all derived controllers, without needing to redefine or even override, although you can allow it to be overridden by adding the virtual keyword. It's generally better, though, to add placeholder methods. For example, you can do something like:

    private virtual Task BeforeCreateAsync(TProduct product, TProductDTO dto) =>
    

    Task.CompletedTask;

    private virtual Task AfterCreateAsync(TProduct product, TProductDTO dto) =>
    

    Task.CompletedTask;

    And then wrap your SaveChangesAsync call:

    _context.Add(product);
    await BeforeCreateAsync(product, dto);
    await _context.SaveChangesAsync();
    await AfterCreateAsync(product, dto);
    

    Then, in your derived controller(s), you can override these to stub in additional functionality:

    private override async Task BeforeCreateAsync(Downloadable product, DownloadableDTO dto)
    {
        // do something specific to downloadable products
    }
    

    That way, the majority of your logic remains self-contained. You can add as many such placeholder methods as make sense for your application.