How to validatie EF Core DBContext config in an unit test
Asked Answered
N

1

8

Currently when there is a mistake in the config of the DBContext model, we get an error at run-time. For example:

The entity type 'MyObject' requires a primary key to be defined. If you intended to use a keyless entity type call 'HasNoKey()'.

This error is throw when EF Core is used for the first time in the application. I would like to validate the model in an unit test.

What I have tried

I noticed the ModelValidator.Validate, in the stacktrace so I tried this:

[TestMethod]
public void MyDbContext_DoesNotThrowExceptions()
{
    // Arrange
    var myDbContext = CreateMyDbContext();
    IModel model = myDbContext.Model;

    var validator = new ModelValidator(new ModelValidatorDependencies(???,???)) // [EntityFrameworkInternal] public ModelValidatorDependencies([NotNull] ITypeMappingSource typeMappingSource,[NotNull] IMemberClassifier memberClassifier)

    // Act
    validator.Validate(model, ???); // public virtual void Validate(IModel model,IDiagnosticsLogger<DbLoggerCategory.Model.Validation> logger)
}

Unfortunately I don't now how to create the ModelValidator / ModelValidatorDependencies or what to pass as IDiagnosticsLogger - see ??? in code. Also the [EntityFrameworkInternal] gives me the idea that I have the wrong approach.

Any one an idea how to fix this unit test? (By creating the ModelValidator or by another approach)

Details

The full stack trace of the error:

Test method Foo threw exception: 
System.InvalidOperationException: The entity type 'MyEntity' requires a primary key to be defined. If you intended to use a keyless entity type call 'HasNoKey()'.
   at Microsoft.EntityFrameworkCore.Infrastructure.ModelValidator.ValidateNonNullPrimaryKeys(IModel model, IDiagnosticsLogger`1 logger)
   at Microsoft.EntityFrameworkCore.Infrastructure.ModelValidator.Validate(IModel model, IDiagnosticsLogger`1 logger)
   at Microsoft.EntityFrameworkCore.Infrastructure.RelationalModelValidator.Validate(IModel model, IDiagnosticsLogger`1 logger)
   at Microsoft.EntityFrameworkCore.SqlServer.Internal.SqlServerModelValidator.Validate(IModel model, IDiagnosticsLogger`1 logger)
   at Microsoft.EntityFrameworkCore.Metadata.Conventions.ValidatingConvention.ProcessModelFinalized(IConventionModelBuilder modelBuilder, IConventionContext`1 context)
   at Microsoft.EntityFrameworkCore.Metadata.Conventions.Internal.ConventionDispatcher.ImmediateConventionScope.OnModelFinalized(IConventionModelBuilder modelBuilder)
   at Microsoft.EntityFrameworkCore.Metadata.Conventions.Internal.ConventionDispatcher.OnModelFinalized(IConventionModelBuilder modelBuilder)
   at Microsoft.EntityFrameworkCore.Metadata.Internal.Model.FinalizeModel()
   at Microsoft.EntityFrameworkCore.ModelBuilder.FinalizeModel()
   at Microsoft.EntityFrameworkCore.Infrastructure.ModelSource.CreateModel(DbContext context, IConventionSetBuilder conventionSetBuilder)
   at Microsoft.EntityFrameworkCore.Infrastructure.ModelSource.GetModel(DbContext context, IConventionSetBuilder conventionSetBuilder)
   at Microsoft.EntityFrameworkCore.Internal.DbContextServices.CreateModel()
   at Microsoft.EntityFrameworkCore.Internal.DbContextServices.get_Model()
   at Microsoft.EntityFrameworkCore.Infrastructure.EntityFrameworkServicesBuilder.<>c.<TryAddCoreServices>b__7_3(IServiceProvider p)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitFactory(FactoryCallSite factoryCallSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine, RuntimeResolverLock lockType)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScopeCache(ServiceCallSite singletonCallSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitConstructor(ConstructorCallSite constructorCallSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine, RuntimeResolverLock lockType)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScopeCache(ServiceCallSite singletonCallSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.Resolve(ServiceCallSite callSite, ServiceProviderEngineScope scope)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.DynamicServiceProviderEngine.<>c__DisplayClass1_0.<RealizeService>b__0(ServiceProviderEngineScope scope)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngine.GetService(Type serviceType, ServiceProviderEngineScope serviceProviderEngineScope)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngineScope.GetService(Type serviceType)
   at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(IServiceProvider provider, Type serviceType)
   at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService[T](IServiceProvider provider)
   at Microsoft.EntityFrameworkCore.DbContext.get_DbContextDependencies()
   at Microsoft.EntityFrameworkCore.DbContext.get_InternalServiceProvider()
   at Microsoft.EntityFrameworkCore.DbContext.Microsoft.EntityFrameworkCore.Infrastructure.IInfrastructure<System.IServiceProvider>.get_Instance()
   at Microsoft.EntityFrameworkCore.Infrastructure.Internal.InfrastructureExtensions.GetService[TService](IInfrastructure`1 accessor)
   at Microsoft.EntityFrameworkCore.Infrastructure.AccessorExtensions.GetService[TService](IInfrastructure`1 accessor)
   at Microsoft.EntityFrameworkCore.Infrastructure.DatabaseFacade.get_Dependencies()
   at Microsoft.EntityFrameworkCore.Infrastructure.DatabaseFacade.Microsoft.EntityFrameworkCore.Internal.IDatabaseFacadeDependenciesAccessor.get_Dependencies()
   at Microsoft.EntityFrameworkCore.RelationalDatabaseFacadeExtensions.GetFacadeDependencies(DatabaseFacade databaseFacade)
   at Microsoft.EntityFrameworkCore.RelationalDatabaseFacadeExtensions.ExecuteSqlRaw(DatabaseFacade databaseFacade, String sql, IEnumerable`1 parameters)
   at Microsoft.EntityFrameworkCore.RelationalDatabaseFacadeExtensions.ExecuteSqlRaw(DatabaseFacade databaseFacade, String sql, Object[] parameters)
   at myCode

Solution

With soms tweaks (EF Core 3 validate method has 2 parameters), Vasil's answer worked for me. I have now:

[TestMethod]
public void MyDbContext_DoesNotThrowExceptions()
{
    // Arrange
    var serviceProvider = CreateServiceProvider();

    var validator = serviceProvider.GetService<IModelValidator>();
    var context = serviceProvider.GetService<MyDbContext>();
    var logger = serviceProvider.GetService<IDiagnosticsLogger<DbLoggerCategory.Model.Validation>>();

    // Act
    validator.Validate(context.Model, logger);
}

private static ServiceProvider CreateServiceProvider()
{
    var serviceCollection = new ServiceCollection()
        .AddEntityFrameworkSqlServer()
        .AddDbContext<MyDbContext>((sp, options) => options
            .UseSqlServer(new SqlConnection()));
    return serviceCollection.BuildServiceProvider();
}

Nonstriated answered 14/1, 2020 at 10:53 Comment(0)
C
9

I've looked around in the EFCore github repository and this is the best i've managed to get working.

var serviceCollection = new Microsoft.Extensions.DependencyInjection.ServiceCollection();
serviceCollection.AddEntityFrameworkSqlServer().AddDbContext<Context>();
var serviceProvider = serviceCollection.BuildServiceProvider();
var validator = serviceProvider.GetService<IModelValidator>();
var context = serviceProvider.GetService<Context>();

validator.Validate(context.Model); 

All the type used are coming from the ef namespace or from packages which ef depends on, so you should be able to get it running without issues.

I've tested it with single entity without primary key and it throw exception 'The entity type 'XXXXX' requires a primary key to be defined', so this should be pretty close to what ef is doing when executing the first query.

Counterpoint answered 14/1, 2020 at 13:48 Comment(2)
I was wrong before, I do need the AddEntityFrameworkSqlServer. Cached test result I think.Nonstriated
@Nonstriated Yeah, it adds all the builders, validators .. ectCounterpoint

© 2022 - 2024 — McMap. All rights reserved.