using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Common;
using System.Data.Entity;
using System.Data.SqlClient;
using System.Linq;
using System.Transactions;
namespace Testing
{
/// <summary>
/// This is a test to reproduce the lazy loading bug (EF6 alpha 3)
/// The issue occurs when there is an entity linked to the same lookup table twice using 2 reference properties. If both reference are initially equal and then one is modified, the other does not lazy load and returns null.
/// Note: You can find attached the exact database used during this test
/// </summary>
[TestClass]
public class EfTests
{
const string connectionStr = @"Data Source=(local);Database=TestContext;Integrated Security=SSPI;";
/// <summary>
/// This is succeeding and would rule out stuff such as missing proxy creation requirements
/// </summary>
[TestMethod]
public void LinkedToDifferentUserLazyLoadingTest()
{
using (var ts = new TransactionScope(TransactionScopeOption.Required, new TimeSpan(1, 0, 0)))
{
InsertSampleData();
var context = new TestContext(new SqlConnection(connectionStr));
//Load booking linked to different users
var target = (from x in context.BookingEntities where x.CreatedById != x.ModifiedById && x.ModifiedById != 3 && x.CreatedById != 3 select x).First();
target.ModifiedById = 3;
//This is succeeding
Assert.IsNotNull(target.CreatedBy);
}
}
/// <summary>
/// This is failing in EF5 and EF6 alpha3
/// </summary>
[TestMethod]
public void LinkedToSameUserLazyLoadingBugTest()
{
using (var ts = new TransactionScope(TransactionScopeOption.Required, new TimeSpan(1, 0, 0)))
{
InsertSampleData();
var context = new TestContext(new SqlConnection(connectionStr));
//Load booking linked to single user
var target = (from x in context.BookingEntities where x.CreatedById == x.ModifiedById && x.ModifiedById != 3 && x.CreatedById != 3 select x).First();
target.ModifiedById = 3;
//This is failing
Assert.IsNotNull(target.CreatedBy);
}
}
#region Sample Data
void InsertSampleData()
{
using(var context = new TestContext(new SqlConnection(connectionStr)))
{
//Create 3 users
var user1 = context.UserEntities.Create();
user1.Id = 1;
user1.Name = "User 1";
context.UserEntities.Add(user1);
var user2 = context.UserEntities.Create();
user2.Id = 2;
user2.Name = "User 2";
context.UserEntities.Add(user2);
var user3 = context.UserEntities.Create();
user3.Id = 3;
user3.Name = "User 3";
context.UserEntities.Add(user3);
//Create booking linked to same user
var booking = context.BookingEntities.Create();
context.BookingEntities.Add(booking);
booking.Id = 1;
booking.CreatedBy = user1;
booking.ModifiedBy = user1;
//Create booking linked to different users
var booking2 = context.BookingEntities.Create();
context.BookingEntities.Add(booking2);
booking2.Id =2;
booking2.CreatedBy = user1;
booking2.ModifiedBy = user2;
context.SaveChanges();
}
}
#endregion
}
[Table("Booking")]
public class Booking
{
[Key]
[Required]
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public virtual long Id { get; set; }
[ForeignKey("CreatedBy")]
[Required]
public virtual long CreatedById { get; set; }
public virtual User CreatedBy { get; set; }
[ForeignKey("ModifiedBy")]
[Required]
public virtual long ModifiedById { get; set; }
public virtual User ModifiedBy { get; set; }
}
[Table("User")]
public class User
{
[Key]
[Required]
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public virtual long Id { get; set; }
public virtual string Name { get; set; }
}
class TestContext : DbContext
{
public TestContext()
{
}
public TestContext(DbConnection connection)
: base(connection, true)
{
}
public IDbSet<Booking> BookingEntities
{
get { return Set<Booking>(); }
}
public IDbSet<User> UserEntities
{
get { return Set<User>(); }
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
Database.SetInitializer<TestContext>(null);
}
}
}
Comments: Investigation analysis: This repros and does seem to be a bug. It happens both with change tracking proxies and with lazy loading proxies with an appropriate DetectChanges inserted. It is limited to situations where two reference navigation properties both point to the same entity. That is, not just the same entity type, but actually the same entity. Also, the entity must not be loaded before changing one of the FKs. When one FK is changed and then an attempt to lazy load _the other relationship_ is made this load will fail.
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Common;
using System.Data.Entity;
using System.Data.SqlClient;
using System.Linq;
using System.Transactions;
namespace Testing
{
/// <summary>
/// This is a test to reproduce the lazy loading bug (EF6 alpha 3)
/// The issue occurs when there is an entity linked to the same lookup table twice using 2 reference properties. If both reference are initially equal and then one is modified, the other does not lazy load and returns null.
/// Note: You can find attached the exact database used during this test
/// </summary>
[TestClass]
public class EfTests
{
const string connectionStr = @"Data Source=(local);Database=TestContext;Integrated Security=SSPI;";
/// <summary>
/// This is succeeding and would rule out stuff such as missing proxy creation requirements
/// </summary>
[TestMethod]
public void LinkedToDifferentUserLazyLoadingTest()
{
using (var ts = new TransactionScope(TransactionScopeOption.Required, new TimeSpan(1, 0, 0)))
{
InsertSampleData();
var context = new TestContext(new SqlConnection(connectionStr));
//Load booking linked to different users
var target = (from x in context.BookingEntities where x.CreatedById != x.ModifiedById && x.ModifiedById != 3 && x.CreatedById != 3 select x).First();
target.ModifiedById = 3;
//This is succeeding
Assert.IsNotNull(target.CreatedBy);
}
}
/// <summary>
/// This is failing in EF5 and EF6 alpha3
/// </summary>
[TestMethod]
public void LinkedToSameUserLazyLoadingBugTest()
{
using (var ts = new TransactionScope(TransactionScopeOption.Required, new TimeSpan(1, 0, 0)))
{
InsertSampleData();
var context = new TestContext(new SqlConnection(connectionStr));
//Load booking linked to single user
var target = (from x in context.BookingEntities where x.CreatedById == x.ModifiedById && x.ModifiedById != 3 && x.CreatedById != 3 select x).First();
target.ModifiedById = 3;
//This is failing
Assert.IsNotNull(target.CreatedBy);
}
}
#region Sample Data
void InsertSampleData()
{
using(var context = new TestContext(new SqlConnection(connectionStr)))
{
//Create 3 users
var user1 = context.UserEntities.Create();
user1.Id = 1;
user1.Name = "User 1";
context.UserEntities.Add(user1);
var user2 = context.UserEntities.Create();
user2.Id = 2;
user2.Name = "User 2";
context.UserEntities.Add(user2);
var user3 = context.UserEntities.Create();
user3.Id = 3;
user3.Name = "User 3";
context.UserEntities.Add(user3);
//Create booking linked to same user
var booking = context.BookingEntities.Create();
context.BookingEntities.Add(booking);
booking.Id = 1;
booking.CreatedBy = user1;
booking.ModifiedBy = user1;
//Create booking linked to different users
var booking2 = context.BookingEntities.Create();
context.BookingEntities.Add(booking2);
booking2.Id =2;
booking2.CreatedBy = user1;
booking2.ModifiedBy = user2;
context.SaveChanges();
}
}
#endregion
}
[Table("Booking")]
public class Booking
{
[Key]
[Required]
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public virtual long Id { get; set; }
[ForeignKey("CreatedBy")]
[Required]
public virtual long CreatedById { get; set; }
public virtual User CreatedBy { get; set; }
[ForeignKey("ModifiedBy")]
[Required]
public virtual long ModifiedById { get; set; }
public virtual User ModifiedBy { get; set; }
}
[Table("User")]
public class User
{
[Key]
[Required]
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public virtual long Id { get; set; }
public virtual string Name { get; set; }
}
class TestContext : DbContext
{
public TestContext()
{
}
public TestContext(DbConnection connection)
: base(connection, true)
{
}
public IDbSet<Booking> BookingEntities
{
get { return Set<Booking>(); }
}
public IDbSet<User> UserEntities
{
get { return Set<User>(); }
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
Database.SetInitializer<TestContext>(null);
}
}
}
Comments: Investigation analysis: This repros and does seem to be a bug. It happens both with change tracking proxies and with lazy loading proxies with an appropriate DetectChanges inserted. It is limited to situations where two reference navigation properties both point to the same entity. That is, not just the same entity type, but actually the same entity. Also, the entity must not be loaded before changing one of the FKs. When one FK is changed and then an attempt to lazy load _the other relationship_ is made this load will fail.