ひっそりと生きるプログラマのブログ

日頃気になった事なりを書き留めるブログです。関心ごとは多くもう少し更新頻度を上げたいところです。

【dnlib】DLL(.NET assembly/module) からちょっとした情報を抽出したい(1)

ソースを解析する際に、人力でコードを追う方法以外に、
DLL のメタ情報から欲しい情報を抽出出来ないかと調べた所、
↓のような素敵なライブラリが合ったので試しに使ってみました。
github.com
www.nuget.org

まずは、nuget にて dnlib をインストール。
ライブラリ自体は dll への read/write 両方とも出来るようですが、
一先ず、色々と抽出したいので read に絞って実験。

private static IEnumerable<Tuple<MethodDef, IMethodDefOrRef>> GetUsedMethodsAndProperties(IEnumerable<string> moduleFiles)
{
    // func を再帰呼び出しする為に定義と実装を分離する。(ここは定義)
    Func<MethodDef, HashSet<MethodDef>, IEnumerable<Tuple<MethodDef, IMethodDefOrRef>>>? func = null;

    // ここは実装
    func = (method, hashSet) =>
    {
        var result = new List<Tuple<MethodDef, IMethodDefOrRef>>();
        // 無限ループになる可能性がある為、1度処理したメソッドは処理しない。
        if (method == null || hashSet.Contains(method)) return result;
        hashSet.Add(method);

        var methodBody = method.Body;
        if (methodBody == null) return result;

        foreach (var calledMethodOrProperty in methodBody.Instructions
            .Where(m => m.Operand is IMethodDefOrRef)
            .Select(m => (IMethodDefOrRef)m.Operand))
        {
            // 出力したい条件を↓に指定する。
            // ToString を呼んでいる処理を抽出
            if (calledMethodOrProperty.Name.Contains("ToString"))
                result.Add(Tuple.Create(method, calledMethodOrProperty));
            result.AddRange(func!(calledMethodOrProperty.ResolveMethodDef(), hashSet));
        }
        return result;
    };

    var h = new HashSet<MethodDef>();
    return moduleFiles
        .Select(m => ModuleDefMD.Load(m))
        .SelectMany(m => m.GetTypes())
        .SelectMany(m => m.Methods)
        .SelectMany(m => func(m, h));
}

GetUsedMethodsAndProperties の引数は解析したい DLL のフルパス。
こんな感じで、一先ず欲しい情報は抽出出来た。
これからは、 example や readme を参考にもう少し調べてみる。

【Entity Framework】LINQ と Oracle の関数でマップされていないものを利用する(3)※固定値を関数に引数にとる場合

saboten-sakura.hatenablog.com

今回は前回より複雑なSQLを発行します。
TO_CHAR(DATE, 'D') で、指定した日付が何曜日かが取れます。
今回は上記の関数を投げて、特定の曜日のみ抽出する処理を目標とします。

流れとしては、途中まで1回目の流れです。
saboten-sakura.hatenablog.com

.net のメソッドを定義(Oracle 関数とマッピング用)

返す型は、 TO_CHAR なので string を返します。
引数は、TO_CHAR 2で二つなんですが、第1引数は任意の値で、第2引数は'D'を渡します。
なので、 .net では DateTime を引数として渡します。

public static string DayOfWeekString(this DateTime? value) => throw new NotImplementedException();

.net で定義したメソッドを Oracle 関数とマッピング

GetMethod で定義した MethodInfo を取得します。

var method = typeof(StringExtension).GetMethod(nameof(StringExtension.DayOfWeekString), new Type[] { typeof(DateTime) })!;

取得した MethodInfo と Oracle の関数を定義付けします。

modelBuilder.HasDbFunction(method).HasTranslation(translation =>
new SqlFunctionExpression(
    "TO_CHAR",
    new SqlExpression[]
    {
        new SqlConstantExpression(
            Expression.Constant("D"),
            new StringTypeMapping(
                "VARCHAR2",
                DbType.String)),
        translation.First()
    },
    true,
    new bool[]
    {
        false,
        false
    },
    typeof(string),
    new StringTypeMapping(
        "VARCHAR2",
        DbType.String)
    )
);

上記を定義した後に、以下のコードを実装。

var context = new SampleDbContext();
var items = (from m in context.SampleTables 
             where
                m.SampleDate.DayOfWeekString() == "3"
             select m);
foreach (var item in items)
{
    Console.WriteLine($"{item.Id} - {item.SampleDate?.ToString("ddd")}");
}

で、出力した SQL がこちら。

SELECT "s"."ID", "s"."SAMPLEDATE", "s"."SAMPLENUMBER"
FROM "SAMPLETABLE" "s"
WHERE TO_CHAR("s"."SAMPLEDATE", 'D') = '3' 

【Entity Framework】LINQ と Oracle の関数でマップされていないものを利用する(2)※Select句で利用する。

saboten-sakura.hatenablog.com

投稿のちょっとした続きです。
前回は Where句 に独自関数を追加しましたが、
今回は Select句 で Oracle の関数を呼んでみます。

var context = new SampleDbContext();
var items = (from m in context.SampleTables 
             where 
             m.SampleNumber.Sqrt() > 3 && 
             m.SampleNumber.Sqrt() < 7 
             select new { test1 = m.SampleNumber, test2 = m.SampleNumber.Sqrt() });
foreach (var item in items)
{
    // SampleNumber の表示と、 SampleNumber の平方根を表示。
    Console.WriteLine($"{item.test1} - {item.test2}");
}

で、出力した SQL がこちら。

SELECT "s"."ID", "s"."SAMPLENUMBER", "SQRT"("s"."SAMPLENUMBER") "test2"
FROM "SAMPLETABLE" "s"
WHERE (("SQRT"("s"."SAMPLENUMBER") > 3) AND ("SQRT"("s"."SAMPLENUMBER") < 7)) 

【Entity Framework】LINQ と Oracle の関数でマップされていないものを利用する(1)

LINQ 正規関数にマッピングされていない Oracle 関数の使い方。
幾つか実装方法はありますが、簡単な方法から。

環境

  • Visual Studio 2022
  • .net 6.0
  • EF Core 6.0.29
  • Oracle EF Core 6.21.1.40
  • NLog (ログを確認の為、出力)

実装

主要部分のみで、内容に意味は無いです。
"こうすれば動くよ!"という事を抜粋して記載します。

流れとしては以下の通りです。

  1. .net のメソッドを定義(Oracle 関数とマッピング用)
  2. .net で定義したメソッドを Oracle 関数とマッピング
  3. .net のメソッドを利用し SQL を発行し結果を確認

.net のメソッドを定義(Oracle 関数とマッピング用)

.net のメソッドをサクッと定義
平方根を算出する "SQRT" とマッピング予定

public static class StringExtension
{
    public static int Sqrt(this int? value) => 
        throw new NotImplementedException();
}

.net で定義したメソッドを Oracle 関数とマッピング

DbContext を継承し OnModelCreating を override します。
.net で定義したメソッドの MethodInfo を取得します。
HasDbFunction の引数に渡して、HasName で何の関数にマッピングするか設定。
※.net のメソッドと Oracle 関数が 1:1 なのでこれで十分

シグネチャが同じであれば、"SQRT" 以外のOracle関数でもマッピング可能。
.net のメソッドと Oracle の関数は役割が近く名前も似ていた方が把握し易くなりますね。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    var sqrt = typeof(StringExtension).GetMethod("Sqrt", new Type[] { typeof(int) })!;
    modelBuilder.HasDbFunction(sqrt).HasName("SQRT");
    base.OnModelCreating(modelBuilder);
}

.net のメソッドを利用し SQL を発行し結果を確認

エンティティのメンバとか全無視して期待した SQL が発行されるか確認。

// SampleNumber の値に、 2, 16, 40, 6423 が設定されたレコード有
// 平方根の結果が、3より大きく 7未満の場合
var context = new SampleDbContext();
var items = (from m in context.SampleTables 
             where 
             m.SampleNumber.Sqrt() > 3 && 
             m.SampleNumber.Sqrt() < 7 
             select m);
foreach (var item in items)
{
    // 16
    // 40
    Console.WriteLine(item.SampleNumber);
}

上記で発行された SQL をログ出力した結果

SELECT "s"."ID", "s"."SAMPLENUMBER"
FROM "SAMPLETABLE" "s"
WHERE (("SQRT"("s"."SAMPLENUMBER") > 3) AND ("SQRT"("s"."SAMPLENUMBER") < 7)) 

マッピングされ期待した結果が無事取得出来ました。
ユーザーが独自に定義した関数も可能だったり、
.net のメソッドと Oracle 関数を 1:n でマッピングだったり出来るので気分が向いたら追記します。

【Entity Framework】テーブルの主キーの値を渡して、レコードを取得する方法(ナビゲーションの値も含む)

以下のような実装で取得可能。

次の実装は呼び出し元

SampleTable というテーブルがあり、
主キーに相当するプロパティが SampleId になります。
SampleSubTable と SampleTable はナビゲーションプロパティが定義しています。

static void Main(string[] args)
{
    SampleTable data = null;
    using (var context = new Sample01Entities())
    {
        context.Configuration.ProxyCreationEnabled = false;
        data = GetSampleTableData(context, 2);
    }
    Console.WriteLine(data.SampleId);
    Console.WriteLine(data.SampleName);
    Console.WriteLine(data.SampleSubTable.Count);
    Console.ReadLine();
}

private static SampleTable GetSampleTableData(
    DbContext context, int key1)
{
    return DbContextUtility.GetData<SampleTable>(
        context,
        new SampleTable()
        {
            SampleId = key1
        });
}

次の実装は呼び出し先
リファクタリングの余地はあるが、
実現したいことはできていると思います。

public static class DbContextUtility
{
    #region EntityType
    private static Dictionary<Type, EntityType> entityTypes =
        new Dictionary<Type, EntityType>();

    private static EntityType GetEntityType(DbContext context, Type type)
    {
        EntityType result;
        if (entityTypes.TryGetValue(type, out result)) return result;
        var objectContext = ((IObjectContextAdapter)context).ObjectContext;
        var metadata = objectContext.MetadataWorkspace;
        result = metadata.GetItem<EntityType>(type.FullName, DataSpace.OSpace);
        entityTypes.Add(type, result);
        return result;
    }
    #endregion

    public static T GetData<T>(
        DbContext context, T entity) where T : class
    {
        var type = typeof(T);
        var entityType = GetEntityType(context, type);
        var keyProperties = entityType.KeyMembers.Select(k => type.GetProperty(k.Name));
        var values = keyProperties.Select(m => m.GetValue(entity)).ToArray();
        var result = (T)context.Set<T>().Find(values);
        GetEntity(context, result, new HashSet<object>());
        return (T)result;
    }

    private static void GetEntity(
        DbContext context, object entity, HashSet<object> values)
    {
        if (values.Contains(entity)) return;
        values.Add(entity);
        var type = entity.GetType();
        var entityType = GetEntityType(context, type);
        var navproperties = entityType.NavigationProperties.Select(
            m => entity.GetType().GetProperty(m.Name));
        foreach (var nav in navproperties)
        {
            if (nav.PropertyType.IsGenericType)
            {
                context.Entry(entity).Collection(nav.Name).Load();
                var enumerable = (IEnumerable)nav.GetValue(entity);
                foreach(var e in enumerable)
                    GetEntity(context, e, values);
            }
            else
            {
                context.Entry(entity).Reference(nav.Name).Load();
                var value = nav.GetValue(entity);
                GetEntity(context, value, values);
            }
        }
    }
}

【Entity Framework】Set やら Find やら使って、ナビゲーションプロパティも含めてDBから取得する方法

調べた結果、できそうな雰囲気。
眠いので今週中位に記事を書く予定。

EntityType から、いろいろな情報が取得できるので、
面白い事ができそうです。

DB(SQL ServerOracle)に、
依存しない形で汎用的な部分を作りこめるのはうれしいですね。

【ASP.NET MVC】モデルのバインドに関して (2)

前回投稿した記事で、
モデルのバインドに関して調べました。
saboten-sakura.hatenablog.com

以下のように複数の Name が存在した場合。

@using (Html.BeginForm("Send", "Home", null, FormMethod.Post))
{
    <input type="text" name="a" />
    <input type="checkbox" name="a" value="true" />
    <input type="hidden" name="a" value="false" />
    <input type="submit" value="送信" />
}

以下のような実装をする事でフォーム上の値が変数にバインドされます。
※引数の型が配列になっています。

public class HomeController : Controller
{
    public ActionResult Send(bool[] a)
    {
        return this.Index();
    }
}

bool 型なので、 true or false のみですが、
string 型 や int 型などでもキャスト可能であれば利用可能です。