dotnet / efcore

EF Core is a modern object-database mapper for .NET. It supports LINQ queries, change tracking, updates, and schema migrations.
https://docs.microsoft.com/ef/
MIT License
13.77k stars 3.18k forks source link

Include on readonly context object changed in EF Core 2.1 preview 2 #11664

Closed EdwinArends closed 6 years ago

EdwinArends commented 6 years ago

In our project we are using EF Core 2.1 preview 2, in the preview 1 version we used to use a lot of Include / ThenInclude methods. In the return methods we use AutoMapper to map the database objects to a return object. We use a UnitOfWork/Repository design pattern for our backend.

We explicitly use the AsNoTracking feature for performance/memory reasons. This worked fine in preview 1, but in preview 2 we get the following exception.

Exception message:
Navigation property 'DocumentType' on entity of type 'Document' cannot be loaded because the entity is not being tracked. Navigation properties can only be loaded for tracked entities.
Stack trace:
System.InvalidOperationException
  HResult=0x80131509
  Message=Navigation property 'DocumentType' on entity of type 'Document' cannot be loaded because the entity is not being tracked. Navigation properties can only be loaded for tracked entities.
  Source=Microsoft.EntityFrameworkCore
  StackTrace:
   at Microsoft.EntityFrameworkCore.Internal.EntityFinder`1.Load(INavigation navigation, InternalEntityEntry entry)
   at Microsoft.EntityFrameworkCore.Internal.LazyLoader.Load(Object entity, String navigationName)
   at Microsoft.EntityFrameworkCore.Proxies.Internal.LazyLoadingInterceptor.Intercept(IInvocation invocation)
   at Castle.DynamicProxy.AbstractInvocation.Proceed()
   at Castle.Proxies.DocumentProxy.get_DocumentType()
   at Service.Atlas.DocumentService.GetDocument(Guid id) in D:\dev\Atlas\Service.Atlas\DocumentService.cs:line 80
   at Atlas.Controllers.DocumentController.GetDocumentHeader(Guid id) in D:\dev\Atlas\Atlas.Controllers\DocumentController.cs:line 107
   at Microsoft.AspNetCore.Mvc.Internal.ActionMethodExecutor.SyncActionResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.<InvokeActionMethodAsync>d__12.MoveNext()

Steps to reproduce

We generate a simple Document class from the SQL Server database using Scaffold-DbContext:

    public partial class Document
    {
        public Document()
        {
        }

        public Guid Id { get; set; }
        public int DocumentNumber { get; set; }
        public string Description { get; set; }
        public DocumentStatus Status { get; set; }
        public string CreatedBy { get; set; }
        public DateTime CreatedDate { get; set; }
        public string ModifiedBy { get; set; }
        public DateTime? ModifiedDate { get; set; }
        public byte[] RowVersion { get; set; }

        public virtual ICollection<AuditTrail> AuditTrail { get; set; } = new HashSet<AuditTrail>();
        public virtual ICollection<DocumentAttributeValue> DocumentAttributeValue { get; set; } = new HashSet<DocumentAttributeValue>();
        public virtual ICollection<DocumentType> DocumentType { get; set; } = new HashSet<DocumentType>();
        public virtual ICollection<Revision> Revision { get; set; } = new HashSet<Revision>();
    }

We use a Repository interface so the service layer can access the database:

    public class Repository<TEntity> : IRepository<TEntity> where TEntity : class
    {
        private readonly UnitOfWork _unitOfWork;
        private readonly IApplicationLogger _applicationLogger;

        public Repository(IUnitOfWork unitOfWork, IApplicationLogger applicationLogger)
        {
            _applicationLogger = applicationLogger;
            _unitOfWork = (UnitOfWork) unitOfWork;
        }

        protected DbSet<TEntity> DbSet => _unitOfWork.DataContext.Set<TEntity>();

        public virtual IQueryable<TEntity> Get()
        {
            return DbSet;
        }

        public virtual IQueryable<TEntity> Query()
        {
            return DbSet.AsNoTracking();
        }

        public virtual IQueryable<TEntity> Get(Expression<Func<TEntity, bool>> where)
        {
            return DbSet.Where(where);
        }

        public virtual TEntity GetById(object id)
        {
            return DbSet.Find(id);
        }

        public virtual void Add(TEntity entity)
        {
            _applicationLogger.Info<TEntity>("Added entity to database.");
            DbSet.Add(entity);
        }

        public virtual void Delete(TEntity entity)
        {
            _applicationLogger.Info<TEntity>($"Deleting entity.");
            _unitOfWork.DataContext.Entry(entity).State = EntityState.Deleted;
        }

        public virtual void Delete(Expression<Func<TEntity, bool>> where)
        {
            var objects = DbSet.Where(where).AsEnumerable();
            foreach (var obj in objects)
            {
                DbSet.Remove(obj);
            }
        }
    }

We inject the repository in the Startup class:

services.AddScoped<IRepository<Document>, Repository<Document>>();

In our document service class the repository is injected:

   public class DocumentService : IDocumentService
    {
        private readonly IRepository<Document> _documentRepository;
        private readonly IRepository<Type> _typeRepository;
        private readonly IRepository<DocumentType> _documentTypeRespository;
        private readonly IRepository<DocumentAttributeValue> _documentAttributeValueRepository;
        private readonly IRepository<Revision> _revisionRepository;
        private readonly IRepository<Attribute> _attributeRepository;
        private readonly IRepository<AttributeValue> _attributeValueRepository;
        private readonly IRepository<AttributeMultiValue> _attributeMultiValueRepository;
        private readonly IRepository<MultiValueAttributeValue> _multiValueAttributeValueRepository;
        private readonly IFileService _fileService;
        private readonly IUnitOfWork _unitOfWork;

        private readonly IAuditService _audit;
        private readonly IApplicationLogger _logger;
        private readonly IAttributeQuery _attributeQuery;
        private readonly ITypeQuery _typeQuery;
        private readonly IRevisionQuery _revisionQuery;

        public DocumentService(IRepository<Document> documentRepository, IUnitOfWork unitOfWork,
            IRepository<Revision> revisionRepository, IRepository<Attribute> attributeRepository,
            IFileService fileService,
            IRepository<DocumentType> documentTypeRespository,
            IRepository<DocumentAttributeValue> documentAttributeValueRepository,
            IRepository<AttributeValue> attributeValueRepository,
            IRepository<AttributeMultiValue> attributeMultiValueRepository,
            IRepository<MultiValueAttributeValue> multiValueAttributeValueRepository,
            IRepository<Type> typeRepository,
            IAuditService audit, IApplicationLogger logger, IAttributeQuery attributeQuery, ITypeQuery typeQuery, IRevisionQuery revisionQuery)
        {
            _documentRepository = documentRepository;
            _unitOfWork = unitOfWork;
            _revisionRepository = revisionRepository;
            _attributeRepository = attributeRepository;
            _fileService = fileService;
            _documentTypeRespository = documentTypeRespository;
            _documentAttributeValueRepository = documentAttributeValueRepository;
            _attributeValueRepository = attributeValueRepository;
            _attributeMultiValueRepository = attributeMultiValueRepository;
            _multiValueAttributeValueRepository = multiValueAttributeValueRepository;
            _typeRepository = typeRepository;
            _audit = audit;
            _logger = logger;
            _attributeQuery = attributeQuery;
            _typeQuery = typeQuery;
            _revisionQuery = revisionQuery;
        }

        public DocumentResponse GetDocument(Guid id)
        {
            var document = _documentRepository.Query()
                .Include(x => x.DocumentType)
                .ThenInclude(x => x.Type)
                .FirstOrDefault(x => x.Id == id);

            if (document == null) return null;

            var documentResponse = Mapper.Map(document, new DocumentResponse());
//ERROR OCCURS BELOW
            documentResponse.DocumentTypes = document.DocumentType.Select(documentType => Mapper.Map(documentType.Type, new TypeResponse()));
            return documentResponse;

        }
   }

This error occurs in EF Core 2.1 preview 2, in EF Core 2.1 preview 1 this code was accepted and ran perfectly well. My question is. Is this by design and do we need to change our code? Or has this accidentally gotten broken in the preview 2 version?

Kind regards, Edwin

Further technical details

EF Core version: EF Core 2.1 preview 2 Database Provider: Microsoft.EntityFrameworkCore.SqlServer Operating system: Windows 10 professional IDE: Visual Studio 2017 15.4

ajcvickers commented 6 years ago

@EdwinArends The error is coming from lazy-loading. I believe in preview1, attempting to lazy-load when the entity is not tracked was silently ignored. In preview2, it started throwing to let you know that lazy-loading wasn't happening. Couple of questions:

ajcvickers commented 6 years ago

Note from triage: we will investigate making this a warning-as-error so it can be disabled.

ajcvickers commented 6 years ago

Made the following changes:

Leaving this open to discuss again in triage and to get answers to questions from @EdwinArends.