4

Current situation

Hello, I have a dotnet standard library in which I use EF Core 2.1.1 (code first approach) to access the persistence layer. To create migrations I use a separate dotnet core console application (in the same solution) that contains an IDesignTimeDbContextFactory<T> implementation.
It is necessary to seed some data and I want to realize it in a comfortable way because in future the data to seed will be extended or rather modified. Therefore in implemented IEntityTypeConfiguration I use the extension method .HasData() which gets an array of objects to seed. The array will be provided from a separate class (TemplateReader), which loads the objects from a JSON file (in which extending and modifying work will be done). Hence it is possible to modify the JSON file's content and add a new migration that will contain generated code to Insert (modelBuilder.InsertData()), Update (modelBuilder.UpdateData()) or Delete (modelBuilder.DeleteData()) statements.
Because I will not ship the JSON file and I want to avoid loading the serialized data for seeding and the execution of .HasData(), I want to use a bool value that will be given into DbContext by the constructor.
To avoid bool value usage if it is not necessary to call the seeding for migration (and .HasData()) I have an overloaded constructor implemented with default value false. Furthermore, I will not use OnConfiguring because I want to be flexible to set up DbContextOptions<T> object in my IoC container or separately for tests.


Code

The following code contains renamed variables to be more anonym about project's content, but represents current implemented logic.

MyDesignTimeDbContextFactory:

public class MyDesignTimeDbContextFactory : IDesignTimeDbContextFactory<MyDbContext>
{
    public MyDbContext CreateDbContext(string[] args)
    {
        var connectionString = ConfigurationManager.ConnectionStrings["SqlServer"].ConnectionString;

        var contextOptionsBuilder = new DbContextOptionsBuilder<MyDbContext>()
            .UseSqlServer(connectionString);

        return new MyDbContext(contextOptionsBuilder.Options, true);
    }
}

MyDbContext:

public sealed class MyDbContext : DbContext
{
    private readonly bool _shouldSeedData;


    public DbSet<Content> Contents { get; set; }

    public DbSet<Template> Templates { get; set; }


    public MyDbContext(DbContextOptions<MyDbContext> options, bool shouldSeedData = false) :
        base(options)
    {
        ChangeTracker.LazyLoadingEnabled = false;

        _shouldSeedData = shouldSeedData;
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.HasDefaultSchema("mySchema");

        modelBuilder.ApplyConfiguration(new TemplateTypeConfiguration(_shouldSeedData));

        base.OnModelCreating(modelBuilder);
    }
}

TemplateTypeConfiguration:

public class TemplateTypeConfiguration : IEntityTypeConfiguration<Template>
{
    private readonly bool _shouldSeedData;


    public TemplateTypeConfiguration(bool shouldSeedData)
    {
        _shouldSeedData = shouldSeedData;
    }


    public void Configure(EntityTypeBuilder<Template> builder)
    {
        builder.Property(p => p.ModuleKey)
            .IsRequired();

        builder.Property(p => p.Html)
            .IsRequired();


        if (_shouldSeedData)
        {
            // reads all templates from configuration file for seeding
            var templateReader = new TemplateReader(Directory.GetCurrentDirectory());

            var templates = templateReader.GetTemplates().ToArray();

            builder.HasData(templates);
        }
    }
}

Template (entity):

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

    public string ModuleKey { get; set; }

    public string Html { get; set; }

    public virtual ICollection<Content> Contents { get; set; }
}

Problem

As far as I know and I already tested the OnModelCreating(ModelBuilder) will be called before constructor's bool value will be set in the constructor (_shouldSeedData = shouldSeedData;). That's because the base constructor will be called immediately, followed by mine. Hence _shouldSeedData's value is false when it will be given into TemplateTypeConfiguration.
Because of that, an Add-Migration results in an "empty" migration without any logic, if I have any modified the above-mentioned JSON file.


Already tested approaches

I already tried to use IModelCacheKeyFactory with an own ModelCacheKey object without any success. As the template, I used this SO-question.

Another approach I tested was to set up _shouldSeedData as public static variable and setting it from MyDesignTimeDbContextFactory to true, but in my opinion, it's a very dirty solution that I want to avoid to implement in production code.

It should be also possible to use DbContextOptionsBuilder<T>'s UseModel(IModel) extension method to avoid the usage of OnModelCreating and initializing TemplateTypeConfiguration with needed shouldSeedData = false. A disadvantage of this approach is to have duplicated code that will differ in TemplateTypeConfiguration's constructor value. In my opinion as nasty as the public static approach.


Question

Is there a clean solution to achieve to set _shouldSeedData by the constructor that OnModelCreating could use it with correct value (true) on design time?
In production it should be false and mentioned TemplateReader in TemplateTypeConfiguration should not be called because of if-condition.

Keyur Ramoliya
  • 1,900
  • 2
  • 16
  • 17
ChW
  • 3,168
  • 2
  • 21
  • 34

1 Answers1

4

OnModelCreating is not triggered by the base DbContext constructor call, so there is absolutely no problem of saving the passed arguments to class members.

In your concrete scenario, the OnModelCreating is triggered by the accessing the ChangeTracker property before storing the passed argument:

public MyDbContext(DbContextOptions<MyDbContext> options, bool shouldSeedData = false) :
    base(options)
{
    ChangeTracker.LazyLoadingEnabled = false; // <--

    _shouldSeedData = shouldSeedData;
}

Simply exchange the lines and the problem will be solved. And in general, always initialize your class members before accessing any context property.

Ivan Stoev
  • 195,425
  • 15
  • 312
  • 343
  • Well, you're right! I had no idea that calling the `ChangeTracker` will result in calling `OnModelCreating`. Do you have an article, book or something else with detailed information about such internal matter? Thanks a lot! – ChW Jul 04 '18 at 12:07
  • 1
    Nope. Almost everything in EF Core is lazy initialized on first access, so it's virtually impossible to document every place which can trigger the model creation. That's why for safety reasons I would assume not calling / accessing any property / method before storing your own stuff. – Ivan Stoev Jul 04 '18 at 12:13