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);
}
}
}
I was able to gather all the information needed by using the official guide.
BookSpecification.cs
IService.cs
GenericService.cs
BaseController.cs
made GET overridableBookController.cs
Program.cs
ignore circles