Skip to content

Instantly share code, notes, and snippets.

@mahpah
Last active November 27, 2018 12:24
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mahpah/9b9ec99ca76fc19f7cd80ada3379f760 to your computer and use it in GitHub Desktop.
Save mahpah/9b9ec99ca76fc19f7cd80ada3379f760 to your computer and use it in GitHub Desktop.
Entity framework core lazyload proxy creation

Đây là một trường hợp hay ho liên quan đến lazy load của entity framework.

Cơ chế lazy load là tạo ra một proxy bọc xung quanh entity, để thay đổi các behaviour của entity mà interface vẫn giữ nguyên. Ví dụ gọi Comment.Creator, thực chất là gọi Proxy<Comment>.Query<Creator>... (đại loại thế, tôi không đi vào implement details). Như vậy nghĩa là muốn lazy load hoạt động thì dbcontext phải tạo được proxy wrap entity đó.

Trường hợp add 1 new entity (không phải proxy), DbContext.Add(entity), ngay sau đó lại dùng chính instance của DbContext này, query entity ra, thì entity đó vẫn là object cũ chứ không phải proxy tương ứng, nhờ cơ chế ChangeTracking của DbContext, dẫn đến việc không load được navigation property. Issue này sẽ rất phổ biến trong các domain event handler do DbContext có lifetime là scoped, tức là dbcontext, repository đều được sử dụng lại per http request.

Cách giải quyết có thể là:

  • tạo instance DbContext mới, hoặc đặt DbContext có lifetime là transient
  • tự reload entity khi cần

Problem và solution được minh họa băng unit test bên dưới

// tôi generate một vài dữ liệu trong database
using System;
using System.Linq;
using LazyLoadTest.Model;
using Microsoft.EntityFrameworkCore;
namespace LazyLoadTest
{
public class Fixture : IDisposable
{
private readonly LazyloadDbContext _dbContext;
public Guid DiscussionId { get; }
public Guid John { get; }
public Guid Jane { get; }
public Fixture()
{
_dbContext = new LazyloadDbContext();
_dbContext.Database.Migrate();
var john = new User("John Doe");
var jane = new User("Jane Doe");
_dbContext.AddRange(john, jane);
_dbContext.SaveChanges();
John = john.Id;
Jane = jane.Id;
var discussion = new Discussion("ping", John);
discussion.Add(new Comment("John say", John));
_dbContext.Add(discussion);
_dbContext.SaveChanges();
DiscussionId = discussion.Id;
}
public void Dispose()
{
_dbContext.Discussions.RemoveRange(_dbContext.Discussions);
_dbContext.RemoveRange(_dbContext.Set<User>());
_dbContext?.Dispose();
}
}
}
// Ở đây tôi lược bỏ các phần constructor và entity configure
using System;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;
namespace LazyLoadTest.Model
{
public class LazyloadDbContext : DbContext
{
public virtual DbSet<Discussion> Discussions { get; set; }
public virtual DbSet<Comment> Comments { get; set; }
public virtual DbSet<User> Users { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder
.UseLazyLoadingProxies()
.UseNpgsql("...");
}
}
public class Discussion
{
public string Content { get; private set; }
public Guid Id { get; private set; }
public virtual IList<Comment> Comments { get; private set; }
public Guid CreatorId { get; private set; }
public virtual User Creator { get; private set; }
}
public class Comment
{
public Guid Id { get; private set; }
public string Content { get; private set; }
public Guid DiscussionId { get; private set; }
public Guid CreatorId { get; private set; }
public virtual User Creator { get; private set; }
}
public class User
{
public Guid Id { get; private set; }
public string Name { get; private set; }
}
}
using System.Linq;
using System.Threading.Tasks;
using FluentAssertions;
using LazyLoadTest.Model;
using Microsoft.EntityFrameworkCore;
using Xunit;
namespace LazyLoadTest
{
public class UnitTest1 : IClassFixture<Fixture>
{
private readonly Fixture _fixture;
public UnitTest1(Fixture fixture)
{
_fixture = fixture;
}
[Fact]
public async Task reload_entity_using_the_same_context_will_not_create_proxy()
{
using (var context = new LazyloadDbContext())
{
// arrange: load an entity from db
var discussion = await context.Discussions.FirstOrDefaultAsync(t => t.Id == _fixture.DiscussionId);
discussion.Should().NotBeNull();
var johnComment = discussion.Comments.First(t => t.Content == "John say");
discussion.Comments.First().Creator.Name.Should().Be("John Doe");
// act: add a new child entity and update
var newComment = new Comment("Jane say 1", _fixture.Jane);
newComment.Creator.Should().BeNull();
discussion.Add(newComment);
context.Update(discussion);
await context.SaveChangesAsync();
// assert: reload discussion and see it content
var loaded = await context.Discussions.FirstOrDefaultAsync(t => t.Id == _fixture.DiscussionId);
// jane's comment is already attached to db context, so no proxy created. This mean
var janeComment = loaded.Comments.FirstOrDefault(t => t.Content == "Jane say 1");
janeComment.Creator.Should().BeNull();
}
}
[Fact]
public async Task reload_entity_using_a_new_context_will_create_proxy()
{
using (var context = new LazyloadDbContext())
{
// arrange: load an entity from db
var discussion = await context.Discussions.FirstOrDefaultAsync(t => t.Id == _fixture.DiscussionId);
discussion.Should().NotBeNull();
// act: add a new child entity and update
var newComment = new Comment("Jane say 2", _fixture.Jane);
newComment.Creator.Should().BeNull();
discussion.Add(newComment);
context.Update(discussion);
await context.SaveChangesAsync();
}
// initial
using (var context = new LazyloadDbContext())
{
var loaded = await context.Discussions.FirstOrDefaultAsync(t => t.Id == _fixture.DiscussionId);
var janeComment = loaded.Comments.FirstOrDefault(t => t.Content == "Jane say 2");
loaded.Creator.Should().NotBeNull();
}
}
[Fact]
public async Task if_use_same_context_must_reload_entity_yourself()
{
using (var context = new LazyloadDbContext())
{
// arrange: load an entity from db
var discussion = await context.Discussions.FirstOrDefaultAsync(t => t.Id == _fixture.DiscussionId);
discussion.Should().NotBeNull();
discussion.Comments.Count().Should().Be(1);
discussion.Comments.First().Creator.Name.Should().Be("John Doe");
// act: add a new child entity and update
var newComment = new Comment("Jane say 3", _fixture.Jane);
newComment.Creator.Should().BeNull();
discussion.Add(newComment);
context.Update(discussion);
await context.SaveChangesAsync();
await context.Entry(newComment).ReloadAsync();
var loaded = await context.Discussions.FirstOrDefaultAsync(t => t.Id == _fixture.DiscussionId);
var janeComment = loaded.Comments.FirstOrDefault(t => t.Content == "Jane say 3");
loaded.Creator.Should().NotBeNull();
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment