Removing Cascade-Delete Conventions
jsoules opened this issue · comments
This tool looks like exactly what I need to test an existing app that's gone without tests for far too long. However, I'm running into an issue with a test in which Entity thinks there's a possible cycle with the default cascade delete convention.
In the DbContext
class I'm mocking, I have an OnModelCreating()
that removes ManyToManyCascadeDeleteConvention
and OneToManyCascadeDeleteConvention
, so the code works fine in practice. However, the tests are failing due to the potential cyclical cascade delete.
What's your recommended way to turn off these cascade-delete conventions for the mocked DbContext
?
Thanks for your help.
Hi, thanks you for reaching out. Would it be possible to create an isolated unit-test (or small repro) that fails for your scenario?
Thanks for your response! I was working up code to repro and having a lot of issues with the in-memory DB connection thinking the model had changed--odd, since there was no persistent database in the reproducing test error--so I decided to retest in the real application, and now the same code is passing. I didn't change anything, but if I can't reproduce it, there's nothing to share.
Thanks for your help--I'll chalk this one up to gremlins and reach out again if I find something I can actually reproduce reliably.
Hi,
I'm not able to reproduce exactly the issue that I was experiencing, however I'm seeing a range of tests that sometimes work and sometimes don't with no code changes and a variety of errors. I think ultimately the error messages are not accurate, and there's just something not quite working correctly with a local SQLite DB.
This originally appeared as a complaint about foreign-key relationships and the cascade-delete convention potentially causing delete cycles. However, I think it's more general instability because I was not able to reproduce that error message, and I closed this issue when the problem disappeared without me changing anything. This morning, however, various problems are back again.
Here's an example of a test that fails--I included more of the data model than strictly necessary because I was originally trying to repro a FK error. But now the same test is failing because of a login failure to the database--odd, since the DbContext is supposed to be a mock... also the same code was passing when I closed the issue on Tuesday.
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.ModelConfiguration.Conventions;
using System.Data.Entity.Validation;
using System.Linq;
using System.Linq.Expressions;
using System.Runtime.Serialization;
using EntityFrameworkMock;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Text;
namespace ConsoleApp1
{
public static class MainClass
{
public static void Main()
{
Database.SetInitializer<MyContext>(null);
Console.Write("Hello World");
}
}
[Serializable] // custom exception class expected to be thrown
public class RecordNotFoundException : Exception
{
public RecordNotFoundException() { }
public RecordNotFoundException(string msg) : base(msg) { }
public RecordNotFoundException(string msg, Exception inner) : base(msg, inner) { }
protected RecordNotFoundException(SerializationInfo info, StreamingContext ctxt) : base(info, ctxt) { }
}
// Data Models
public class Fair
{
[Key]
public int FairId { get; set; }
[Required]
public int Year { get; set; }
[Required]
public int DefaultInterviewLength { get; set; }
[Required]
public int InterviewBreakLength { get; set; }
}
public class Employer
{
[Key] public int EmployerId { get; set; }
[Required] public string Name { get; set; }
}
public class EmployerFair
{
[Key]
public int EmployerFairId { get; set; }
[Required]
public Employer Employer { get; set; }
[Required]
public Fair Fair { get; set; }
[Required]
public int InterviewLength { get; set; }
[Required]
public EmployerRepresentative PrimaryContact { get; set; }
}
public class EmployerRepresentative
{
[Key]
public int EmployerRepresentativeId { get; set; }
[Required, Index(IsUnique = true), StringLength(40)]
public string Guid { get; set; }
public DateTime StatusChangeDate { get; set; }
}
public class Opportunity
{
[Key]
public int OpportunityId { get; set; }
[Required]
public Employer Employer { get; set; }
[Required]
public Fair Fair { get; set; }
[Required]
public string Title { get; set; }
[Required]
public string Description { get; set; }
public Reservation Reservation { get; set; }
}
public class Reservation
{
public int ReservationId { get; set; }
[Required]
public Employer Employer { get; set; }
public IList<Opportunity> Opportunities { get; set; }
}
// DBContext and its interface
public interface IMyContext : IDisposable
{
DbSet<Fair> Fairs { get; set; }
DbSet<Opportunity> Opportunities { get; set; }
int SaveChanges();
DbSet Set(Type entityType);
DbEntityEntry Entry(object entity);
DbSet<T> Set<T>() where T : class;
}
public class MyContext : DbContext, IMyContext
{
public MyContext() : this("MyContext")
{ }
public MyContext(string connString) : base(connString)
{
Database.SetInitializer<MyContext>(null);
}
public virtual DbSet<Fair> Fairs { get; set; }
public virtual DbSet<Opportunity> Opportunities { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Conventions.Remove<ManyToManyCascadeDeleteConvention>();
modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>();
}
}
// REPO UNDER TEST
public class FairRepository
{
public FairRepository(IMyContext ctxt)
{
Context = ctxt;
}
protected IMyContext Context { get; }
public virtual int Count()
{
return Context.Set<Fair>().Count();
}
public virtual IQueryable<Fair> FindAll()
{
var query = Context.Set<Fair>().AsQueryable();
return query;
}
public virtual Fair FindById(int id)
{
var q = Context.Set<Fair>();
var result = q.Where(IdMatchesModelLambda(id)).ToList();
var msg = $"Error retrieving {typeof(Fair)} with key {id}\n";
if (!result.Any())
throw new RecordNotFoundException(msg);
return result.First();
}
public virtual int Commit()
{
try
{
return Context.SaveChanges();
}
catch (DbEntityValidationException validationEx)
{
foreach (var validationErrs in validationEx.EntityValidationErrors)
{
var errMsg = new StringBuilder("Validation failed for entities:\n");
foreach (var validationErr in validationErrs.ValidationErrors)
errMsg.AppendFormat("\tProperty: {0} Error: {1}\n",
validationErr.PropertyName, validationErr.ErrorMessage);
}
throw;
}
}
private Expression<Func<Fair, bool>> IdMatchesModelLambda(int id)
{
var objectContext = ((IObjectContextAdapter)Context).ObjectContext;
var set = objectContext.CreateObjectSet<Fair>();
var primaryKey = set.EntitySet.ElementType.KeyMembers
.Select(k => k.Name)
.First();
var modelType = Expression.Parameter(typeof(Fair));
var modelId = Expression.MakeMemberAccess(modelType, typeof(Fair).GetProperty(primaryKey));
var idToCompare = Expression.Constant(id);
var equalityCheck = Expression.Equal(modelId, idToCompare);
var inputMatchesModelLambda = Expression.Lambda<Func<Fair, bool>>(equalityCheck, modelType);
return inputMatchesModelLambda;
}
}
// UNIT TEST ITSELF
[TestClass]
public class RepositoryTest
{
private FairRepository _repo;
[TestInitialize()]
public void SetUp()
{
var seedVals = new[]
{
new Fair
{
FairId = 1,
Year = 2017,
DefaultInterviewLength = 20,
InterviewBreakLength = 5
},
new Fair
{
FairId = 2,
Year = 2018,
DefaultInterviewLength = 21,
InterviewBreakLength = 4
}
};
var mock = new DbContextMock<MyContext>("fake connection2");
var _ = mock.CreateDbSetMock(x => x.Fairs, seedVals);
_repo = new FairRepository(mock.Object);
}
[TestMethod]
public void TestCount() // WORKS
{
var count = _repo.Count();
Assert.IsTrue(count == 2);
}
[TestMethod, ExpectedException(typeof(RecordNotFoundException))]
public void TestRecordNotFoundById() // FAILS, with various errors
{
_repo.FindById(id: 500);
}
}
}
I got same error and I resolved with moq.protected.
mockContext.Protected().Setup("OnModelCreating", ItExpr.IsAny<DbModelBuilder>())
.Callback<DbModelBuilder>((DbModelBuilder model) =>
model.Conventions.Remove<OneToManyCascadeDeleteConvention>());