Home > Net >  Dynamically Test All Entity Framework Core DbContext DbSet<> objects with xUnit
Dynamically Test All Entity Framework Core DbContext DbSet<> objects with xUnit

Time:11-10

I'm using xUnit and FluentAssertions to write an integration test to validate that our models are correctly mapped. We have dozens of EF contexts and for each context, there are one or more DbSet<> properties like this:

public class SomeContext : DbContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder builder) { // ... }

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

They are all laid out the same way so this would be an ideal candidate for using [MemberData] to get each context dynamically as input to the test and the DbSet<> property, invoke it, and just make sure it doesn't fail doing a simple query. The Container.GetInstance() call is a call to my DI container to get the actual DbContext object:

public class ContextTests
{
    public static IEnumerable<object[]> EntitiesMappedInCode =>
        typeof(DbContextInitializer).Assembly.GetTypes()
            .Where(t => t.IsSubclassOf(typeof(DbContext)) && !t.IsAbstract)
            .SelectMany(dbContextType =>
            {
                var context = (DbContext) Container.GetInstance(dbContextType);
                var entities = context.Model.GetEntityTypes();
                var dbSetProperties = context.GetType().GetProperties().Where(p =>
                    typeof(DbSet<>).IsAssignableFrom(p.PropertyType.GetGenericTypeDefinition()));
                
                return dbSetProperties.Select(dbSet => new [] {context, dbSet.GetValue(context)});
            });


    [Theory]
    [MemberData(nameof(EntitiesMappedInCode))]
    public void EntitiesDefinedInCode_ExistsInDatabase(DbContext context, object dbSetObject)
    {
        var dbSet = dbSetObject as DbSet<dynamic>; 
        dbSet.Invoking(dbSet => Queryable.FirstOrDefault<dynamic>(dbSet))
            .Should().NotThrow<SqlException>("the entity framework model should match the database");
    }
}

The problem is the reflection does not work properly to give back the runtime instance and fails at p.PropertyType.GetGenericTypeDefinition() with an error.

Has anyone dynamically retrieved the DbSet<> properties of contexts and successfully invoked queries on them?

CodePudding user response:

Exception is thrown because GetGenericTypeDefinition() does not work on non-generic types, so you should first check if type is actually generic before invoking it:

var dbSetProps = typeof(HwContext).GetProperties().Where(c => 
        c.PropertyType.IsGenericType && 
        typeof(DbSet<>).IsAssignableFrom(c.PropertyType.GetGenericTypeDefinition()));

Also do not cast to DbSet<dynamic>, just use dynamic:

foreach (var prop in dbSetProps)
{
    dynamic dbSet = prop.GetValue(context);
    // this should not throw
    Queryable.FirstOrDefault(dbSet);                
}

CodePudding user response:

Thanks to Evk I was able to get it working like this:

public class ContextTests
{
    public static IEnumerable<object[]> EntitiesMappedInCode =>
        typeof(DbContextInitializer).Assembly.GetTypes()
            .Where(t => t.IsSubclassOf(typeof(DbContext)) && !t.IsAbstract)
            .SelectMany(dbContextType =>
            {
                var dbSetProps = dbContextType.GetProperties()
                    .Where(c => c.PropertyType.IsGenericType 
                                && typeof(DbSet<>).IsAssignableFrom(c.PropertyType.GetGenericTypeDefinition()));
                var context = Container.GetInstance(dbContextType);
                return dbSetProps.Select(dbSetProp => new [] {context, dbSetProp.GetValue(context)});
            });


    [Theory]
    [MemberData(nameof(EntitiesMappedInCode))]
    public void EntitiesDefinedInCode_ExistsInDatabase(DbContext context, dynamic dbSet)
    {
        context.Invoking(dbContext => Queryable.FirstOrDefault(dbSet))
            .Should().NotThrow<SqlException>("the entity framework model should match the database");
    }
}

  • Related