1

Maybe this is already solved somewhere but I can´t find a solution...

I work with EF Core and I want to get rid of lazyLoading. I want to write a generic method, which includes different NavigationProperties where needed. I have an Interface which defines crud-methods. I use AutoMapper to map the recieved entities to viewModels. I have different groups of entities. Some can be retrieved without any Navigations, others need specific navigationProperties included.

Actually I want to use one Base-Service-Interface and choose the right implementation during runtime. Or some conditional decision (as described below) in the Service-Implementation.

This is my Service:

public interface IService
{
    Task<IEnumerable<TViewModel> GetAsync<TEntity, TViewModel>()
        where TEntity : class
        where TViewModel : class;

    Task PostAsync<TEntity, TViewModel>(TViewModel model)
        where TEntity : class
        where TViewModel : class;

    Task<TViewModel> PatchAsync<TEntity, TViewModel>(int id)
        where TEntity : class;
}

This is my Navigation-Interface:

public interface INavigate
{
    int NavigationTypeId {get;set;}
    NavigationType NavigationType {get;set;}
}

This is my service-implementation:

public class Service
{
    public async Task PatchAsync<TEntity, TViewModel>(Guid id,
JsonPatchDocument<TViewModel> patchDoc)
        where TEntity : class
        where TViewModel : class
    {
        var query = this.dbContext.Set<TEntity>();
        if(typeof(INavigate).IsAssignableFrom(typeof(TEntity)))
        {
            query = this.dbContext.Set<TEntity>()
                .Include(e=>e.NavigationType); // obviously this won't work
        }
        var entity = await query.FirstAsync();
        var viewModel = this.mapper.Map<TViewModel>(entity);
        patchDocument.ApplyTo(viewModel);
        var updatedEntity = this.mapper.Map(viewModel, entity);
        this.dbContext.Entry(entity).CurrentValues.SetValues(updatedEntity);
        this.dbContext.SaveChangesAsync();
    }
}

So... This isn't a solution. I think there must be some way to solve this problem somehow without generating different services for each Navigation-Interface and specific where-clauses (where TEntity : class, INavigate) - but I don't know where to go from here.

Joshit
  • 1,238
  • 16
  • 38
  • 1
    Why do not use `ProjectTo`? Then you do not need Includes. – Svyatoslav Danyliv Jul 08 '22 at 12:11
  • I would suggest do not use such abstractions at the start. Write several controllers using direct access to DbContext then you will find by yourself how to effectively create abstraction or just extension methods. `GetAllAsync` which loads everything from database, usually not needed in production, but filters, maybe complex. filters, grouping, etc. – Svyatoslav Danyliv Jul 08 '22 at 14:09
  • @SvyatoslavDanyliv this is a very good solution for the useCase I explained in my question. Works fine for GetRequests. I edit my question and add use cases where this doesn't help. – Joshit Jul 10 '22 at 14:17
  • 1
    Try this Include `Include(“NavigationType”)` – Svyatoslav Danyliv Jul 11 '22 at 07:52

1 Answers1

0

If someone else is looking for a solution I'll post the implementation I ended up with. Thanks to Svyatoslav Danyliv who pointed me to this direction. If someone has a better solution I'd highly appreciate (and accept) an answer.

Here are other questions, that helped me: Include with reflection, ThenInclude and ProjectTo.

I use Automapper and have created a bidirectional Map for every entity/viewModel pair. I have different groups of entities. They either have different navigationProperties which are part of the viewModel or navigationProperties which aren't needed during crud or no navigationProperties at all. I use different interfaces for those navigations (such as INavigate). Some navigations have more than one level to include:

public interface ITopLevelNavigation
{
    int TopLevelNavigationId { get; set; }
    TopLevelNavigation TopLevelNavigation { get; set; }
}

public class TopLevelNavigation : INavigate
{
    public int Id { get; set; }
    public int NavigationTypeId { get; set; }
    public NavigationType NavigationType { get; set; }
}

And I have entities which have a required relationship:

public interface IRequireDependency
{
    int DependencyId { get; set; }
    RequiredDependency Dependency { get; set; }
}

I want to assure (for insert and update), that these dependencies exist to avoid an ef-core exception (ForeignKey-Violation) and being able to respond with an accurate and understandable feedback.

Based on the comments for my question, the easiest case is Get:

public Task<IEnumerable<TViewModel> GetAsync<TEntity, TViewModel>()
    where TEntity : class
{
    return await this.mapper.ProjectTo<TViewModel>(this.dbContext.Set<TEntity>())
        .ToListAsync();
}

This works perfectly fine. For generic post I only need to validate required dependencies, everything else can be done with Automapper:

public async Task PostAsync<TEntity, TViewModel>(TViewModel model)
    where TEntity : class
    where TViewModel : class
{
    var entity = this.mapper.Map<TEntity>(model);
    if (entity is IRequireDependency dependency)
    {
        if(!await this.dbContext.RequiredDependency
            .AnyAsync(e => e.Id == dependency.DependencyId)) 
                throw new Exception("Invalid DependencyId");
    }
    await this.dbContext.Set<TEntity>().AddAsync(entity);
    await this.dbContext.SaveChangesAsync();
}

To solve the include-issue I have to use the string-overload. I wrote an extension-method:

public static IQueryable<T> IncludeByInterface<T>(this IQueryable<T> queryable)
    where T : class
{
    if (typeof(INavigate).IsAssignableFrom(typeof(T)))
    {
        queryable = queryable.Include(nameof(INavigate.NavigationType));
    }
    if (typeof(ITopLevelNavigation).IsAssignableFrom(typeof(T)))
    {
        queryable = queryable.Include(nameof(ITopLevelNavigation.TopLevelNavigation));
        queryable = queryable.Include($"{nameof(ITopLevelNavigation.TopLevelNavigation)}.
            {nameof(ITopLevelNavigation.Navigation.NavigationType)}");
    }
    return queryable;
}

Now I can patch it like this:

public async Task Patch<TEntity, TViewModel>(int id, JsonPatchDocument<TViewModel> doc)
    where TEntity : class
    where TViewModel : class
{
    var entity = await this.dbContext.Set<TEntity>().AsQueryable()
        .IncludeByInterface().FirstAsync();
    var viewModel = this.mapper.Map<TViewModel>(entity);
    patchDocument.ApplyTo(viewModel);
    var updatedEntity = this.mapper.Map(viewModel, entity);
    this.dbContext.Entry(entity).CurrentValues.SetValues(updatedEntity);
    await this.dbContext.SaveChangesAsync();
}

I hope this helps if someone is facing similar issues. And again: If you think this can be done better - please let me know.

Joshit
  • 1,238
  • 16
  • 38