I'm new to ef core development and this looked at eShopOnWeb for guidance. I adapted many things and ended up with a somewhat working API, where I just can't load related data. I have used Ardalis.Specification but ended up not being able to fix the issue. Originally I coded according to Ardalis and then adapted somewhat in Java Spring Patterns having and approach of using Conrtoller > Service > Repository > Entity.

In a strucuture like this:

.sln

 - Controllers
 - Core
 - Infrastructure

When fetching api/Book/ I get this result:

[{
  "bookId":1,
  "title":"Borja On Fleek \uD83D\uDD25",
  "description":"De jung Pirat Borja mischt die 7 Weltmeer uf",
  "coverImageUrl":"https://s3.amazonaws.com/[...].png",
  "chapters":null,
  "bookCategories":null,
  "bookTags":null
}]

This is fine as there is one book in the DB. But there are also:

  • 0 Chapters in the DB
  • 1 BookCategory in the DB
  • 2 BookTag in the DB
Entry BookId CategoryId
1 1 2
Entry BookId TagId
1 1 1
1 1 2

The corresponding entries in Tag and Category do exist.

I stuck to Microsofts' guide to relations in order to create Many-To-Ony (Chapters) and Many-To-Many (Tags, Categories).

The entites:

Book.cs

using Core.Interfaces;

namespace Core.Entities.BookAggregate
{
    public class Book :  IAggregateRoot
    {
        public int BookId { get; set; }
        public string Title { get; set; }
        public string Description { get; set; }
        public string CoverImageUrl { get; set; }
        public ICollection<Chapter> Chapters { get; set; }
        public ICollection<BookCategory> BookCategories { get; set; }
        public ICollection<BookTag> BookTags { get; set; }
    }
}

Category.cs

using Core.Entities.BookAggregate;
using Core.Interfaces;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Core.Entities.Aggregates
{
    public class Category : IAggregateRoot
    {
        public int CategoryId { get; set; }
        public string Name { get; set; }
        public int Order { get; set; }
        public string ImageUrl { get; set; }
        public ICollection<BookCategory> BookCategories { get; set; }
    }
}

BookCategory.cs

using Core.Entities.Aggregates;
using Core.Entities.BookAggregate;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Core.Entities
{
    public class BookCategory
    {
        public int BookId { get; set; }
        public Book Book { get; set; }
        public int CategoryId { get; set; }
        public Category Category { get; set; }
    }
}

As Tag.cs and BookTag.cs are equal they are omitted.

Chapter.cs

using Core.Entities.BookAggregate;
using Core.Interfaces;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Core.Entities
{
    public class Chapter : IAggregateRoot
    {
        public int ChapterId { get; set; }
        public string Title { get; set; }
        public int Ordinal { get; set; }
        public int BookId { get; set; }
        public Book Book { get; set; }
        public ICollection<Page> Pages { get; set; }
    }

}

The Controller is mostly abstracted, so for the BookController we have this code.

BookController.cs

using Core.Entities.BookAggregate;
using Core.Interfaces;
using Microsoft.AspNetCore.Mvc;

// For more information on enabling Web API for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860

namespace API.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class BookController : BaseController<Book>
    {
        public BookController(IService<Book> service) : base(service)
        {
        }

        [HttpPut("{id}")]
        public override async Task<IActionResult> Put(int id, [FromBody] Book book)
        {
            Book entity = await _service.GetByIdAsync(id);
            entity.Title = book.Title;
            entity.Chapters = book.Chapters;
            
            await _service.UpdateAsyc(entity);
            return Ok(entity);
        }
    }
}

Which inherits from BaseController.cs

using Core.Interfaces;
using Microsoft.AspNetCore.Mvc;

namespace API.Controllers
{
    public class BaseController<T> : Controller where T : class, IAggregateRoot
    {
        private protected readonly IService<T> _service;

        public BaseController(IService<T> service)
        {
            _service = service;
        }

        [HttpGet]
        public async Task<IEnumerable<T>> Get()
        {
            return await _service.ListAsync();
        }

        [HttpGet("{id}")]
        public async Task<T> Get(int id)
        {
            return await _service.GetByIdAsync(id);
        }

        [HttpPost]
        public async Task<IActionResult> Post([FromBody] T t)
        {
            await _service.AddAsync(t);
            return Ok(t);
        }

        [HttpPut("{id}")]
        public virtual async Task<IActionResult> Put(int id, [FromBody] T t)
        { 
            throw new NotImplementedException();
        }

        [HttpDelete("{id}")]
        public async Task<IActionResult> Delete(int id)
        {
            if (await _service.GetByIdAsync(id) == null)
                return NotFound();

            await _service.DeleteByIdAsync(id);
            return Ok();
        }
    }
}

The GenericService and IService are generics:

IService.cs

namespace Core.Interfaces
{
    public interface IService<T> where T : class, IAggregateRoot
    {
        Task<T> GetByIdAsync(int id);
        Task<IEnumerable<T>> ListAsync();
        Task DeleteByIdAsync(int id);
        Task AddAsync(T t);
        Task UpdateAsyc(T t);
    }
}

GenericService.cs

using Core.Interfaces;

namespace Core.Services
{
    public class GenericService<T> : IService<T> where T : class, IAggregateRoot
    {
        private readonly IRepository<T> _repository;
        private readonly IAppLogger<GenericService<T>> _logger;


        public GenericService(IRepository<T> repository, IAppLogger<GenericService<T>> logger)
        {
            _repository = repository;
            _logger = logger;
        }

        public async Task<T> GetByIdAsync(int id)
        {
            return await _repository.GetByIdAsync(id);
        }

        public async Task<IEnumerable<T>> ListAsync()
        {
            return await _repository.ListAsync();
        }

        public async Task DeleteByIdAsync(int id)
        {
            var t = await _repository.GetByIdAsync(id);

            if (t == null)
            {
                _logger.Error($"Element with id: {id} can not be found!");
                throw new ArgumentException($"Element with id: {id} can not be found!");
            }

            await _repository.DeleteAsync(t);
        }

        public async Task AddAsync(T t)
        {
            await _repository.AddAsync(t);
        }

        public async Task UpdateAsyc(T t)
        {
            await _repository.UpdateAsync(t);
        }
    }
}

IRepository.cs

using Ardalis.Specification;

namespace Core.Interfaces
{
    public interface IRepository<T> : IRepositoryBase<T> where T : class, IAggregateRoot
    {
    }
}

Everything is strapped-in in Program.cs.

using Core.Interfaces;
using Infrastructure.Data;
using Infrastructure.Logging;
using Microsoft.EntityFrameworkCore;
using Core.Services;
using Core.Entities.BookAggregate;
using Core.Entities;
using Core.Entities.Cells;
using Core.Entities.Aggregates;
using Microsoft.OpenApi.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Logging.AddConsole();

Infrastructure.Dependencies.ConfigureServices(builder.Configuration, builder.Services);


builder.Services.AddScoped(typeof(IRepository<>), typeof(EfRepository<>));
builder.Services.AddScoped(typeof(IReadRepository<>), typeof(EfRepository<>));
builder.Services.AddScoped<IService<Book>, GenericService<Book>>();
builder.Services.AddScoped<IService<Chapter>, GenericService<Chapter>>();
builder.Services.AddScoped<IService<Page>, GenericService<Page>>();
builder.Services.AddScoped<IService<Cell>, GenericService<Cell>>();
builder.Services.AddScoped<IService<Category>, GenericService<Category>>();
builder.Services.AddScoped<IService<Tag>, GenericService<Tag>>();
builder.Services.AddScoped(typeof(IAppLogger<>), typeof(LoggerAdapter<>));

builder.Services.AddMemoryCache();

builder.Services.AddControllers();

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c =>
{
    c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" });
});

var app = builder.Build();

app.Logger.LogInformation("PublicApi App created...");

app.Logger.LogInformation("Seeding Database...");

using (var scope = app.Services.CreateScope())
{
    var scopedProvider = scope.ServiceProvider;
    try
    {
        var bookDesinerContext = scopedProvider.GetRequiredService<BookDesinerContext>();
        if (bookDesinerContext.Database.IsSqlServer())
        {
            bookDesinerContext.Database.EnsureCreated();
            bookDesinerContext.Database.Migrate();
        }
    }
    catch (Exception ex)
    {
        app.Logger.LogError(ex, "An error occurred seeding the DB.");
    }
}

if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}

app.UseHttpsRedirection();

app.UseRouting();

// Enable middleware to serve generated Swagger as a JSON endpoint.
app.UseSwagger();

// Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.), 
// specifying the Swagger JSON endpoint.
app.UseSwaggerUI(c =>
{
    c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
});

app.UseEndpoints(endpoints =>
{
    endpoints.MapControllers();
});

app.Logger.LogInformation("LAUNCHING PublicApi");
app.Run();

I tried adding a Specification but don't understand how it works and where to bootstrap it. BookSpecification.cs

using Ardalis.Specification;
using Core.Entities.BookAggregate;

namespace Core.Specifications
{
    public class BookSpecification : Specification<Book>
    {
        public BookSpecification(int tagId) 
        {
            Query
                .Where(b => b.BookId == tagId)
                .Include(b => b.BookTags);
        }

        /** Can't do that because this type of constructor already exists
        public BookSpecification(int categoryId)
        {
            Query
                .Where(b => b.BookId == categoryId)
                .Include(b => b.BookCategories);
        }

        public BookSpecification(int tagId)
        {
            Query
                .Where(b => b.BookId == tagId)
                .Include(b => b.BookTags);
        }
        **/
    }
}

From the official Microsoft documentation I know that I should be able to use Include() or ThenInclude(), but it doesn't seem to work.

Where would I have to place the Include() or what is the correct way to use Specification or what else can I do to fix this? I know I made myself enough problems by mixing these two architectual styles, but I exhausted all possible error sources I could think of.

Update 1

According to GitHub FAQ for Specification Tried a new approach that didn't work:

using Ardalis.Specification;
using Core.Entities.BookAggregate;

namespace Core.Specifications
{
    public class BookSpecification : Specification<Book>
    {
        public BookSpecification() 
        {
            Query.Include(b => b.BookTags);
            Query.Include(b => b.BookCategories);
            Query.Include(b => b.Chapters);
        }
    }
}
1

There are 1 best solutions below

0
On

I was able to gather all the information needed by using the official guide.

BookSpecification.cs

using Ardalis.Specification;
using Core.Entities.BookAggregate;

namespace Core.Specifications
{
    public class BookSpecification : Specification<Book>
    {
        public BookSpecification() 
        {
            Query.Include(b => b.BookTags);
            Query.Include(b => b.BookCategories);
            Query.Include(b => b.Chapters);
        }
    }
}

IService.cs

using Ardalis.Specification;

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.cs

using Ardalis.Specification;
using Core.Interfaces;
using Core.Specifications;

namespace Core.Services
{
    public class GenericService<T> : IService<T> where T : class, IAggregateRoot
    {
        private readonly IRepository<T> _repository;
        private readonly IAppLogger<GenericService<T>> _logger;


        public GenericService(IRepository<T> repository, IAppLogger<GenericService<T>> logger)
        {
            _repository = repository;
            _logger = logger;
        }

        public async Task<T> GetByIdAsync(int id)
        {
            return await _repository.GetByIdAsync(id);
        }

        // Adapt to use specifications
        public async Task<IEnumerable<T>> ListAsync()
        {
            return await _repository.ListAsync();
        }

        public async Task<IEnumerable<T>> ListAsyncWithSpec(Specification<T> spec)
        {
            return await _repository.ListAsync(spec);
        }

        public async Task DeleteByIdAsync(int id)
        {
            var t = await _repository.GetByIdAsync(id);

            if (t == null)
            {
                _logger.Error($"Element with id: {id} can not be found!");
                throw new ArgumentException($"Element with id: {id} can not be found!");
            }

            await _repository.DeleteAsync(t);
        }

        public async Task AddAsync(T t)
        {
            await _repository.AddAsync(t);
        }

        public async Task UpdateAsyc(T t)
        {
            await _repository.UpdateAsync(t);
        }
    }
}

BaseController.cs made GET overridable

using Core.Interfaces;
using Microsoft.AspNetCore.Mvc;

namespace API.Controllers
{
    public class BaseController<T> : Controller where T : class, IAggregateRoot
    {
        private protected readonly IService<T> _service;

        public BaseController(IService<T> service)
        {
            _service = service;
        }

        [HttpGet]
        public virtual async Task<IEnumerable<T>> Get()
        {
            return await _service.ListAsync();
        }

        [HttpGet("{id}")]
        public async Task<T> Get(int id)
        {
            return await _service.GetByIdAsync(id);
        }

        [HttpPost]
        public async Task<IActionResult> Post([FromBody] T t)
        {
            await _service.AddAsync(t);
            return Ok(t);
        }

        [HttpPut("{id}")]
        public virtual async Task<IActionResult> Put(int id, [FromBody] T t)
        { 
            throw new NotImplementedException();
        }

        [HttpDelete("{id}")]
        public async Task<IActionResult> Delete(int id)
        {
            if (await _service.GetByIdAsync(id) == null)
                return NotFound();

            await _service.DeleteByIdAsync(id);
            return Ok();
        }
    }
}

BookController.cs

using Core.Entities.BookAggregate;
using Core.Interfaces;
using Core.Specifications;
using Microsoft.AspNetCore.Mvc;

// For more information on enabling Web API for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860

namespace API.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class BookController : BaseController<Book>
    {
        public BookController(IService<Book> service) : base(service)
        {
        }

        [HttpGet]
        public override async Task<IEnumerable<Book>> Get()
        {
            return await _service.ListAsyncWithSpec(new BookSpecification());
        }

        [HttpPut("{id}")]
        public override async Task<IActionResult> Put(int id, [FromBody] Book book)
        {
            Book entity = await _service.GetByIdAsync(id);
            entity.Title = book.Title;
            entity.Chapters = book.Chapters;
            
            await _service.UpdateAsyc(entity);
            return Ok(entity);
        }
    }
}

Program.cs ignore circles

builder.Services.AddControllers().AddJsonOptions(x => x.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles);