xin9le.net

Microsoft の製品/技術が大好きな Microsoft MVP な管理人の技術ブログです。

DeclarativeSql rebooted - Now supports .NET Standard 2.0

数年間開発を放置した DeclarativeSql を更新して .NET Standard 2.0 に対応させました!過去バージョンとの互換は結構崩れてしまっている点は大変申し訳ないのですが、今の自分が欲しいなぁという機能をシンプルに詰め込むために致し方なし。

実は .NET Core がまだリリース前だった頃から .NET Core 対応を目論んでいて net-core というブランチでヒッソリと作業していたのですが、.NET Standard 1.0 が出たての頃と言ったら IDbConnection を廃止するだの Type / TypeInfo あたりが壊滅的に使いにくいなど超絶グダグダで、それに辟易して結局諦めてしまいました。だけど時代が進んで今は .NET Standard 2.x。フレームワーク起因で開発上困るようなことがほとんどなくなったのと、改めて .NET Core に対応したものが欲しくなったのでメンテしました。

GitHub の ReadMe に書いてあることのコピペみたいなものですが、僕は日本語が最も得意 () なので日本語でもある程度解説をば。

属性ベースのテーブルマッピング

Entity Framework などはそうですが、テーブルとマップする型に属性をアレコレ付けることで SQL をイイ感じに生成できるようにします。これが最も面倒な作業ですが、データベース内のテーブル表現をコード上で (ある程度) 再現しないと自動化できないので、こればっかりは頑張って書くしかないです。Entity Framework はテーブル情報を引っ張ってきてコードを自動生成してくれるので強いんですが、そういう自動化するコードを書くのはそこまで難しくないので、各々のプロジェクトでイイ感じに自動生成するバッチでも作ればよいと思っています。

using System;
using DeclarativeSql.Annotations;

namespace SampleApp
{
    // DB の種類ごとにテーブル名をカスタマイズできる
    [Table(DbKind.MySql, "T_Customer")]
    [Table(DbKind.SqlServer, "T_Person", Schema = "dbo")]
    public class Person
    {
        [PrimaryKey]  // 主キー
        [AutoIncrement]  // 自動採番
        public int Id { get; set; }

        [Unique(0)]  // インデックスで一意制約を指定
        public string Email { get; set; }

        // DB の種類ごとに列名も変えられる
        [Column(DbKind.MySql, "氏名")]
        [Column(DbKind.SqlServer, "名前")]
        public string Name { get; set; }

        [AllowNull]  // Nullable
        public int? Age { get; set; }

        [CreatedAt]  // 生成日時を入れるマーカー属性
        [DefaultValue(DbKind.SqlServer, "SYSDATETIME()")]  // 入れる値
        public DateTimeOffset CreatedOn { get; set; }

        [ModifiedAt]  // 更新日時を入れるマーカー属性
        [DefaultValue(DbKind.SqlServer, "SYSDATETIME()")]  // 入れる値
        public DateTimeOffset UpdatedOn { get; set; }
    }
}

特に今回最も頑張った機能が [CreatedAt] / [ModifiedAt] 属性 + [DefaultValue] 属性による挿入/更新日時の自動設定です。毎回毎回 DateTimeOffset.Now をプロパティに入れて Insert / Update するの、相当ダルくないですか?僕はとてもダルい!そして DateTimeOffset.Now は複数台のサーバーになったときにサーバーによって時間が違うので、時刻の信頼性が若干低下します。データベースの時刻を使えばより誤差を低減できると期待できます。ので、そういう信頼性高めの日時の挿入を自動でやってれる SQL を吐き出したかった...という私欲まみれなヤツです。

他にも以前から「SQL Server と MySQL で列名とかスキーマ名が違うんだけど同じものを表すテーブルを表現したい」みたいな要望がありました。複数の DB に同時に接続することを中途半端にしか (ぇ) 想定していなかったので、今回はそういうところにももうちょっと配慮して [Table] 属性と [Column] 属性を作ってみたりしました。ちゃんと応えられているのかは若干不安ですが...。

SQL の自動生成

テーブルとマッピングする型が手に入れば SQL を自動生成できます。SQL は方言が多くてなんでもかんでも対応するのは無理なので、基本どんなデータベースに対しても動くものだけカジュアルサポートしています。作れる SQL は以下の通りです。それぞれドストレートなメソッドが提供されているので簡単に分かると思います。

  • count
  • select
  • insert
  • update
  • delete
  • truncate
  • where
  • order by
var sql
    = DbProvider.SqlServer.QueryBuilder
    .Select<Person>(x => new { x.Id, x.Name })
    .Where(x => x.Name == "xin9le")
    .OrderByDescending(x => x.Name)
    .ThenBy(x => x.CreatedOn)
    .Build()
    .Statement;

/*
select
    [Id] as Id,
    [名前] as Name
from [dbo].[T_Person]
where
    [名前] = @p1
order by
    [名前] desc,
    [CreatedOn]
*/
var sql
    = DbProvider.SqlServer.QueryBuilder
    .Insert<Person>()
    .Build()
    .Statement;

/*
insert into [dbo].[T_Person]
(
    [Email],
    [名前],
    [Age],
    [CreatedOn],
    [UpdatedOn]
)
values
(
    @Email,
    @Name,
    @Age,
    SYSDATETIME(),
    SYSDATETIME()
)
*/
var sql
    = DbProvider.SqlServer.QueryBuilder
    .Update<Person>(x => new { x.Name, x.Age })
    .Where(x => x.Age < 35 || x.Name == "xin9le")
    .Build()
    .Statement;

/*
update [dbo].[T_Person]
set
    [名前] = @Name,
    [Age] = @Age,
    [UpdatedOn] = SYSDATETIME()
where
    [Age] < @p1 or [名前] = @p2
*/

Dapper との統合

データベースを相手にしていて最も使うのは超シンプルな CRUD です。inner join とか group by みたいな複雑な (?) ものこそ SQL を手書きすればよし!という割り切りのもと、シンプルな CRUD に対するショートハンドなメソッドを提供します。これは過去のバージョンから変わってない機能です。下記のサンプルは同期メソッドで書いてありますが、非同期メソッド版もありますのでご安心を。

//--- 全件取得
var p1 = connection.Select<Person>();

//--- 特定列だけに絞りつつ全件取得
var p2 = connection.Select<Person>(x => new { x.Id, x.Name });

//--- 'ID = 3' のレコードのみ取得
var p3 = connection.Select<Person>(x => x.Id == 3);

//--- 'ID = 3' のレコードを特定列に絞って取得
var p4 = connection.Select<Person>
(
    x => x.Id == 3,
    x => new { x.Id, x.Name }
);
//--- 1 件 insert
var p5 = connection.Insert(new Person { Name = "xin9le", Age = 30 });

//--- 複数件 Insert
var p6 = connection.InsertMulti(new []
{
    new Person { Name = "yoshiki", Age= 49, },
    new Person { Name = "suzuki",  Age= 30, },
    new Person { Name = "anders",  Age= 54, },
});

//--- バルク処理で insert
//--- 現在 SQL Server のみ対応 : そのうち他もやるかも
var p7 = connection.BulkInsert(new []
{
    new Person { Id = 1, Name = "yoshiki", Age= 49, },
    new Person { Id = 2, Name = "suzuki",  Age= 30, },
    new Person { Id = 3, Name = "anders",  Age= 54, },
});

//--- insert 後に自動採番された ID を取得
var p8 = connection.InsertAndGetId(new Person { Name = "xin9le", Age = 30 });
//--- 条件に一致する行の指定列を更新
var p9 = connection.Update
(
    new Person { Name = "test", Age = 23 },
    x => x.Age == 30,
    x => new { x.Name, x.Age }
);
//--- 全件削除
var p10 = connection.Delete<Person>();

//--- 条件に一致する行を削除
var p11 = connection.Delete<Person>(x => x.Age != 30);
//--- truncate 文による全件削除
var p12 = connection.Truncate<Person>();
//--- テーブルのレコード数を取得
var p13 = connection.Count<Person>();

//--- 条件に一致するレコード数を取得
var p14 = connection.Count<Person>(x => x.Name == "xin9le");

高可用性を持つデータベース接続

サービスの規模が相当大きくなってくるとデータベースへの負荷が上がってレスポンスが悪化したりします。そういうときは DB Server のインスタンスサイズを上げて金で解決するのが最も簡単な手法ですが、稀にそれで追いつかなくなって「読み込みは Read Replica からにしたい」みたいなケースも出てくるかと思います。そう言ったときに、先手を打って Master / Slave が別になることを想定したコードを書いておくと良いでしょう。

Master / Slave がインフラによって同期されていることが前提ですが、HighAvailabilityConnection はその一助になるのではないかと思います。

public class FooConnection : HighAvailabilityConnection
{
    public FooConnection()
        : base("ConnectionString-ToMasterServer", "ConnectionString-ToSlaveServer")
    {}

    protected override IDbConnection CreateConnection(string connectionString, AvailabilityTarget target)
        => new SqlConnection(connectionString);
}
using (var connection = new FooConnection())
{
    //--- Slave DB から読み込み
    var p = connection.Slave.Select<Person>();

    //--- Master DB に書き込み
    connection.Master.Insert(new Person { Name = "xin9le" });
}

Master / Slave の同期がどのくらいの期間で行われるかはインフラ/クラウド環境次第なので、自身のサービスで担保したい高可用性のレベルにマッチする場合にお使いください。

まとめ

ちょっとは戦える...と思う。難しいクエリをしたいときは生 SQL で良いけど、簡単な SQL まで手書きするのはイヤ!という怠惰な自分のための一品です。そして僕は Entity Framework と戦いたいわけでは決してないのです。

FastEnum - 更なる高速化を果たした v1.1.0 リリース

先の記事FastEnum という超高速な Enum Utility ライブラリを作ってリリースしてから 3 日。v1.1.0 をリリースしました。パフォーマンスはさらに磨きがかかっていて、他の追随を許さないレベルに昇華されました。最早グラフにしても棒が表示されてない...(

f:id:xin9le:20190909221347p:plain

今回は主に以下のような変更を行っています。早速思いっきり Breaking Chages!!

  • API Surface が大きく変更に
  • 更なるパフォーマンスの改善
  • API の追加
  • etc...

API Surface の変更

のいえ先生 (@neuecc) から「Generics Class な API は使い心地が悪いからヤだ」と指摘を受け、全くそのとーりだったので改善しました。思いっきり Breaking Changes!! なんですが、リリース数日なので許してください!(泣

概ね以下のように変更されています。Generics クラスだったものを Generics メソッドに変更した感じです。大変自然というか、System.Enum にかなり近づいた API になったと思います。

//--- Before
var values = FastEnum<Fruits>.Values;
var names = FastEnum<Fruits>.Names;
var defined = FastEnum<Fruits>.IsDefined(123);
var parse = FastEnum<Fruits>.Parse("Apple");
var tryParse = FastEnum<Fruits>.TryParse("Apple", out var value);

//--- After
var values = FastEnum.GetValues<Fruits>();
var names = FastEnum.GetNames<Fruits>();
var defined = FastEnum.IsDefined<Fruits>(123);
var parse = FastEnum.Parse<Fruits>("Apple");
var tryParse = FastEnum.TryParse<Fruits>("Apple", out var value);

更なるパフォーマンスの改善

.NET Core に勝っただけで世界最速!などと大きなことを言ってしまったことを反省しています。世の中には Enums.NET というライブラリがあって、同様に high-performance を謳っていました。それとの比較なしに最速を勝手に語るのは思い上がりなので、ちゃんと比較しました。

結果として IsDefined メソッドで Enums.NET より 4 倍ほど遅い結果が出てしまって大変がっかり...。

ということで必死に改善しました。改善した結果は記事冒頭のグラフの通りで、Enums.NET と比べて 6 倍 (v1.0 の実装と比べると 25 倍) 程速くなっています。どのメソッドにおいても Enums.NET よりも System.Enum よりも速い!今度こそ最速!(たぶん

まとめ

1 [ns] レベルの改善するために数日かけるという非効率の極み...。このチューニングによる時間は回収できるものなのか分かりませんが、大変勉強にはなりました。もうこれ以上の速度は出せない気がする (気がするだけかも) ので、ぜひ速いものに巻かれる (?) つもりで利用してみてください。バグ報告や PR などもお待ちしております :)

FastEnum - 世界最速の enum ライブラリ

というのを、この数日をかけて勢いで作りました。ずーーっと昔から「enum は遅い」と言われ続けていたので何か手を入れたいと思っていたのですが、突然やる気になりました。久々にプライベートのコーディング意欲が爆上げしたー。勢いが強過ぎて毎日寝不足のまま明け方までのコーディングを繰り返し、しかも家庭を顧みませんでした...(超ヨクナイ

どのくらい速いかというと、.NET Core 3.0 よりも何倍も速いです。もう狂ったくらい速い。全てのメソッドがゼロ・アロケーションです。プログラミングする上で enum はかなり使うので、多少なり性能改善に寄与できるのではないかと思います。ぜひ使ってみた感想をいただければなーと思います :)

f:id:xin9le:20190906201828p:plain

簡単に使い方解説

.NET 標準の System.Enum に近い使い方ができるように API を設計しています。以下のような感じで、とてもシンプル!

//--- FastEnum
var values = FastEnum<Fruits>.Values;
var names = FastEnum<Fruits>.Names;
var name = Fruits.Apple.ToName();
var defined = FastEnum<Fruits>.IsDefined(123);
var parse = FastEnum<Fruits>.Parse("Apple");
var tryParse = FastEnum<Fruits>.TryParse("Apple", out var value);
//--- .NET
var values = Enum.GetValues(typeof(Fruits)) as Fruits[];
var names = Enum.GetNames(typeof(Fruits));
var name = Enum.GetName(typeof(Fruits), Fruits.Apple);
var defined = Enum.IsDefined(typeof(Fruits), 123);
var parse = Enum.Parse<Fruits>("Apple");
var tryParse = Enum.TryParse<Fruits>("Apple", out var value);

おまけ機能

enum 周りでよく使う機能があって、それらも一緒に組み込むことでより便利に使えるようにしています。

1. メンバー情報を丸ッと取得

たまに enum の値と名前のペアが欲しくなることがあります。そういうケースに耐えるために Member<TEnum> というのを用意しています。FieldInfo なども一緒に入っているので、リフレクションなどのお供にご利用ください。

class Member<TEnum>
{
    public TEnum Value { get; }
    public string Name { get; }
    public FieldInfo FieldInfo { get; }
    // etc...
}

//--- こんな感じで取得できるので、煮るなり焼くなり
var member = Fruits.Apple.ToMember();

2. EnumMemberAttribute.Value の取得

EnumMemberAttribute をフィールド名の別名として利用している方をちょくちょく見かけます。このケースに対応するため、Value プロパティからサッと値を取得できるようにしました。

enum Company
{
    [EnumMember(Value = "Apple, Inc.")]
    Apple = 0,
}

var value = Company.Apple.GetEnumMemberValue();  // Apple, Inc.

3. 複数のラベルを付与

EnumMemberAttributeAllowMultiple = false になっているため、同一のフィールドに対して複数の属性を付けられません。それが不便で個人的には好んでおらず、その代替として LabelAttribute というのを作って使っています。FastEnum にこの機能を追加することで、以下のような感じで便利に利用できます。

enum Company
{
    [Label("Apple, Inc.")]
    [Label("AAPL", 1)]
    Apple = 0,
}

var x1 = Company.Apple.GetLabel();   // Apple, Inc.
var x2 = Company.Apple.GetLabel(1);  // AAPL

制限

System.Enum に対して完全に互換があるかというと、実はそうではないです。標準で提供されているいくつかの機能を削ることで速さを得ている部分があります。

1. Generics な API のみを提供

標準の System.Enumtypeof(TEnum) を用いたオーバーロードが用意されているのですが、その場合、引数か戻り値が object になります。そこで box 化が走って非常に遅くなってしまうので、機能として削ることで速度を稼ぐようにしています。

2. カンマ区切りの文字列を Parse できない

実は System.Enum.Parse は以下のような感じの文字も parse できます。あまり知られていないんじゃないかと思うくらい、ひっそりと存在する仕様です。

//--- こんなのがあるとして
[Flags]
enum Fruits
{
    Apple = 1,
    Lemon = 2,
    Melon = 4,
    Banana = 8,
}

//--- カンマ区切りの文字列を与える
var value = Enum.Parse<Fruits>("Apple, Melon");
Console.WriteLine((int)value);  // 5

フラグ処理をするときに便利な機能のようですが、これまで 10 年以上一度も使ったことがありませんでした。このカンマ区切りの解析を加えようとするとそのオーバーヘッドが出てしまうので、思い切って機能を削ることで速度を稼いでいます。ほとんど使われない (少なくとも自分は使わない) 機能のために速度を落とすのが嫌だったのでそうしています。そのあたりは本家に譲ればいいかな、と...。

速さの秘訣

言わずもがなでお察しかもしれませんが、内部でキャッシュしているからです。Static Type Caching というアプローチを取っていて、そのおかげで読み込み速度が限りなく 0 になっています*1。これを基礎としつつ、アロケーション回避のためのテクニックを交えたり、内部で利用する辞書を特定のキーに特化させるなどしています*2

このあたりのテクニックはのいえ先生 (@neuecc) の講演スライドCysharp 印の GitHub リポジトリが大変参考になります。

*1:BenchmarkDotNet にも「計測不可能」と言われる速度

*2:保守性の観点では好ましくないけれど、速度を稼ぐためにやむを得ず...

C# でインストールされている Windows Store App の一覧を取得する

とある先輩に「Windows Store のアプリ一覧を取得したいんだけど、やり方知らない?」と聞かれたのでやってみました。今回は、その方法と簡単な実装のメモの回。

最初から答えを授かりました

2 分ほど iPhone で調べて PowerShell コマンドで最低限できそうというところまでは当たりをつけていたのですが、C# から PowerShell コマンドを呼び出すのではない方法がいいなーと思っていました。と思っていたら C# MVP の大先輩である @matarillo さんがパッと答えを教えてくださいました!本当に感謝!

どうやら WMI (= Windows Management Instrumentation) という OS 管理の機能/技術を使えばできるみたいです。

実装

LINQPad で書いてみたのですが、ザッと以下のような感じで取得できました。WQL なるクエリ言語で書いて投げるだけの簡単なお仕事でした。ちなみに実行には管理者権限が必要です。

void Main()
{
    WindowsStoreApp.GetInstalled().Dump();
}

public sealed class WindowsStoreApp
{
    public string Architecture { get; }
    public string Language { get; }
    public string Name { get; }
    public string ProgramId { get; }
    public string Vendor { get; }
    public Version Version { get; }

    private WindowsStoreApp(PropertyDataCollection props)
    {
        this.Architecture = (string)props[nameof(Architecture)].Value;
        this.Language = (string)props[nameof(Language)].Value;
        this.Name = (string)props[nameof(Name)].Value;
        this.ProgramId = (string)props[nameof(ProgramId)].Value;
        this.Vendor = (string)props[nameof(Vendor)].Value;
        this.Version = Version.Parse((string)props[nameof(Version)].Value);
    }

    public static WindowsStoreApp[] GetInstalled()
    {
        // これで取り出せちゃう!簡単!
        const string wql = "select * from Win32_InstalledStoreProgram";
        using (var searcher = new ManagementObjectSearcher(wql))
        {
            return searcher
                .Get()
                .Cast<ManagementBaseObject>()
                .Select(x => x.Properties)
                .Select(x => new WindowsStoreApp(x))
                .ToArray();
        }
    }
}

手元の環境では結果は以下のようになっていて、この NameProgramId から所望のアプリがインストール済みかを判定できそうです。

f:id:xin9le:20190828023330p:plain

Microsoft Office の Application ID

例えば Microsoft Office の Windows Store 版が入っているかは、以下のサイトにある Application ID を参考にすると判定できるような気がしています。「気がしている」というのは、僕が Office 365 を契約していないので Windows Store 版をインストールできていないから、なだけなのです...(悪しからず

LINQPad.Controls でお手軽 GUI 操作

今頃気が付いたのですが、超カジュアル IDE の LINQPadLINQPad.Controls なる GUI 部品の名前空間が追加されていたので遊んでみました。以下のようなインタラクティブなことがお手軽にできるようになります。

f:id:xin9le:20190825152552g:plain

使い方

使い方はかなり簡単で、C# で WinForms / WPF / UWP などのデスクトップアプリを作ったことがある方にとってはお馴染みの方法です。TextBoxButton などのインスタンスを new して、イベント処理などをコードで表現するだけです。唯一違うのは GUI 部品を .Dump() すること。これで結果画面に GUI 部品が表示されるようになります。

var textBox = new TextBox("This is initial text.").Dump();  // TextBox 表示
var button = new Button("Execute").Dump();  // Button 表示
button.Click += (sender, e) =>
{
    $"Hello, my name is {textBox.Text}.".Dump();
};

サンプル

LINQPad をインストールすると、LINQPad.Controls のサンプルが結構含まれているので参考になります。その中に正規表現評価のサンプルがありました。以下のようなこともチョイチョイっとコードを書くとできちゃうみたいです。便利!

f:id:xin9le:20190825160839g:plain