0
votes

I am trying to use Entity Framework Core to dynamically create tables at runtime.

I have a procedure that creates a class like this:

public class Builder
{
    private readonly AssemblyName _assemblyName;
    
    public Builder(string assemblyName)  
    {  
        this._assemblyName = new AssemblyName(assemblyName);  
    }

    public object CreateObject(string[] propertyNames, Type[] types)
    {
        void CreateConstructor(TypeBuilder typeBuilder) => 
            typeBuilder.DefineDefaultConstructor(MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName);

        TypeBuilder CreateClass()  
        {   
            var moduleBuilderAttribute = 
                TypeAttributes.Public | TypeAttributes.Class |  TypeAttributes.AutoClass |
                TypeAttributes.AnsiClass | TypeAttributes.BeforeFieldInit | TypeAttributes.AutoLayout;
            
            var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(this._assemblyName, AssemblyBuilderAccess.Run);
            ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule("MainModule");  
            TypeBuilder typeBuilder = moduleBuilder.DefineType(this._assemblyName.FullName, moduleBuilderAttribute, null);  
            return typeBuilder;  
        }
        
        void CreateProperty(TypeBuilder typeBuilder, string propertyName, Type propertyType)  
        {  
            FieldBuilder fieldBuilder = typeBuilder.DefineField("_" + propertyName, propertyType, FieldAttributes.Private); 
            PropertyBuilder propertyBuilder = typeBuilder.DefineProperty(propertyName, PropertyAttributes.HasDefault, propertyType, null);  
            
            var propertyBuilderAttributes = MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig;
            MethodBuilder getPropertyMethodBuilder = 
                typeBuilder.DefineMethod("get_" + propertyName, propertyBuilderAttributes, propertyType, Type.EmptyTypes);  
            ILGenerator getGenerator = getPropertyMethodBuilder.GetILGenerator();  
  
            getGenerator.Emit(OpCodes.Ldarg_0);  
            getGenerator.Emit(OpCodes.Ldfld, fieldBuilder);  
            getGenerator.Emit(OpCodes.Ret);  
  
            MethodBuilder setPropertyMethodBuilder =
                typeBuilder.DefineMethod("set_" + propertyName,  propertyBuilderAttributes, null, new[] { propertyType });
            ILGenerator setGenerator = setPropertyMethodBuilder.GetILGenerator();  
            Label modifyProperty = setGenerator.DefineLabel();  
            Label exitSet = setGenerator.DefineLabel();  
  
            setGenerator.MarkLabel(modifyProperty);  
            setGenerator.Emit(OpCodes.Ldarg_0);  
            setGenerator.Emit(OpCodes.Ldarg_1);  
            setGenerator.Emit(OpCodes.Stfld, fieldBuilder);  
  
            setGenerator.Emit(OpCodes.Nop);  
            setGenerator.MarkLabel(exitSet);  
            setGenerator.Emit(OpCodes.Ret);  
  
            propertyBuilder.SetGetMethod(getPropertyMethodBuilder);  
            propertyBuilder.SetSetMethod(setPropertyMethodBuilder);  
        }
        
        if(propertyNames.Length != types.Length)  
        {  
            throw new Exception("The number of property names should match their corresponding types number");
        }

        TypeBuilder dynamicClass = CreateClass();
        CreateConstructor(dynamicClass);

        for (int index = 0; index < propertyNames.Count(); index++)
        {
            CreateProperty(dynamicClass, propertyNames[index],types[index]); 
        }
        
        Type? type = dynamicClass.CreateType();

        if (type == null)
        {
            throw new Exception("Problem with creating dynamic class");
        }
        
        return Activator.CreateInstance(type); 
    }
}

And I use it like:

var classBuilder = new Builder("ClassName");
var dynamicClass= enumEntityClassBuilder.CreateObject(new [] { "id", "value" }, new [] {typeof(int), typeof(string)});

So now when I check type of generated class (using dynamicClass.GetType()) I see it is class named ClassName.

Next step is I want to create context of postresql database so:

const string connectionString = "Server=localhost;Database=Test;User Id=postgres;Password=postgres";
var options = new DbContextOptionsBuilder<Context>();
options.UseNpgsql(connectionString);
var dbContext = new Context(options.Options, new List<object> { dynamicClass });

And context setup, in witch probably is source of the problem:

public class Context : DbContext
{
    private readonly IEnumerable<object> _dynamicTypes;

    public Context(DbContextOptions options, IEnumerable<object> dynamicTypes) : base(options)
    {
        this._dynamicTypes = dynamicTypes;
    }
    
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder.UseNpgsql();
    
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        try
        {
            var methodBindingFlags = BindingFlags.Public | BindingFlags.Instance;
            var entityMethod = 
                typeof(ModelBuilder)
                    .GetMethods(methodBindingFlags)
                    .Single(m => m.Name=="Entity" && m.GetParameters().Length == 0);
            // HERE I WANT TO ADD DYNAMIC GENERATED CLASSES AS TABLES
            //BUT IT DOES NOT WORK
            foreach (var type in this._dynamicTypes)
            {
                entityMethod?.MakeGenericMethod(type.GetType()).Invoke(modelBuilder, Array.Empty<object>());
            }
            base.OnModelCreating(modelBuilder);
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex);
        }
    }
}

And simple migration method:

public class Repository
{
    private readonly Context _dbContext;

    public Repository(Context dbContext)
    {
        this._dbContext = dbContext;
    }

    public async Task CreateDataBase()
    {
        await using (_dbContext)
        {
            try
            {
                await _dbContext.Database.MigrateAsync();
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex);
            }
        }
    }
}

So my problem is in context setup I want to add my dynamic generated classes to create tables in migration process. So is it possible, or what am I missing?

Why are you doing this? A DbContext-derived class is a Unit-of-Work for a specific use case/scenario, not a database connection. It makes little sense to have a Unit-of-Work with arbitrary tables. Besides, the DbSets of a DbContext are dynamics. The properties are just placeholders making it easier to use DbContext as a Unit-of-Work (and cache metadata, although EF Core allows this even for the Set<T>() and Set(Type) method now).Panagiotis Kanavos
In any case, if you already have types you can try using Set(Type) to create the DbSets and configure them. DbContext already uses reflection to inspect types, apply conventions, read attributes to construct metadata, queries and migrations. If neither classes nor tables are available until runtime though, why use EF at all? Your own code won't benefit from strong typing. You might as well use plain old DataTable or use Dapper with dynamic objects for data manipulation, and generate the CREATE TABLE statements from your actual metadata directly.Panagiotis Kanavos
What's the underlying problem you're trying to solve? What's the reason for working with dynamic classes? And are you aware of the fact that EF stores the compiled model only one per application life time? (Well, that's practically true, there are ways to renew the model). Either way, if you really need this dynamic architecture you better look at a tool like Dapper.Gert Arnold