Returned object has null/empty ICollection unless it is accessed/inspected first

≯℡__Kan透↙ 提交于 2020-01-17 02:37:11

问题


Seeding deserialised JSON data in the Context and then attempting to return the DbSet. api/module returns all Modules with empty ICollections (null if I don't instantiate them), where as the Assessments happily return the virtual Module.

Previous experience in MVC whereby I would have been accessing the object before it being sent to the view, so I haven't encountered this issue before.

The commented out line:

//Enumerable.ToList(ModuleItems.Include(mi => mi.Assessments));

Resolves the issue but feels very hacky and would need repeating for each DbSet.Passing the model names as parameters to include them when making the call in the repository also feels like a hack too.

What is the best practice?

EDIT: To add, when I inspect the DbSet when seeding the ICollections are populated and later, the Assessment DbSet has 6 items within it.

Module

public class Module
        {
            [Key]
            public int Id { get; set; }
            public string Code { get; set; }
            public string Description { get; set; }
            public DateTime InstanceStartDate { get; set; }
            public DateTime InstanceEndDate { get; set; }

            public ICollection<UnitLeaderModules> UnitLeaderModules { get; set; } = new HashSet<UnitLeaderModules>();
            public ICollection<Assessment> Assessments { get; set; } = new HashSet<Assessment>();

        }

Assessment

public class Assessment
{
    [Key]
    public int Id { get; set; }
    [ForeignKey("Module")]
    public int ModuleId { get; set; }
    public string Description { get; set; }
    public DateTime SubmissionDateMain { get; set; }
    public DateTime SubmissionDateResit { get; set; }
    public string SubmissionMethod { get; set; }

    public virtual Module Module { get; set; }
}

Generic Repository

public class Repository<T> : IRepository<T> where T : class
    {
        protected readonly DbContext Context;
        protected DbSet<T> DbSet;

        public Repository(DbContext context)
        {
            Context = context;
            DbSet = context.Set<T>();
        }

        public T Get<TKey>(TKey id)
        {
            return DbSet.Find(id);
        }

        public IQueryable<T> GetAll()
        {
            return DbSet;
        }

        public IQueryable<T> GetWhere(Expression<Func<T, bool>> whereExpression)
        {
            return DbSet.Where(whereExpression);
        }

        public void Add(T entity)
        {
            Context.Set<T>().Add(entity);

            Save();
        }

        public void Update(T entity)
        {
            Save();
        }

        private void Save()
        {
            Context.SaveChanges();
        }
    }

Module Controller

 [Route("api/module")]
 [ApiController]
    public class ModuleController : ControllerBase
    {
        private readonly IRepository<Module> _repository;

        public ModuleController(IRepository<Module> repository)
        {
            _repository = repository;
        }

        [HttpGet]
        public ActionResult<IQueryable<Module>> GetAll()
        {
            return Ok(_repository.GetAll());
        }

        [HttpGet("{id}", Name = "GetModule")]
        public ActionResult<Module> GetById(int id)
        {
            var item = _repository.Get(id);
            if (item == null)
            {
                return NotFound();
            }

            return item;
        }

    }

Context

public class UnitLeaderContext : DbContext
    {
        public DbSet<Leader> UnitLeaderItems { get; set; }
        public DbSet<UnitLeaderModules> UnitLeaderModuleItems { get; set; }
        public DbSet<Module> ModuleItems { get; set; }
        public DbSet<Assessment> AssessmentItems { get; set; }

        public UnitLeaderContext(DbContextOptions<UnitLeaderContext> options)
            : base(options)
        {
            ChangeTracker.LazyLoadingEnabled = false;

            if (!EnumerableExtensions.Any(ModuleItems))
            {
                var data =
@"[
        {
            ""id"": 1,

            ""code"": ""YEP404"",
            ""description"": ""Marine Systems"",
            ""instanceStartDate"": ""2018-09-24T00:00:00"",
            ""instanceEndDate"": ""2019-05-17T00:00:00"",
            ""assessments"": [
                {
                    ""id"": 1,
                    ""moduleId"": 1,
                    ""description"": ""Report 1 (60%)"",
                    ""submissionDateMain"": ""2019-01-15T00:00:00"",
                    ""submissionDateResit"": ""2019-07-06T00:00:00"",
                    ""submissionMethod"": ""Upload""

                }, 
                {
                    ""id"": 2,
                    ""moduleId"": 1,
                    ""description"": ""Examination (40%)"",
                    ""submissionDateMain"": ""2019-03-28T00:00:00"",
                    ""submissionDateResit"": ""2019-07-08T00:00:00"",
                    ""submissionMethod"": ""Email Lecturer""
                }
            ]
        }, 
        {
            ""id"": 2,
            ""code"": ""EEN402"",
            ""description"": ""Marine Production"",
            ""instanceStartDate"": ""2018-09-24T00:00:00"",
            ""instanceEndDate"": ""2019-05-17T00:00:00"",
            ""assessments"": [
                {
                    ""id"": 3,
                    ""moduleId"": 2,
                    ""description"": ""Report 1 (60%)"",
                    ""submissionDateMain"": ""2019-04-10T00:00:00"",
                    ""submissionDateResit"": ""2019-07-03T00:00:00"",
                    ""submissionMethod"": ""SOL""
                }, 
                {
                    ""id"": 4,
                    ""moduleId"": 2,
                    ""description"": ""Log Book 1 (40%)"",
                    ""submissionDateMain"": ""2019-04-10T00:00:00"",
                    ""submissionDateResit"": ""2019-07-03T00:00:00"",
                    ""submissionMethod"": ""SOL""
                }
            ]
        }, 
        {
            ""id"": 3,
            ""code"": ""YEP402"",
            ""description"": ""Marine Materials"",
            ""instanceStartDate"": ""2018-09-24T00:00:00"",
            ""instanceEndDate"": ""2019-05-17T00:00:00"",
            ""assessments"": [
                {
                    ""id"": 5,
                    ""moduleId"": 3,
                    ""description"": ""Report 1 (60%)"",
                    ""submissionDateMain"": ""2019-03-15T00:00:00"",
                    ""submissionDateResit"": ""2019-07-03T00:00:00"",
                    ""submissionMethod"": ""Hand-in Office""
                }, 
                {
                    ""id"": 6,
                    ""moduleId"": 3,
                    ""description"": ""Examination"",
                    ""submissionDateMain"": ""2019-04-10T00:00:00"",
                    ""submissionDateResit"": ""2019-07-03T00:00:00"",
                    ""submissionMethod"": ""In-person Exam""
                }
            ]
        }
]
";
                var aaa = JsonConvert.DeserializeObject<List<Module>>(data);

                ModuleItems.AddRange(aaa);
                SaveChanges();
            }

            //Enumerable.ToList(ModuleItems.Include(mi => mi.Assessments));
        }

        protected override void OnModelCreating(ModelBuilder builder)
        {
            builder.Entity<Module>().HasKey(m => m.Id);
            builder.Entity<Module>().HasMany(m => m.UnitLeaderModules);
            builder.Entity<Module>().HasMany(m => m.Assessments);

            builder.Entity<Assessment>().HasKey(m => m.Id);
            builder.Entity<Assessment>().HasOne(m => m.Module);
        }


    }

回答1:


[Unsing an Include] resolves the issue but feels very hacky and would need repeating for each DbSet

It's not a hack. Each Controller defines the shape of its returned data, ie whether the client should get just the Module or all the Assesments as well. And using the Include on the DbSet is the way your Controller specifies the shape of the data it needs.

Your JSON Serializer might end up triggering Lazy Loading for some Navigation Properties, but you should disable Lazy Loading and build your object graphs explicitly when serializing to JSON.




回答2:


You should consider avoiding passing entities between server and client. By declaring these references as non-virtual, they will not lazy-load on demand. From the perspective of controllers and JSON serialization you don't want to trigger lazy loading anyways as this will cripple performance. So you need to eager-load all child references you want to be included on the client. However, this is a bad idea for a number of reasons including:

  1. Performance- Do you need every property on this entity and all of its children? By passing entities you need your database to load all columns, transmit that over the wire to the app server, allocate memory for all of that data, then transmit it over the wire to the client and allocate the memory for that state on the browser. Is it really needed? If you have lazy loading enabled, serialization can trip the lazy load proxy calls and lead to crippling performance. Passing entities is often justified to avoid an extra read call when updating. However, most systems read far more than they write. Making reads faster outweighs the possible saving of an extra read when writing.

  2. Completeness - Do you eager load every child reference? Today you need maybe one set of children and don't eager load everything to help minimize point #1. However tomorrow someone may look at the code and assume that they have a complete entity graph to work with. It isn't complete, and this means potential bugs and more inclusions to worsen performance.

  3. Complexity & Scaleability- It seems simple to load the entity and send it to the client, then let the client modify that entity, send it back to the server, attach it to a DbContext, and SaveChanges. No need to load the entity twice. Except reattaching entity graphs is messy, and that entity's data may have changed in that time. "Last in wins" is ok for some systems, but can be problematic if users aren't expecting that.

  4. Security - Do you expose all of that data to all users? Your UI may restrict the data that users can see or edit, however if your structure passes around entities and you're inclined to simply attach entities sent back to the server and have the context save them then you're opening the system to a significant hacking risk. Clever users can see the data being sent to the client in it's complete state using debugging tools on the browser. They can also modify responses to the server to change data they otherwise wouldn't have been able to edit, even possibly changing FK references to affect data they are not authorized to see or change. You can mitigate this with checks against the database prior to committing, but then your entities are nothing more than overweight view models/DTOs.

Using mapped view models that are materialized from EF Linq expressions via Select helps with all these scenarios. Tools like Automapper can reduce the "boring" extra code to perform the mapping. By using view models:

  1. Performance - SQL Server only passes back exactly the data you need to send to the client. This means faster read queries and lighter bandwidth/memory usage.
  2. Completeness - A view model's purpose is the view's model. Nothing more, nothing less. It's not an entity and there are no assumptions about what is, and is not valid/present.
  3. Complexity & Scaleability - No messy code, especially if you use Automapper. Mapping code is boring, but it's dead simple to understand. There is no messy reattaching, and it can be easily compared against current data state before updates are committed.
  4. Security - Users can only see what's in the view model, and only change what is in the view model. Server-side code is structured to load authorized entities, and inspect the values before committing.

Some food for thought for anyone coming across questions around passing entities around.



来源:https://stackoverflow.com/questions/53302824/returned-object-has-null-empty-icollection-unless-it-is-accessed-inspected-first

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!