Programming w/ C# ~ ちょっとおもしろいパターンマッチ記法の発見

発見

ちょっとおもしろいパターンマッチ記法を発見をした。

ネスト構造の任意データ型の文字列化

ネスト構造の任意データ型の要素1つ1つに再帰である処理を掛けてして文字列化したく、こんな処理を書いてみた。

public static IEnumerable<string> ToStringRecursive(this IEnumerable<object?> source) =>
  source
    .Select( x => x switch {
      IEnumerable<object?> seq => $"({seq.ToStringRecursive().Join(", ")})",
      _                        => x?.ToString() ?? "NULL", // 本当はココにもう少し複雑な処理が入る
    } );

要素が単体ならば _ に落ちて x?.ToString() ?? "NULL" で処理する、要素がさらにシーケンスになっているならば再帰して1つの文字列にしたものを () で囲む、というつもり。

ところが、int 配列 new[] { 1, 2, 3 } をこれに掛けて処理しようとすると、IEnumerable<object?> にパターンマッチさせる意図に反して _ に落ち、int 配列型デフォルトのメソッド Type.ToString() が掛かって System.Int32[] という出力になってしまう。

Chat GPT 先生に聞いて調べてみると、IEnumerable<object?> の型引数 object? は何でも受けられる基底かと思いきや、なんと、ジェネリックの場合は参照型にしか効かず共変性が失われるのだとか。

そこで、値型・参照型無差別な IEnumerable にパターンマッチさせるにはジェネリックにしなければよいのだ、とこのように改善すると int 配列はうまく通る。

public static IEnumerable<string> ToStringRecursive(this IEnumerable<object?> source) =>
  source
    .Select( x => x switch {
      IEnumerable seq => $"({seq.Cast<object?>().ToStringRecursive().Join(", ")})",
      _               => x?.ToString() ?? "NULL",
    } );

new[] { 1, 2, 3 }.ToStringRecursive();

// Result 
// (1, 2, 3)

階層構造データのテスト

階層構造データもうまくいくぞ。よしよし。

new object[] { 1, 2, 3, new[] { 4, 5, 6 } }.ToStringRecursive();

// Result 
// (1, 2, 3, (4, 5, 6))

文字列を含めたテスト

文字列も要素に加えてみるか。あれ!? なんで X が一段階よけいに階層化されるんだ ... あっ!! そうか ...

new object[] { 1, 2, "X", new[] { 4, 5, 6 } }.ToStringRecursive();

// Result 
// (1, 2, (X), (4, 5, 6))

文字列 string 型は char[] とも解釈されうるから IEnumerable にひっかかってしまうのか ... 2文字以上で試してみるとやはり。

new object[] { 1, 2, "Test", new[] { 4, 5, 6 } }.ToStringRecursive();

// Result 
// (1, 2, (T, e, s, t), (4, 5, 6))

おもしろい記法

ではこう書かなければ。string 型のときは除くなんて、こんな形のパターンマッチ記法は初めて見た。

public static IEnumerable<string> ToStringRecursive(this IEnumerable<object?> source) =>
  source
    .Select( x => x switch {
      IEnumerable seq when x is not string => $"({seq.Cast<object?>().ToStringRecursive().Join(", ")})",
      _                                    => x?.ToString() ?? "NULL",
    } );

new object[] { 1, 2, "Test", new[] { 4, 5, 6 } }.ToStringRecursive();

// Result 
// (1, 2, Test, (4, 5, 6))

Programming w/ C# ~ 抽象 record データコンテナ・テンプレートから派生クラスを Reflection.Emit で動的生成

抽象 record データコンテナ・テンプレートから派生クラスを Reflection.Emit で動的生成

やりたいこと

下記のようなコードがある。

public abstract record Format {
  // EntityFrameworkCore で利用する共通機能を書く
  // プロパティは書かない
}

// データコンテナ・テンプレート (例えばこのようなもの. 実際は業務データのフォーマット.)
[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 で動的に派生クラス作ってみるぞー。

難所

新しい型を作るには Reflection.Emit を使う。Reflection.Emit は .NET VM の中間コードを組み立てるもの。これがまあ参考情報がなくて難しい。SharpLab などを使って IL ダンプしてみるけど詳しくはわからない (から Chat GPT に解釈させる)。
record class は難しい。なぜなら、プログラマがコードを書かなくても基本機能を C# コンパイラがコード自動生成するものだから。要はコンパイラがやるコード補完を (部分的に) 再現するということ。
継承が難しい。派生クラスから自クラスや基底クラスのコンストラクタやクローンメソッドをどうコールしているか (よく見えないオートマトンを) よく洞察・想像しないと再現できない。
Chat GPT の扱いが難しい。Chat GPT はコード書きは速いが、世間であまり手掛けられていないこなれていない領域に対する知恵はほとんど持ち合わせていない。

試行錯誤すること丸 3 日。Chat GPT にコード書きさせること 20 数回。こちらが期待していることを Chat GPT が汲んでくれなくて堂々巡りすること 5 巡。こちらが知恵を絞り、手を変え品を変え、いろんな角度からちょっとずつヒントを与えて、ほんの少しだけ正解に近づいたかなと思える部分・部分をこそぎ取ってこちらで組み合わせて捏ねていく作品作り ...

結果

結果できたのがコレ。

using System;
using System.ComponentModel.DataAnnotations.Schema;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;

public static class DynamicFormatCreator {
  // static property ***************************************************************************************************

  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; }

  // static method *****************************************************************************************************

  // 基底 (抽象) クラスである Format のテンプレートから具体クラスを動的に生成する
  // 下記が生成される
  //
  //   [Table($tableName$, Schema = $schemaName$])]
  //   public class $formatTypeName$ : TBaseFormat {}
  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); // 自己参照型コンストラクタ new TDerived(TBase that)
    baseType.DefineCtor(derivedTypeBuilder, isSelfRef: false);                                // デフォルトコンストラクタ new TDerived()

    // 派生クラスの <Clone>$ メソッドを作成する
    baseType.DefineCloneMethod(derivedTypeBuilder, derivedCtorBuilder);

    return derivedTypeBuilder.CreateType();
  }

  // 派生クラスのコンストラクタビルダーを作成し、基底クラスのコンストラクタを呼び出す IL コードを生成する
  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;
  }

  // 派生クラスのメソッドビルダーを作成し、派生クラスのコンストラクタを呼び出す IL コードを生成する
  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>()         // <Clone>$ メソッドにコンストラクタ系属性を追加する
      .SetCtorAttribute<CompilerGeneratedAttribute>();
    var il                 = cloneMethodBuilder.GetILGenerator(); // IL ジェネレーターを取得し、<Clone>$ メソッドの本体を生成する
    var baseCloneMethod    = baseType                             // <Clone>$ メソッドを基底クラスのメソッドとしてオーバーライドする
      .GetMethod(cloneMethodName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
      ?? throw new NotImplementedException($"Method {cloneMethodName}() on {baseType.Name} was not found.");

    derivedTypeBuilder                                            // Override
      .DefineMethodOverride(cloneMethodBuilder, baseCloneMethod);

    il.Emit(OpCodes.Ldarg_0);                                     // IL コードとして、自分自身をロード
    il.Emit(OpCodes.Newobj, derivedCtorBuilder);                  // 新しいインスタンスを作成するためにコンストラクタを呼び出す
    il.Emit(OpCodes.Ret);                                         // 戻り値として返す

    return cloneMethodBuilder;
  }

  // TypeBuilder にテーブル属性を付与する
  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;

    // local method ********************
    static Exception GetException_(string message) =>
      new NotImplementedException($"{message} on the attribute '{nameof(TableAttribute)}' was not found.");
  }

  // MethodBuilder にコンストラクタ系属性を付与する
  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());

// displayed as follows:
//
// [Table("table_derived202308", Schema = "oltp")]
// public record DerivedFormat202308 : BaseFormat {
//  public int    id   { get; init; }
//  public string text { get; init; }
// }

Console.Error.WriteLine(inst);

// displayed as follows:
// 
// DerivedFormat202308 { id = 1, text = test }

ポイント

ポイントはこういうことだった。

  • デフォルトコンストラクタ new TDerivedFormat() を IL で定義するだけではダメ。<Clone>$() という隠れクローンメソッドを定義しなければ。
  • <Clone>$() という隠れクローンメソッドを定義するだけではダメ。それをオーバーライドメソッドとして定義しなければ。
  • <Clone>$() という隠れクローンメソッドをオーバーライド定義するだけではダメ。自己参照型コンストラクタ new TDerivedFormat(TBaseFormat that) を定義しなければ。

まあ、本来の record クラスは Equals() や GetHashCode() なども定義しなければならないため、また、本来の派生クラスは基底の定義に加えてプロパティやメソッドがありこれも定義すべきであるため、本当の継承では書くべきコード量はもっと増えるのだが。
いまのままでは、定義が不足しているメソッドはおそらく基底クラスのそれが用いられる。ここでは、具象クラスであることと Table 属性が付与されていることだけが派生クラスと基底クラスの相違点で、プロパティもメソッドも全く同じである、という形でしか使わないため、ここまでにする。

*1:このトピックはここで考察している。→ PythonのDjangoのデータベース操作。ORMと生Queryどっちを使う方がいいのでしょうか?(ORMで調べても出来ない作業を生Queryでやってもええんやろか)に対するNiwa Yosukeさんの回答 - Quora

*2:クエリビルダの長所は SQL を自由に組めること。対する短所はデータコンテナの型安全性が担保されないテキスト編集であること。ここを改善したのが DbExtensions ライブラリだが、これでもまだ少し不満がある。

*3:DbExtensions を用いてもまだ残る不満は、EntityFrameworkCore の IQueryable<T> のようにデータコンテナ T に対する型安全性と再利用性・応用性が高くないこと。要はプリミティブ型ではなく "データコンテナ T" に着目した関数型プログラミングで Fluent API 的に流れるように書きつつ、それでいて似たデータコンテナやコード片を使い回せるようにしてコード記述量を極小化したい。そうしないとクエリの可読性が向上せず、メンテナンスが大変になる。

Programming w/ PostgreSQL ~ テーブル・レコード更新日時の記録

PostgreSQL のテーブルにレコード更新された日時を記録する実験

仕掛けづくりは下記の通り。

-- [初期設定] スキーマ作成
CREATE SCHEMA sandbox;

-- [初期設定] 観測対象テーブルのレコード最終更新日時を記録するテーブルの定義
CREATE TABLE IF NOT EXISTS sandbox.table_last_modified (
  table_name       TEXT      PRIMARY KEY,
  last_modified_at TIMESTAMP
);

-- [初期設定] レコード更新時に発動する関数の定義
CREATE OR REPLACE FUNCTION sandbox.update_table_last_modified() RETURNS trigger AS $$
BEGIN
  -- レコード更新されたテーブル名と現在時刻を挿入・更新する
  INSERT INTO sandbox.table_last_modified (table_name, last_modified_at) VALUES (TG_TABLE_NAME, CURRENT_TIMESTAMP)
  ON CONFLICT (table_name) DO UPDATE SET last_modified_at = CURRENT_TIMESTAMP;
  RETURN NULL;
END;
$$ LANGUAGE plpgsql;

-- [テーブル作成都度] 観測対象テーブルの定義
CREATE TABLE IF NOT EXISTS sandbox.table_observable (
  id   INTEGER PRIMARY KEY,
  text TEXT
);

-- [テーブル作成都度] 観測対象テーブルに対するトリガーの設定
CREATE TRIGGER sandbox.test_last_modified
AFTER INSERT OR UPDATE OR DELETE ON sandbox.table_observable 
FOR EACH STATEMENT EXECUTE PROCEDURE update_table_last_modified();

実行結果

観測対象テーブルにも更新記録テーブルにも、初めは何も入っていない。

SELECT * FROM sandbox.table_observable;
--|id|text|
--|  |    |

SELECT * FROM sandbox.table_last_modified;
-- |table_name|last_modified_at|
-- |          |                |

観測対象テーブルに1行挿入してみる。

INSERT INTO sandbox.table_observable VALUES (1, 'test');

観測対象テーブルには1行挿入され、更新記録テーブルにもレコード更新の記録が残る。

SELECT * FROM sandbox.table_observable;
--|id|text|
--| 1|test|

SELECT * FROM sandbox.table_last_modified;
-- |table_name      |last_modified_at          |
-- |table_observable|2023-08-11-22:22:22.222222|

利用例

レコード最終更新日時も同時に取得したいとき、このように使ったらよいのではないか。
ただし、最終更新日時カラムの情報は各レコードごとの更新情報ではなく、テーブルのそれである。OLAP ではデータキューブの一括更新時刻を知りたいことがある *1

WITH qry AS (SELECT last_modified_at FROM sandbox.table_last_modified WHERE table_name = 'table_observable')
SELECT tbl.*, (SELECT qry.last_modified_at FROM qry) FROM sandbox.table_observable AS tbl;

-- |id|text|last_modified_at          |
-- | 1|test|2023-08-11-22:22:22.222222|

*1:レコードごとの最終更新日時を保持・参照したいときは観測対象テーブル側にそのようなカラムを用意すればよい。こちらは OLTP でよくやる通常の方法。

Programming w/ PHP ~ PostgreSQL への接続

メモ

<?php
$host   = "";
$dbname = "";
$user   = "";
$passwd = "";

$conn = pg_connect("host=$host dbname=$dbname user=$user password=$passwd");

if (!$conn) {
  $error = pg_last_error(); // エラーの取得
  echo "接続失敗";
  exit;
}

$query = "SELECT * FROM xxx;";
$result = pq_query($conn, $query);

while ($row = pg_fetch_assoc($result)) {
  echo $row['column'] . "<br>";
}

pg_close($conn);
?>

Programming w/ C# ~ プロパティの型 "定義" の抽出方法 (再訂正・改良版)

プロパティの型 "定義" の抽出方法 (訂正・改良版)

やりたいこと

データを record class 等のコンテナクラスで扱う際、特にそれをデータベースにマップする際に、カラムに対応する各プロパティの正確なデータ型 "定義" をプログラム上で知りたいことがある。
これができるとコードファースト / モデルファースト的にコンテナクラスの情報から CREATE TABLE を実行できる。

問題の背景

C# 標準 ORM の Entity Framework Core ではこの辺りのマッピングがよくできていて、プログラマがやらなければいけないことはあまりない。
Entity Framework Core は ORM としては優秀である一方、複雑な集合演算に弱く、クエリビルダーとしては貧弱であることは否めない。
そこで EntityFramework Core のデータコンテナに対する型安全性を活かしつつ、Dapper や DbExtensions といったクエリビルダーの集合演算に対する柔軟性を併せ持つ、型安全クエリビルダー・ライブラリを構築する。
その際に record class のプロパティ定義から正確なデータ型 "定義" を抽出する必要がある。

難所

これは System.Reflection を用いればできるように見えて、一筋縄ではいかない。プロパティ情報はいちおう typeof(TContainer).GetProperties() とすることで取得できるが、取得した型が string である場合、ヌル非許容参照型 string とヌル許容参照型である string? とを識別できないためだ。.NET 仮想マシンはデータ型としては string? 型は string 型として保持しており、両者の区別がないため、実行コード上では (一見) 識別できないということになる。

var obj = new TContainer();

obj.GetType().GetProperties().Select( p => p.GetValue(obj)?.GetType() );

などとしてもダメ。プロパティのインスタンスから取得しても、そのプロパティに割り付けられている値が string か null かを得るだけでプロパティの型 "定義" 情報を得ることはできない。

知りたいのはコンパイル時の型定義であって、実行時のインスタンス情報ではない。

解決方法

このように記述するとプロパティの型定義を Nullable / Non Nullable の違いを含めて正確に取得できる。
プロパティに対し (Nullable を外した型名、Nullable であるか否か) とタプルにした情報を返す。

  public static (Type Type, bool IsNullable) GetTypeExact(PropertyInfo prop) =>
    prop.PropertyType switch {
      var pType when pType.IsEnum                              => (pType                         , false                            ),
      var pType when pType.IsGenericTypeOf(typeof(Nullable<>)) => (pType.GetGenericArguments()[0], true                             ),
      var pType when pType.IsValueType                         => (pType                         , false                            ),
      var pType                                                => (pType                         , IsNullableReferenceProperty(prop)),
    };

  public static bool IsNullableReferenceProperty(PropertyInfo prop) {
    return (GetNullableFlagFirst_(prop) ?? GetNullableContextFlag_(prop)) switch {
        0    => ! prop.PropertyType.IsValueType, // 参照型なら Null 許容, 値型なら Null 非許容 (C# 7 or former 後方互換)
        1    => false                          , // Null 非許容
        2    => true                           , // Null   許容
        null => throw new NotImplementedException("NullableFlags or Flag argument failed to be obtained."),
        _    => throw new NotImplementedException("NullableFlags or Flag instance is an unexpected value (other than 0, 1 or 2)."),
    };

    // local method ********
    // [Nullable(byte[] NulableFlags = new[] { 0 or 1 or 2 })] 属性から 0, 1, 2, null (属性なし) を抽出する
    static byte? GetNullableFlagFirst_(PropertyInfo prop) =>
      (prop.GetCustomAttributes()
        .FirstOrDefault( a => a.GetType().FullName == "System.Runtime.CompilerServices.NullableAttribute" )
          .As(out var attrib)
          ?.GetType().GetField("NullableFlags")
          ?.GetValue(attrib) as byte[])?[0];

    // [NullableContext(byte Flag = 1 or 2)] 属性から 1, 2, null (属性なし) を抽出する (C# 8 では属性付与されないため非有効)
    static byte? GetNullableContextFlag_(PropertyInfo prop) =>
      prop.DeclaringType!.GetCustomAttributes()
        .FirstOrDefault( a => a.GetType().FullName == "System.Runtime.CompilerServices.NullableContextAttribute" )
        .As(out var attrib)
        ?.GetType().GetField("Flag")
        ?.GetValue(attrib) as byte?;
}

public static class Extension {
  // 後方参照用中間変数定義ユーティリティ・拡張メソッド
  public static T1 As<T1>(this T1 target, out T1 val1) =>
    val1 = target;
}

あとは下表のように、得られた型と Nullable / NonNullable 情報を元にデータベースの型へマップしてやるだけ。

C# PostgreSQL
int INTEGER NOT NULL
int? INTEGER
double DOUBLE PRECISION NOT NULL
double? DOUBLE PRECISION
string TEXT NOT NULL
string? TEXT

ポイント

  • string と string? は、型では識別できないため、System.Runtime.CompilerServices.NullableAttribute と .NullableContextAttribute という属性にて識別する。
    • プロパティに NullableAttribute 属性が付されているか否かを調べ、その NullableFlags フィールド (byte[]) の第1要素 *1 を調べる。
      • 0 ならばその要素が値型か参照型かで null 許容/非許容が決まる *2 。値型は非許容、参照型は許容。
      • 1 ならば null 非許容参照型。(string)
      • 2 ならば null 許容参照型。(string?)
    • ただし、NullableAttribute 属性が付されていない場合もある。そのときは当該プロパティを包含するクラスに NullableContextAttribute 属性が付されているか否か *3 を調べ、その Flag フィールド (byte) を調べる。
      • 1 ならば null 非許容参照型。(string)
      • 2 ならば null 許容参照型。(string?)
    • プロパティの NullableAttribute 属性の前にクラスの NullableContextAttribute 属性で定義されている *4 ということなのだろう。
  • ヌル許容値型 (int? など) には属性 NullableAttribute は付与されない。
    • 値型の Nullable / NonNullable 判定には .IsGenericTypeOf(typeof(Nullable<>)) を用い、nullable の原型判定には .GetGenericArguments()[0] を用いる。
  • 属性 NullableAttribute はプロパティに、属性 NullableContextAttribute はそれを包含するクラスに付与され存在しうるが、当該属性はシステム管理用であるため、プログラマが直接操作することはできない。
    • そのため、.GetCustomAttribute<NullableAttribute>() などとして属性クラス指定で判定することはできず、全属性をいったん文字列化してから NullableAttribute や NullableContextAttribute の存在を判定する。

*1:属性のコンストラクタ第1引数を調べてはいけない。フィールドは常に byte[] 型だが、C# 言語バージョンによってコンストラクタは byte[] だったり byte だったりする。仕様変更があったようだ。

*2:これはおそらく C# 7 以前に対する後方互換性。

*3:NullableContextAttribute は C# 8 では機能しない。C# 8 は Nullable の扱いが不完全であった。

*4:ただ、NullableContextAttribute がどのように決まり、スイッチされるか C# コンパイラ内部仕様は不明。nullable コンテクストを変えていない同一プロジェクト、同一モジュール内において同一のプロパティ定義をしているクラス間でもこの NullableContextAttribute 属性が異なるケースがあった。

Programming w/ C# ~ 鬼滅の刃コラボ 無限列車編

鬼滅の刃 無限列車編

C#er は、鬼滅の刃 無限列車編 をこう見る。(下記コード部分にはネタバレを含みます。)
NotImplemented はきちんと実装しないと使えませんよ。

using System;
using System.Collections.Generic;
using System.Linq;
using ヒノカミ神楽 = 日の呼吸;

public interface 日の呼吸 {
  public    object? 壱ノ型_円舞()            => throw new NotImplementedException();
  public    object? 弐ノ型_碧羅の天()        => throw new NotImplementedException();
  public    object? 参ノ型_烈日紅鏡()        => throw new NotImplementedException();
  public    object? 肆ノ型_灼骨炎陽()        => throw new NotImplementedException();
  public    object? 伍ノ型_陽華突()          => throw new NotImplementedException();
  public    object? 陸ノ型_日暈の龍_頭舞い() => throw new NotImplementedException();
  public    object? 漆ノ型_斜陽転身()        => throw new NotImplementedException();
  public    object? 㭭ノ型_飛輪陽炎()        => throw new NotImplementedException();
  public    object? 玖ノ型_輝輝恩光()        => throw new NotImplementedException();
  public    object? 拾ノ型_火車()            => throw new NotImplementedException();
  public    object? 拾壱ノ型_幻日虹()        => throw new NotImplementedException();
  public    object? 拾弐ノ型_炎舞()          => throw new NotImplementedException();
  protected object? 拾参ノ型()               => throw new NotImplementedException(); // 後ほど public に修正します
  public    void    回復の呼吸()             { /* ... */ }
  public    void    全集中の呼吸()           { /* ... */ }
  public    void    全集中_常中()            { /* ... */ }
}

public interface 炎の呼吸 : 日の呼吸 { // すべての呼吸は始まりの呼吸の派生 ... のはずなのだが、基底インターフェースのメソッドを実装してないね
  public object? 壱ノ型_不知火()       { /* ... */ } // 無限列車内・対 上弦の参 猗窩座戦で利用
  public object? 弐ノ型_昇り炎天()     { /* ... */ } // 炭治郎防護に利用
  public object? 参ノ型_気炎万象()     { /* ... */ } // 映画/劇場版小説オリジナル
  public object? 肆ノ型_盛炎のうねり() { /* ... */ } // 破壊殺・空式に対応
  public object? 伍ノ型_炎虎()         { /* ... */ } // 破壊殺・乱式に対応
  // 陸・漆・捌ノ型は Obsolete ではないよね ... ?
  public object? 玖ノ型_奥義_煉獄()    { /* ... */ } // 破壊殺・滅式に対応
}

public interface 水の呼吸 : 日の呼吸 {
  public object? 壱ノ型_水面斬り()    { /* ... */ }
  public object? 弐ノ型_水車()        { /* ... */ }
  public object? 参ノ型_流流舞い()    { /* ... */ }
  public object? 肆ノ型_打ち潮()      { /* ... */ }
  public object? 伍ノ型_干天の慈雨()  { /* ... */ }
  public object? 陸ノ型_ねじれ渦()    { /* ... */ }
  public object? 漆ノ型_雫波紋突き()  { /* ... */ }
  public object? 捌ノ型_滝壷()        { /* ... */ }
  public object? 玖ノ型_水流飛沫_乱() { /* ... */ }
  public object? 拾ノ型_生生流転()    { /* ... */ }
  public object? 拾壱ノ型_凪()        => throw new NotImplementedException(); // 義勇さんだけが実装してるよ
}

public interface 雷の呼吸 : 日の呼吸 {
  public object? 壱ノ型_霹靂一閃() { /* ... */ }
  public object? 弐ノ型_稲魂()     { /* ... */ }
  public object? 参ノ型_聚蚊成雷() { /* ... */ }
  public object? 肆ノ型_遠雷()     { /* ... */ }
  public object? 伍ノ型_熱界雷()   { /* ... */ }
  public object? 陸ノ型_電轟雷轟() { /* ... */ }
  public object? 漆ノ型_火雷神()   => throw new NotImplementedException(); // 善逸だけが実装することになるよ
}

public interface 獣の呼吸 : 日の呼吸 { // 風の呼吸に似ているが、派生ではない
  public object? 壱ノ牙_穿ち抜き()      { /* ... */ }
  public object? 弐ノ牙_切り裂き()      { /* ... */ }
  public object? 参ノ牙_喰い裂き()      { /* ... */ }
  public object? 肆ノ牙_切細裂き()      { /* ... */ }
  public object? 伍ノ牙_狂い裂き()      { /* ... */ }
  public object? 陸ノ牙_乱杭咬み()      { /* ... */ }
  public void    漆ノ型_空間識覚()      { /* ... */ }
  public void    捌ノ型_爆裂猛進()      ;
  public object? 玖ノ牙_伸_うねり裂き() ;
  public object? 拾ノ牙_円転旋牙()      ;
}

public interface 血鬼術 {
  public void 再生() { /* ... */ }
}

public interface 爆血 : 血鬼術 {
  public object? 爆血() { /* ... */ }
}

public interface 瑠火の言葉 {
  public void 強く生まれた者の責務を果たす() { /* ... */ } // ここにいる者は誰も死なせない
  public void あとを託す()                   { /* ... */ } // 竈門少年, 猪頭少年, 黄色い少年 ...
}

public interface 柱 {
  public void 後輩の盾となる() { /* ... */ } // 柱ならば後輩の盾となるのは当然だ 柱ならば誰であっても同じことをする 若い芽は摘ませない
}

public class 煉獄杏寿郎 : 炎の呼吸, 柱, 瑠火の言葉 { // 基本に忠実で独自実装がない ... 至高の領域に近い?
}

public class 竈門炭治郎 : ヒノカミ神楽, 水の呼吸 { // 無限列車編 : ヒノカミ神楽のメソッドはまだ 2 つしか実装していないよ
  public    object? 壱ノ型_円舞()            { /* ... */ } // 那田蜘蛛山 対 下弦の伍 累戦で利用
  public    object? 弐ノ型_碧羅の天()        { /* ... */ } // 対 下弦の壱 魘夢戦で利用
  public    object? 参ノ型_烈日紅鏡()        => null; // 実装中 (until 遊郭編)
  public    object? 肆ノ型_灼骨炎陽()        => null; // 実装中 (until 遊郭編)
  public    object? 伍ノ型_陽華突()          => null; // 実装中 (until 刀鍛冶の里編)
  public    object? 陸ノ型_日暈の龍_頭舞い() => null; // 実装中 (until 刀鍛冶の里編)
  public    object? 漆ノ型_斜陽転身()        => null; // 実装中 (until 無限城編)
  public    object? 㭭ノ型_飛輪陽炎()        => null; // 実装中 (until 無限城編)
  public    object? 玖ノ型_輝輝恩光()        => null; // 実装中 (until 無限城編)
  public    object? 拾ノ型_火車()            => null; // 実装中 (until 遊郭編)
  public    object? 拾壱ノ型_幻日虹()        => null; // 実装中 (until 遊郭編)
  public    object? 拾弐ノ型_炎舞()          => null; // 実装中 (until 遊郭編)
  protected object? 拾参ノ型()               => null; // 実装中 (until 無限城編)
  public    object? 円舞一閃()               => null; // 実装中 (until 刀鍛冶の里編)
}

public class 竈門禰豆子 : 爆血 {
  public object? 爪で引き裂く() { /* ... */ }
  public object? 蹴る()         { /* ... */ }
  public void    鬼化進行()     {}            // 実装中 (until 遊郭編)
  public object? 解毒()         => null;      // 実装中 (until 遊郭編)
  public object? 爆血刀()       => null;      // 実装中 (until 刀鍛冶の里編)
  public void    太陽克服()     {}            // 実装中 (until 刀鍛冶の里編)
}

public class 嘴平伊之助 : 獣の呼吸 {
  public void    捌ノ型_爆裂猛進()      {}       // 実装中 (until 遊郭編)
  public object? 玖ノ牙_伸_うねり裂き() => null; // 実装中 (until 無限城編)
  public object? 拾ノ牙_円転旋牙()      => null; // 実装中 (until 無限城編)
  public object? 思いつきの投げ裂き()   => null; // 実装中 (until 無限城編)
}

public class 我妻善逸 : 雷の呼吸 {
  public IEnumerable<object?> 壱ノ型_霹靂一閃_六連() => Enumerable.Repeat((this as 雷の呼吸).壱ノ型_霹靂一閃(), 6);
  public IEnumerable<object?> 壱ノ型_霹靂一閃_八連() => null!;                               // 実装中 (until 遊郭編)
  public object?              壱ノ型_霹靂一閃_神足() => null;                                // 実装中 (until 遊郭編)
  public object?              弐ノ型_稲魂()          => throw new NotImplementedException(); // 頑張ってください善逸君 一番応援してますよ
  public object?              参ノ型_聚蚊成雷()      => throw new NotImplementedException(); // 頑張ってください善逸君 一番応援してますよ
  public object?              肆ノ型_遠雷()          => throw new NotImplementedException(); // 頑張ってください善逸君 一番応援してますよ
  public object?              伍ノ型_熱界雷()        => throw new NotImplementedException(); // 頑張ってください善逸君 一番応援してますよ
  public object?              陸ノ型_電轟雷轟()      => throw new NotImplementedException(); // 頑張ってください善逸君 一番応援してますよ
  public object?              漆ノ型_火雷神()        => null;                                // 実装中 (until 無限城編)
}

実装中 = 修行により確立
将来予約 = 実戦中に獲得

Programming w/ C# ~ DocFx の利用方法

はじめに

Javajavadoc のように C# においても /// がドキュメントコメントになることは知っており VS Code にもそれ用の拡張機能を加えていた。しかし、これまできちんと運用したことがなかったため、ここらで導入してみることにした。(DoxFx は 2.61.0 時点でまだまだバグがたくさんありそうだ。)
DocFx で Web ページのセットを構築するには、けっこう注意点があるため、ここに記録する。

運用方法

以下のように運用すると非常に便利。

  1. ソースコードとは独立した場所に仕様文書専用フォルダを切る。
  2. すべてのプロジェクトはそのサブフォルダに文書化する。
  3. 各プロジェクトの仕様文書専用フォルダに docfx.json を配置し、そのフォルダを working dir として docfx を起動する doc タスク定義をソースコードの側の .vscode/tasks.json に記述する。
  4. 仕様文書専用フォルダを Docker の Apache で常時 Web 展開する。

VS Code タスク設定

ビルド中間生成物や DocFx のキャッシュなどが悪さをすることがあるため、clean コマンドも整備しておく。

    {
      "label": "doc",
      "type": "shell",
      "command": "docfx",
      "args": [
        "...(仕様文書専用フォルダ)...\\...(プロジェクトサブフォルダ)...\\docfx.json",
        "--cleanupCacheHistory",
      ],
      "group": {
        "kind": "build",
        "isDefault": true,
      },
      "presentation": {
        "reveal": "always",
        "clear": true,
      },
    },
    {
      "label": "clean",
      "type": "shell",
      "command": ".vscode\\_clean.bat",
      "args": [
      ],
      "group": {
        "kind": "build",
        "isDefault": true,
      },
      "presentation": {
        "reveal": "always",
        "clear": true,
      },
    },

クリーンアップバッチは各自の環境におけるビルド出力先 obj フォルダ, bin フォルダに応じて設定する。

@echo off
dotnet clean --nologo
rd /s /q bin
for /f "usebackq tokens=1 delims==" %%d in (`dir /d /b /s ^| findstr /e \\obj ^| findstr /v \\obj\\`) do rd /s /q %%d

名前空間ページの作成

名前空間のページを作成するには namespaces フォルダを新設し、下記のようにアクセスを許可しておく。

    "overwrite": [
      {
        "files": [
          "namespaces/**.md"
        ]
      }
    ]

そして、各名前空間のページを Markdown で記述する。uid には名前空間の識別子を、summary には *content を指定する。

---
uid: UserLib.Common
summary: *content
---
UserLib.Common 名前空間は, 標準の各種オブジェクト型に対する拡張メソッドまたは機能拡張した派生クラスを扱う.

表示抑制の設定

継承メンバーの表示抑制

継承メンバーの表示が煩わしい場面があるため、以下のようにしてノイズ的な情報を表示抑制する。
filterConfig.yml に正規表現を記述して表示抑制する際は docfx --cleanupCacheHistory で文書ビルドしないと正しく反映されないことが多い

継承メンバーの項目全消去 (不採用)

継承メンバーの項目全体を消去するには下記を追記する。docfx の出力文書ではなく css で表示を抑制するようだ。ただ一律消去は弊害が大きいため、不採用とする。

inheritedMembers { display: none; }

Obsolete メンバーの消去

Obsolete メンバーを消去するには、スタイル設定ではなく filterConfig.yml が必要だ。filterConfig.yml は Python のようにインデントにセンシティブであることに注意を要する。

  "build": {
    "filter": "filterConfig.yml",
  }

apiRules:
- exclude:
    hasAttribute:
        uid: System.ObsoleteAttribute

下記のように GitHub の Q&A に従うとメンバーではなく [Obsolete] 属性自体しか消えないため注意。これはこれで属性を表示抑制するときに使えるが。
また uidRegex正規表現であるため、名前空間や引数の揺らぎを吸収するように記述する必要がある。

attributeRules:
- exclude:
    uidRegex: ^System\.Obsolete$

基本クラスのメンバーの消去

System.Object.ToString() などすべてのクラスが継承しているメンバーを Inherited Members に表示されると煩わしいため、消去する。object は SystemObjectobject の間で揺らぎがあることに注意。ここでは Member としているが、Field, Event, Method, Property ごとの指定も可能。(.NET API Docs | docfx 参照)

- exclude:
    uidRegex: ^(System\.)?[Oo]bject
    type: Member

ただし、.Equals(),.HashCode(),.SequenceEquals() などを利用して演算子オーバーロードしている演算系ユーティリティなどでは Inherited Members を消去しない方が望ましいと思われる。object のみならず、独自 Exception を定義している場合は System.Exception のメソッドを消去してもよいかもしれない。

- exclude:
    uidRegex: ^(System\.)?Exception
    type: Method

隠蔽した基底クラス・メソッドの消去

基礎ライブラリのメソッドが充実しすぎているためにオーバーライドして隠蔽したケースなどでは基底クラスのメソッド表示を消去する。

- exclude:
    uidRegex: ^DbExtensions.Database
    type: Member

データコンテナのプロパティの消去

コード付帯のオブジェクトというよりは POCO データコンテナとして用いる record class などは各プロパティの個別説明表示ではなく一覧表として remarks タブ内にリスト表示した方が見やすいため、プロパティ表示を消去する。

- exclude:
    uidRegex: ^DataModel\.(.*\.)?ContainerFormat.+
    type: Property

Markdown 記事の追加

Markdown 記事の追加方法

Markdown で追加記事を記述できる。まずは docfx.json ファイル内 buid > content の第2要素 (メニューバー内容相当) に追加することでリソースアクセスを許可する。

  "build": {
    "content": [
      {
        "files": [
          "reference/**.md",
          "reference/**/toc.yml",
        ]
      }
    ]
  }

次にメニューバーにリンクを設ける。

- name: Reference
  href: reference/

そしてフォルダを切って記事を書く。

### API Reference
- [Ms Docs](https://learn.microsoft.com/ja-jp/dotnet/api/)
- [DbExtensions](https://maxtoroq.github.io/DbExtensions/)

toc.yml は左サイドメニューペインのコンテンツになる。ここで Markdown のページ内参照 (# 以降の部分) の記法は、英文字を小文字に、スペースをハイフンに、アンダースコア以外の記号文字を省略にする必要がある。

- name: API Reference
  href: index.md#api-reference

画像等の追加方法

画像等のファイルは Markdown 記事のフォルダに置いても参照できないようだ。working dir 直下に images フォルダを切ってその中に格納する。

    "resource": [
      {
        "files": [
          "images/**"
        ]
      }
    ]

外部リンク動作の設定

外部リンクは別タブで開く設定を Web ページに施す。そのためにまず javascript の格納場所 styles を作る。注意すべきは docfx.json での記述は格納場所のアクセス権の設定でしかないということだ。ここにスクリプトを置いてもそれだけで Web ページに反映されるわけではない。

 "build": {
    "resource": [
      {
        "files": [
          "styles/*.js",
        ]
      }
    ]
  }

javascript コードを記述する。target="_blank" を使う時は rel="noopener noreferrer" の設定も忘れないこと。

$(document).ready( function () {
  $("a[href^='http']").attr('target', '_blank');
  $("a[href^='http']").attr('rel', 'noopener noreferrer');
} );

最後に、適用する Web ページ (Markdown ファイル) の冒頭に下記の 2 行を追記する。各 Web ページに javascript コントロールのインポートを記述しなければ適用されない。また、google api スクリプトのインポートを 2 つ目のスクリプトに内包して動的呼び出しにしようとすると HTML 評価タイミングの関係で動作しなくなるようで script タグは 2 つ記述する必要がある。

<script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script type="text/javascript" src="../styles/target_blank.js"></script>

スタイル設定

事前準備としてスタイル用の javascript, css をアクセス権の専用フォルダに配置する。その上でスタイル記述をしたファイルへの参照をスタイル適用するページに追記する必要がある。

  "build": {
    "resource": [
      {
        "files": [
          "styles/*.js",
          "styles/*.css"
        ]
      }
  }