How do I work with related data in a repository pattern using Fluent API?

I need to work with related data, I've been looking at the Fluent API to make this a little easier to do, one area that I never saw documented during my searches was, how to use this approach in a repository pattern. I need some help in marrying these concepts together in a real-world scenario. I have looked at all the documentation but, again, nothing really points to a repository pattern and I also find some of the code looks quite bloated and I like to simplify as much as possible to keep my application clean.

So, here is my setup at the moment. I have an ASP.NET Core 3.1 web application that has 4 projects within a single solution.

  • MyProject.UI
  • MyProject.Data
  • MyProject.Repo
  • MyProject.Services

I want to create a one to many relationship between two entities Car and Tag, whereby one Car can have many Tags. I've followed some documentation and this is what I've done so far

MyProject.Repo/Car.cs

using System.Collections;
using System.Collections.Generic;

namespace MyProject.Data
{
    public class Car : BaseEntity
    {
        public string CarName { get; set; }
        public ICollection<Tag> Tags { get; }
    }
}

MyProject.Repo/Tag.cs

namespace MyProject.Data
{
    public class Tag : BaseEntity
    {
        public string TagName { get; set; }

        public int CurrentCarId { get; set; }
        public Car Car { get; set; }
    }
}

Under my database context I have made the required changes specifically under OnModelCreating that define the relationship that is expected.

MyProject.Repo/MyContext.cs

using MyProject.Data;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;

namespace MyProject.Repo
{
    public class MyContext : IdentityDbContext<ApplicationUser>
    {
        public MyContext(DbContextOptions<MyContext> options)
            : base(options) { }

        protected override void OnModelCreating(ModelBuilder builder)
        {
            builder.Entity<Car>()
                .HasMany<Tag>(x => x.Tags)
                .HasForeignKey(s => s.CurrentCarId);

            base.OnModelCreating(builder);
        }
        public DbSet<Car> Cars { get; set; }
        public DbSet<Tag> Tags { get; set; }
    }
}

For clarity I've included the code and setup for my repository pattern. The following code demonstrates how it's setup and used.

MyProject.Repo/Repository.cs

using MyProject.Data;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;

namespace MyProject.Repo
{
    public class Repository<TEntity> : IRepository<TEntity>
                    where TEntity : BaseEntity
    {
        private readonly MyContext _dbContext;
        private readonly DbSet<TEntity> entities;
        string errorMessage = string.Empty;

        public Repository(MyProject context)
        {
            this._dbContext = context;
            entities = context.Set<TEntity>();
        }
        public IEnumerable<TEntity> GetAll()
        {
            return entities.AsEnumerable();
        }

        public TEntity Get(int id)
        {
            return entities.SingleOrDefault(s => s.Id == id);
        }
        public void Insert(TEntity entity)
        {
            if (entity == null)
            {
                throw new ArgumentNullException("entity");
            }
            entities.Add(entity);
            _dbContext.SaveChanges();
        }

        public void Update(TEntity entity)
        {
            if (entity == null)
            {
                throw new ArgumentNullException("entity");
            }
            _dbContext.Update(entity);
            _dbContext.SaveChanges();
        }

        public void Delete(TEntity entity)
        {
            if (entity == null)
            {
                throw new ArgumentNullException("entity");
            }
            entities.Remove(entity);
            _dbContext.SaveChanges();
        }
        public void Remove(TEntity entity)
        {
            if (entity == null)
            {
                throw new ArgumentNullException("entity");
            }
            entities.Remove(entity);
        }

        public void SaveChanges()
        {
            _dbContext.SaveChanges();
        }
    }
}

MyProject.Repo/IRepository.cs

using MyProject.Data;
using System.Collections.Generic;

namespace MyProject.Repo
{
    public interface IRepository<TEntity>
                where TEntity : BaseEntity
    {
        IEnumerable<TEntity> GetAll();
        TEntity Get(int id);
        void Insert(TEntity entity);
        void Update(TEntity entity);
        void Delete(TEntity entity);
        void Remove(TEntity entity);
        void SaveChanges();
    }
}

For each entity I have a service/interface that uses the repository and can then be injected into a controller.

MyProject.Services/ICarService.cs

using MyProject.Data;
using MyProject.Repo;
using System.Collections.Generic;

namespace MyProject.Services
{
    public class CarService : ICarService
    {
        private IRepository<Car> carRepository;

        public CarService(IRepository<Car> carRepository)
        {
            this.carRepository = carRepository;
        }
        public IEnumerable<Car> GetCars()
        {
            return carRepository.GetAll();
        }
        public Car GetCar(int id)
        {
            return carRepository.Get(id);
        }
        public void InsertCar(Car car)
        {
            carRepository.Insert(car);
        }
        public void UpdateCar(Car car)
        {
            carRepository.Update(car);
        }
        public void DeleteCar(int id)
        {
            Car car = GetCar(id);
            carRepository.Remove(car);
            carRepository.SaveChanges();
        }
    }
}

MyProject.Services/CarService.cs

using MyProject.Data;
using System.Collections.Generic;

namespace MyProject.Services
{
    public interface ICarService
    {
        IEnumerable<Car> GetCars();
        Car GetCar(int id);
        void InsertCar(Car car);
        void UpdateCar(Car car);
        void DeleteCar(int id);
    }
}

The same setup applied to the Tag entity too so I've not included it for brevity. Once the interfaces are defined I can then inject them into my controllers, for example:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using MyProject.Data;
using MyProject.Repo;
using MyProject.Services;
using MyProject.UI.Helpers;
using Kendo.Mvc.Extensions;
using Kendo.Mvc.UI;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace MyProject.UI.Controllers
{
    public class HomeController : Controller
    {
        private readonly ICarService _carService;
        private readonly ITagService _tagService;

        public HomeController(ICarService carService, ITagService tagService)
        {
            _carService = carService;
            _tagService = tagService;
        }
        public IActionResult Index()
        {
            var cars = _carService.GetCars();
            return View(cars);
        }


        [HttpGet]
        public IActionResult Edit(int id)
        {
            var cars = _carService.GetCar(id);
            return View(cars);
        }
    }
}

With the above information sorted out, I now have the task creating a way of being able to read the related data, update it, and delete it. Looking at the documentation shows a way of created loops but that's the part that feels bloated because I use a repository there is no manner to define things like .include() in my code, my question is, how do I read this related data with my configuration and work with it? You'll notice in my code that I also use KendoUI which, in many instances, requires JSON data to be returned. Are the loops mention in the documentation the only way? Any guidance is appreciated.