やりたいこと
下記のようなコードがある。
public abstract record Format {
}
[PrimaryKey(nameof(id))]
public abstract record BaseFormat : Format {
public int id { get; init; }
public string text { get; init; }
}
ここから、下記のようなコードを Reflection.Emit で動的に生成したい。
[Table("table_derived202308", Schema = "oltp")]
public record DerivedFormat202308 : BaseFormat {
}
EntityFrameworkCore を ORM として用いてデータベース接続するのだが、Code First でも Model First でも EntityFrameworkCore を OLAP 目的で使うには2点難点がある。
- 複雑な集合演算が苦手
- テーブル (に紐づけるデータコンテナの型) を動的に作成できない
まあ、1点目は仕方ない。EntityFrameworkCore はメモリと DB 間の激しい相互作用に耐える ORM として設計されていて、テーブル構成をオブジェクト構成に見立てる方向 (Repository Pattern ORM) を目標としている *1 から。すべてを DB サーバーサイドで演算する業務・分析系はクエリビルダを使えということなのだろう。それはそれで不満 *2*3 があって、解消するために自分なりのソリューションを構築したが、それはまた別途紹介する。
ここでの問題は2点目。コード (データコンテナの型) を静的に書かなければならない? いやいや、OLAP のビッグデータとか 1 テーブルでは済まないから、分割統治するためにパラメータ与えて複数のテーブルをシステマティックに作成したいですけど? 年間 n 億レコードの会計データなんか溜め込むとしたら、期間分割して年月でテーブル分けしたいと思うのは当然。この CREATE TABLE を人手なんかでは書けませんって。テーブル数も多いし、1つ1つのテーブルだって単純構造ではなく、多段階テーブル・パーティショニングした上で末端テーブルにインデックス付けたりするから SQL は長くなる。これは自動化案件。
要するに EntityFrameworkCore で Model First 的にデータコンテナの型を起点に (でも実際は .ExecuteSqlRaw("CREATE TABLE ...") で) OLAP データキューブ・テーブルを動的に作りたいぞーということ。
というわけで、Reflection.Emit で動的に派生クラス作ってみるぞー。
結果
結果できたのがコレ。
using System;
using System.ComponentModel.DataAnnotations.Schema;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
public static class DynamicFormatCreator {
private static string AssemblyName { get; } = "Assembly";
private static string ModuleName { get; } = nameof(DynamicFormatCreator);
private static string NameSpace { get => typeof(DynamicFormatCreator).Namespace is {} ns ? $"{ns}." : string.Empty; }
public static Type CreateFormat<TBaseFormat>(string derivedTypeName, string tableName, string schemaName) where TBaseFormat : Format {
var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName(AssemblyName), AssemblyBuilderAccess.Run);
var moduleBuilder = assemblyBuilder.DefineDynamicModule(ModuleName);
var baseType = typeof(TBaseFormat);
var derivedTypeBuilder = moduleBuilder
.DefineType(derivedTypeName, TypeAttributes.Public, baseType)
.SetTableAttribute(tableName, schemaName);
baseType.DefineCtor(derivedTypeBuilder, isSelfRef: true ).As(out var derivedCtorBuilder);
baseType.DefineCtor(derivedTypeBuilder, isSelfRef: false);
baseType.DefineCloneMethod(derivedTypeBuilder, derivedCtorBuilder);
return derivedTypeBuilder.CreateType();
}
private static ConstructorBuilder DefineCtor(this Type baseType, TypeBuilder derivedTypeBuilder, bool isSelfRef) {
var baseCtorArg = isSelfRef ? new Type[] { baseType } : Type.EmptyTypes;
var derivedCtorArg = isSelfRef ? new Type[] { derivedTypeBuilder } : null ;
var ctorBuilder = derivedTypeBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, derivedCtorArg);
var il = ctorBuilder.GetILGenerator();
var baseCtor = baseType
.GetConstructor(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, null, baseCtorArg, null)
?? throw new NotImplementedException($"Constructor .ctor({(isSelfRef ? baseType.Name : string.Empty)}) on {baseType.Name} was not found.");
il.Emit(OpCodes.Ldarg_0);
if (isSelfRef)
il.Emit(OpCodes.Ldarg_1);
il.Emit(OpCodes.Call, baseCtor);
il.Emit(OpCodes.Ret);
return ctorBuilder;
}
private static MethodBuilder DefineCloneMethod(this Type baseType, TypeBuilder derivedTypeBuilder, ConstructorBuilder derivedCtorBuilder) {
var cloneMethodName = "<Clone>$";
var cloneMethodBuilder = derivedTypeBuilder
.DefineMethod(cloneMethodName, MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig, derivedTypeBuilder, null)
.SetCtorAttribute<PreserveBaseOverridesAttribute>()
.SetCtorAttribute<CompilerGeneratedAttribute>();
var il = cloneMethodBuilder.GetILGenerator();
var baseCloneMethod = baseType
.GetMethod(cloneMethodName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
?? throw new NotImplementedException($"Method {cloneMethodName}() on {baseType.Name} was not found.");
derivedTypeBuilder
.DefineMethodOverride(cloneMethodBuilder, baseCloneMethod);
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Newobj, derivedCtorBuilder);
il.Emit(OpCodes.Ret);
return cloneMethodBuilder;
}
private static TypeBuilder SetTableAttribute(this TypeBuilder typeBuilder, string tableName, string schemaName) {
var attribType = typeof(TableAttribute);
var attribCtor = attribType.GetConstructor(new[] { typeof(string) }) ?? throw GetException_("Constructor .ctor(string)");
var attribProperty = attribType.GetProperty(nameof(TableAttribute.Schema)) ?? throw GetException_("Property .Schema");
var attribBuilder = new CustomAttributeBuilder(attribCtor, new object[] { tableName }, new[] { attribProperty }, new object[] { schemaName });
typeBuilder.SetCustomAttribute(attribBuilder);
return typeBuilder;
static Exception GetException_(string message) =>
new NotImplementedException($"{message} on the attribute '{nameof(TableAttribute)}' was not found.");
}
private static MethodBuilder SetCtorAttribute<TAttribute>(this MethodBuilder methodBuilder) where TAttribute : Attribute {
var attrib = typeof(TAttribute);
var ctor = attrib.GetConstructor(Type.EmptyTypes)
?? throw new NotImplementedException($"Constructor .ctor() on {attrib.Name} was not found.");
methodBuilder.SetCustomAttribute(new CustomAttributeBuilder(ctor, new object[] {}));
return methodBuilder;
}
}
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
public static ExtensionGeneric {
public static T1 As<T1>(this T1 target, out T1 val1) =>
val1 = target;
public static string Join(this IEnumerable<object> source, string delimiter) =>
string.Join(delimiter, source.Select( x => x.ToString() ));
public static TFormat New<TFormat>(this Type type) where TFormat : Format =>
type.IsAssignableTo(typeof(TFormat)) switch {
true => (TFormat)(type.GetDefaultInstance() ?? throw new NotImplementedException($"Default constructor on {type.Name} wan not found.")),
_ => throw new ArgumentException($"{type.Name} is not assignable to {typeof(TFormat).Name}."),
};
public static string RegexReplace(this string target, string pattern, Func<string[], string> replacer, RegexOptions option = RegexOptions.None) =>
Regex.Replace(target, pattern, new MatchEvaluator( m => replacer(m.Groups.Cast<Group>().Select( x => x.ToString() ).ToArray()) ), option);
public static object? GetDefaultInstance(this Type type) =>
type.GetConstructor(Type.EmptyTypes)?.Invoke(null);
public static string ToStructureString(this Type type) =>
(type.GetCustomAttribute<TableAttribute>() switch {
{} attrib => new[] { $"[Table(\"{attrib.Name}\", Schema = \"{attrib.Schema}\")]" },
_ => Enumerable.Empty<string>(),
})
.Append($"{type} : {$"{type.BaseType}".RegexReplace(@"^.*?([^\.]+)$", m => m[1])} {{")
.Concat(type
.GetProperties()
.Select( x => $" public {x[0]} {x[1]} {{ get; init; }}" ))
.Append("}")
.Join(Environment.NewLine);
}
var type = DynamicFormatCreator.CreateFormat<BaseFormat>("DerivedFormat202308", "table_derived202308", "olap");
var inst = type.New<BaseFormat>() with {
id = 1,
text = "test",
};
Console.Error.WriteLine(type.ToStructureString());
Console.Error.WriteLine(inst);