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?
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 theCREATE TABLE
statements from your actual metadata directly. – Panagiotis Kanavos