I have a asp.net application using entity framework. I have these two models:
public class CustomerModel
{
public int Id{get;set; }
[Required]
public string Name {get;set; }
[Required]
public string Surname { get; set; }
[Required]
[Range(18,110)]
public uint Age { get; set; }
[Required]
public virtual AdressModel Adress { get; set; }
[Required]
public DateTime Created { get; set; }
}
and
public class AdressModel
{
public int Id { get; set; }
[Required]
public int HouseNumber { get; set; }
[Required]
public string Town { get; set; }
[Required]
public string ZipCode { get; set; }
[Required]
public string Country { get; set; }
}
And a dbcontext class that looks like this:
public class DemoContext : DbContext
{
public DbSet<CustomerModel> Customers { get; set; }
public DbSet<AdressModel> Adresses { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder options)
{
options.UseLazyLoadingProxies();
options.UseSqlite(@"Data Source=/home/ask/RiderProjects/Parkz/identifier.sqlite");
}
}
and then I have a controller that just needs to load all the customers that I have in my database, and their adresses.
For that purpose I have this:
public IActionResult sendhere()
{
List<CustomerModel> customers = new List<CustomerModel>();
using (var db = new DemoContext()) {
customers = db.Customers
.Include(c => c.Adress)
.ToList();
}
return Content("hi");
}
Which I have tried to debug a bit.
The issue is that as soon as I exit my "using" block, all the related adress objects only consist of this error:
System.InvalidOperationException: An error was generated for warning 'Microsoft.EntityFrameworkCore.Infrastructure.LazyLoadOnDisposedContextWarning': An attempt was made to lazy-load navigation 'CustomerModelProxy.Adress' after the associated DbContext was disposed. This exception can be suppressed or logged by passing event ID 'CoreEventId.LazyLoadOnDisposedContextWarning' to the 'ConfigureWarnings' method in 'DbContext.OnConfiguring' or 'AddDbContext'.
at Microsoft.EntityFrameworkCore.Diagnostics.EventDefinition`2.Log[TLoggerCategory](IDiagnosticsLogger`1 logger, TParam1 arg1, TParam2 arg2)
at Microsoft.EntityFrameworkCore.Diagnostics.CoreLoggerExtensions.LazyLoadOnDisposedContextWarning(IDiagnosticsLogger`1 diagnostics, DbContext context, Object entityType, String navigationName)
at Microsoft.EntityFrameworkCore.Infrastructure.Internal.LazyLoader.ShouldLoad(Object entity, String navigationName, NavigationEntry& navigationEntry)
at Microsoft.EntityFrameworkCore.Infrastruc…
Which is because I exited the context.
How do I still get to access the adresses of my customers, even though I exited the Dbcontext, so I can return them to a view
CodePudding user response:
My general advice when working with EF is that entities shouldn't be referenced outside of the scope of their DbContext. You certainly can work with detached entities, but you have to respect that they are provided in an "as-is" state from the moment they leave the scope of the DbContext. For that reason I recommend that anything passed outside of the scope of the DbContext should be a POCO ViewModel or DTO class to avoid confusing whether an entity class is actually a functional, attached entity representing data domain state, or a detached shell.
Option 1: Deal with DTO/ViewModels.
public IActionResult sendhere()
{
using (var db = new DemoContext()) {
var customerDTOs = db.Customers
.Select(c => new CustomerDTO
{
// fill in the fields you want here.
Addresses = c.Addresses.Select(a => new AddressDTO
{
// fill in the address fields you want here.
}).ToList()
}).ToList();
return View(customerDTOs);
}
}
You can alternatively leverage Automapper, set up the desired projection rules and use ProjectTo<CustomerDTO>(config)
to replace the use of Select()
above.
When leveraging projection, you do not need lazy loading proxies at all. This has arguably become the defacto recommended approach for EF.
The advantages of the projection method are that these DTOs (or ViewModels) cannot be confused with being entities. The issue with detached entities is that where you have methods in your code that might accept entities, these methods might expect to get entities and access members that aren't loaded. If they are attached and within the scope of a DbContext, those members can be lazy-loaded (not ideal for performance reasons, but functional) however if they are detached you get errors or NullRefExceptions. The other advantage of projection is the payload of data being pulled from the database and sent to the view logic or end consists of just the data needed.
Option 2: Don't de-scope the DbContext. With projects like ASP.Net MVC web applications, you can leverage an IoC Container to provide dependency injection into your Controllers. In this way you can set up the DbContext to be injected into the constructor with a lifetime scope set to the Request. In this way, for any given request, all services/classes you might call can be managed by the container and have access to the DbContext.
public class SomeController
{
private readonly DemoContext _context;
public SomeController(DemoContext context)
{
_context = context ?? throw new ArgumentNullException(nameof(context));
}
public IActionResult sendhere()
{
var customers = _context.Customers
.Include(c => c.Address)
.ToList();
return View(customers);
}
}
This can be combined with Option 1 to avoid needing to scope a DbContext with each request/action and better facilitate situations where you may want to make multiple calls against the DbContext and ensure the same context instance is used. For IoC containers there are a number of different ones available, and I believe ASP.Net Core comes with a default one, though I personally use and recommend Autofac. It has good documentation and examples on how to wire it up with MVC projects.
Option 3: Eager load everything you're going to need to reference. The example you provided should actually work, but your real code is likely missing an eager-load (.Include()
) for the desired relationship given your example doesn't attempt to do anything with the Customers collection you load.
If your code does:
List<CustomerModel> customers = new List<CustomerModel>();
using (var db = new DemoContext()) {
customers = db.Customers
.Include(c => c.Address)
.ToList();
}
var firstAddressId = customers.FirstOrDefault()?.Address.Id;
This should work as Addresses was eager loaded. However, if you had:
List<CustomerModel> customers = new List<CustomerModel>();
using (var db = new DemoContext()) {
customers = db.Customers
.ToList();
}
var firstAddressId = customers.FirstOrDefault()?.Address.Id;
... without the Include(c => c.Address)
, then it would fail with that error.
With EF Core if you are going to want to return entities outside of the scope of a DbContext and you have lazy loading proxies enabled, you will want to temporarily turn off the proxy creation to avoid proxy errors. In this case anything you don't eager load will be left #null or default.
List<CustomerModel> customers = new List<CustomerModel>();
using (var db = new DemoContext()) {
db.ContextOptions.ProxyCreationEnabled = false;
customers = db.Customers
.Include(c => c.Address)
.ToList();
}
Return View(customers);
This should ensure that EF doesn't use proxies for the queries in the scope of that DbContext instance which should be avoided whenever you want to pass entities outside of the scope of the DbContext. This can be useful when you know you won't need the overhead of eager loading every reference. However, it is much better to use projection (Option 1) in this case to avoid future confusion around whether entities might actually have #null data, or merely it wasn't eager loaded.