Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue running test cases with DbContext using ValueConverters depending on singleton object. #35284

Open
abhisheksaraiya opened this issue Dec 6, 2024 · 0 comments
Assignees

Comments

@abhisheksaraiya
Copy link

abhisheksaraiya commented Dec 6, 2024

When using ValueConverters which depend on singleton objects, I get a runtime error when run in test cases as described below.
In my code below I am trying to restore a property from a singleton service using HasConversions. If i were to run one of the test cases below, it would succeed.
However, if both the test cases were to run one after the other, the second one would fail even though they are entirely independent tests which create their own DbContexts.

code

public class MyDbContext : DbContext
{
    private readonly MySingletonConsentService _singletonConsentService;

    public MyDbContext(DbContextOptions<MyDbContext> options, MySingletonConsentService singletonConsentService) :
        base(options)
    {
        _singletonConsentService = singletonConsentService;
    }

    public DbSet<User> Users { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseInMemoryDatabase("db");
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<User>().HasKey(x => x.Name);
        modelBuilder.Entity<User>().Property(x => x.Consent)
            .HasConversion(c => c.Code, code => _singletonConsentService.GetConsent(code));
    }
}

public class User
{
    public string Name { get; set; }
    public ConsentDef Consent { get; set; }
}

public record ConsentDef(string Code);

public class MySingletonConsentService
{
    private List<ConsentDef> _consents;

    public MySingletonConsentService(List<ConsentDef> consents)
    {
        _consents = consents;
    }
    
    public ConsentDef GetConsent(string code)
    {
        return _consents.First(x => x.Code == code);
    }
}

public class UnitTests
{
    [Fact]
    public void CreateUserAndGetUser()
    {
        var consentService = new MySingletonConsentService([new ConsentDef("Consent1"), new ConsentDef("Consent2")]);
        // Arrange
        var user = new User()
        {
            Name = "John Doe",
            Consent = consentService.GetConsent("Consent1")
        };

        var myDbContext = new MyDbContext(new DbContextOptionsBuilder<MyDbContext>().Options, consentService);
        myDbContext.Users.Add(user);
        myDbContext.SaveChanges();

        var myDbContext2 = new MyDbContext(new DbContextOptionsBuilder<MyDbContext>().Options, consentService);
        var firstOrDefault = myDbContext2.Users.FirstOrDefault(x => x.Name == "John Doe");
        Assert.NotNull(firstOrDefault);
    }
    
    [Fact]
    public void CreateUserAndGetUser2()
    {
        var consentService = new MySingletonConsentService([new ConsentDef("Consent3"), new ConsentDef("Consent4")]);
        // Arrange
        var user = new User()
        {
            Name = "John Doe2",
            Consent = consentService.GetConsent("Consent3")
        };

        var myDbContext = new MyDbContext(new DbContextOptionsBuilder<MyDbContext>().Options, consentService);
        myDbContext.Users.Add(user);
        myDbContext.SaveChanges();

        var myDbContext2 = new MyDbContext(new DbContextOptionsBuilder<MyDbContext>().Options, consentService);
        var firstOrDefault = myDbContext2.Users.FirstOrDefault(x => x.Name == "John Doe2");
        Assert.NotNull(firstOrDefault);
    }
}

Note that the values within the singleton are purposely different in both the test cases in order to show the issue.
It seems that somehow, the DbContext object within the second test case has still retained the singleton object used in the value converter of the first test case and therefore now it cant find the new set of values it expects.

This however works if i were to add a IMaterializationInterceptor with an empty overload of InitializedInstance even if it does nothing in the overload.

I dont understand how this is possible. Is this a bug ?

This is error I get from one of the tests. This happens because the DbContext from the second test has still retained the singleton used in the ValueConverter from the first test.

System.InvalidOperationException: Sequence contains no matching element

System.InvalidOperationException
Sequence contains no matching element
   at System.Linq.ThrowHelper.ThrowNoMatchException()
   at System.Linq.Enumerable.First[TSource](IEnumerable`1 source, Func`2 predicate)
   at MySingletonConsentService.GetConsent(String code) in T:\SoftwareRepo\RiderProjects\EFCoreConversionTests\EFCoreConversionTests\MyDbContext.cs:line 51
   at Microsoft.EntityFrameworkCore.Storage.ValueConversion.ValueConverter`2.<>c__DisplayClass7_0`2.<SanitizeConverter>b__1(Object v)
   at Microsoft.EntityFrameworkCore.InMemory.Storage.Internal.InMemoryTable`1.SnapshotRows()
   at Microsoft.EntityFrameworkCore.InMemory.Storage.Internal.InMemoryStore.GetTables(IEntityType entityType)
   at Microsoft.EntityFrameworkCore.InMemory.Query.Internal.InMemoryQueryContext.GetValueBuffers(IEntityType entityType)
   at lambda_method63(Closure)
   at Microsoft.EntityFrameworkCore.InMemory.Query.Internal.InMemoryQueryExpression.ResultEnumerable.GetEnumerator()
   at Microsoft.EntityFrameworkCore.InMemory.Query.Internal.InMemoryShapedQueryCompilingExpressionVisitor.QueryingEnumerable`1.Enumerator.MoveNextHelper()
   at Microsoft.EntityFrameworkCore.InMemory.Query.Internal.InMemoryShapedQueryCompilingExpressionVisitor.QueryingEnumerable`1.Enumerator.MoveNext()
   at System.Linq.Enumerable.TryGetSingle[TSource](IEnumerable`1 source, Boolean& found)
   at lambda_method62(Closure, QueryContext)
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.ExecuteCore[TResult](Expression query, Boolean async, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.Execute[TResult](Expression query)
   at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryProvider.Execute[TResult](Expression expression)
   at EFCoreConversionTests.UnitTests.CreateUserAndGetUser() in T:\SoftwareRepo\RiderProjects\EFCoreConversionTests\EFCoreConversionTests\UnitTests.cs:line 24
   at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor)
   at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)

EF Core version:
Database provider: Microsoft.EntityFrameworkCore.InMemory (9.0.0)
Target framework: NET 9.0
Operating system: Windows 11
IDE: Rider

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants