Add-Migration with seeds throwing "no value provided" exception

.net-core asp.net-core-2.1 c# ef-core-2.1 entity-framework-core

Question

I am trying to create a package that will help seed databases with data from JSON files which can be created manually or extracted from a legacy database. Currently I have created a generic class SeedCreator<T> which retrieves JSON from a given file (the name of the entity plus .json) and deserialises it into an object of the given type. This part is working fine.

To make this process as dynamic as possible I am using reflection on the entities in the project by identifying all classes with the namespace Entities. With this, I am looping through the retrieved List and checking if the JSON file exists. If it does I pass the path and the generic type to the class SeedsCreator. When debugging whilst running Add-Migration, the data comes back as I'd expect from the JSON file but after the variable modelBuilder gets returned I get the error The seed entity for entity type 'Table1' cannot be added because there was no value provided for the required property 'Id'.

If I manually put in the following it works fine.

modelBuilder.Entity(typeof(Table1)).HasData(data);

Any help would be greatly appreciated. Especially if I'm blind and have done something very simple and stupid.

public class Seeds
{
    public ModelBuilder CreateSeeds(ModelBuilder modelBuilder)
    {
        var entities = (from t in Assembly.GetExecutingAssembly().GetTypes()
                       where t.Namespace != null && (t.IsClass && t.Namespace.Contains("Entities"))
                       select t).ToList();

        foreach (var type in entities)
        {
            if (File.Exists("./Seeds/" + type.Name + ".json"))
            {
                Type[] typeArr = { type };
                var seeds = typeof(SeedCreator<>).MakeGenericType(typeArr);
                var activatedSeeds = Activator.CreateInstance(seeds);
                var data = seeds.GetMethod("GetSeeds")?.Invoke(activatedSeeds, new object[] { "./Seeds/" + type.Name + ".json" });
                modelBuilder.Entity(type).HasData(data);
            }
        }

        return modelBuilder;
    }
}

public class SeedCreator<T>
{
    public List<T> GetSeeds(string jsonPath)
    {
        using (var sr = new StreamReader(jsonPath))
            return JsonConvert.DeserializeObject<List<T>>(sr.ReadToEnd());
    }
}

public class Table1
{
    public int Id { get; set; }
}

Inside the DbContext

using (var dataSeed = new Seeds()) modelBuilder = dataSeed.CreateSeeds(modelBuilder);

Example JSON file (Table1.json)

[
  {
    "id": 1
  },
  {
    "id": 2
  }
]

Stack trace

System.InvalidOperationException: The seed entity for entity type 'Table1' cannot be added because there was no value provided for the required property 'Id'. at Microsoft.EntityFrameworkCore.Infrastructure.ModelValidator.ValidateData(IModel model) at Microsoft.EntityFrameworkCore.Infrastructure.ModelValidator.Validate(IModel model) at Microsoft.EntityFrameworkCore.Infrastructure.RelationalModelValidator.Validate(IModel model) at Microsoft.EntityFrameworkCore.Internal.SqlServerModelValidator.Validate(IModel model) at Microsoft.EntityFrameworkCore.Metadata.Conventions.Internal.ValidatingConvention.Apply(InternalModelBuilder modelBuilder) at Microsoft.EntityFrameworkCore.Metadata.Conventions.Internal.ConventionDispatcher.ImmediateConventionScope.OnModelBuilt(InternalModelBuilder modelBuilder) at Microsoft.EntityFrameworkCore.ModelBuilder.FinalizeModel() at System.Lazy`1.ViaFactory(LazyThreadSafetyMode mode) at System.Lazy`1.ExecutionAndPublication(LazyHelper executionAndPublication, Boolean useDefaultConstructor) at System.Lazy`1.CreateValue() at Microsoft.EntityFrameworkCore.Internal.DbContextServices.CreateModel() at Microsoft.EntityFrameworkCore.Internal.DbContextServices.get_Model() at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScoped(ScopedCallSite scopedCallSite, ServiceProviderEngineScope scope) at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitConstructor(ConstructorCallSite constructorCallSite, ServiceProviderEngineScope scope) at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScoped(ScopedCallSite scopedCallSite, ServiceProviderEngineScope scope) 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.Internal.InternalAccessorExtensions.GetService[TService](IInfrastructure`1 accessor) at Microsoft.EntityFrameworkCore.Design.Internal.DbContextOperations.CreateContext(Func`1 factory) at Microsoft.EntityFrameworkCore.Design.Internal.DbContextOperations.CreateContext(String contextType) at Microsoft.EntityFrameworkCore.Design.Internal.MigrationsOperations.AddMigration(String name, String outputDir, String contextType) at Microsoft.EntityFrameworkCore.Design.OperationExecutor.AddMigrationImpl(String name, String outputDir, String contextType) at Microsoft.EntityFrameworkCore.Design.OperationExecutor.OperationBase.<>c__DisplayClass3_0`1.<Execute>b__0() at Microsoft.EntityFrameworkCore.Design.OperationExecutor.OperationBase.Execute(Action action)

1
2
12/20/2018 10:50:12 AM

Accepted Answer

You are falling into typical params object[] trap.

The HasData method has 2 overloads - one with params object[] data and one with IEnumerable<object> data. Due to the reflection call, the type of your data variable is object. Hence you are calling the first overload with single item object[] containing your data.

If you are wondering why the exception message is telling you that no value is provided for a property, the explanation is simple. HasData does not require the passed objects to be of the same type as the entity being seeded. This is to allow specifying the shadow properties which does not exist in the entity class, but are required for data seeding. So it allows you to pass any anonymous or concrete type which contains all the entity properties.

Hence it's trying to reflect the actual type of the passed objects and find a property Id. Since the actual type of the passed single object in your case is List<TEntity>, of course it has no Id property, hence the exception message.

With all that being said, the fix is of course to call the correct HasData overload (with IEnumerable<object> data):

modelBuilder.Entity(type).HasData((IEnumerable<object>)data);
7
12/20/2018 10:44:22 AM


Related Questions





Related

Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow