カテゴリー: C#

  • C#/.NETにおけるPrivateObjectの使い道と現代のテスト設計ベストプラクティス

    ソフトウェア開発において「テスト」は品質保証の要です。その中でも、C#や.NETの開発環境でのユニットテストの書き方は年々進化しています。特に.NET 8以降では、古くから存在していたPrivateObjectクラスが廃止されたことで、「privateメンバーをどうテストすべきか?」という課題が再浮上しました。

    かつて、PrivateObjectを使えば、簡単にprivateフィールドやメソッドにアクセスしてテストが書けました。しかし、これは現在のソフトウェア設計の原則においては必ずしも推奨される方法ではありません。

    この記事では、かつてのPrivateObjectの使い方から、現在の.NET環境におけるベストプラクティスまでを詳しく紹介します。そして、なぜprivateメンバーの直接的なテストを避けるべきか、どうしてもテストしたい場合にどう対応すればよいのかを解説していきます。


    PrivateObjectとは何か?

    PrivateObjectは、.NET Framework時代に提供されていたクラスで、主にVisual Studioの単体テストフレームワーク(MSTest)とともに利用されていました。このクラスは、リフレクションを利用してprivateなフィールドやメソッドにアクセスする機能を提供していました。

    典型的な使い方

    var target = new SomeClass();
    var privateObj = new PrivateObject(target);
    
    // private メソッドの呼び出し
    privateObj.Invoke("SomePrivateMethod");
    
    // private フィールドの取得・設定
    var value = privateObj.GetField("someField");
    privateObj.SetField("someField", "newValue");
    

    このように、テストから直接privateメンバーにアクセスできるため、カバレッジ向上やデバッグが容易になるというメリットがありました。


    .NET Core以降での変化とPrivateObjectの廃止

    .NET Coreに移行して以降、PrivateObjectは正式にはサポートされなくなり、.NET 8においてもフレームワーク標準には含まれていません。これは、.NETの設計思想そのものがより「クリーンアーキテクチャ」や「SOLID原則」などのモダンな開発パラダイムに移行したことが背景にあります。

    とはいえ、PrivateObject自体のソースコードはオープンソース(MITライセンス)としてGitHubなどで入手可能であり、必要であればライブラリ化して自前で再利用することもできます。


    テストにおける現代的なベストプラクティス

    1. カプセル化の原則を尊重する

    オブジェクト指向プログラミングでは、クラスの内部構造を外部から隠蔽すること(カプセル化)は非常に重要な概念です。これにより、クラスの内部ロジックが変更されたとしても、publicなインターフェースが保たれていれば、他のコードへの影響を最小限に抑えることができます。

    もしテストがprivateメンバーに依存していると、実装のちょっとした変更でもテストコードの大幅な書き換えが必要になる可能性があります。これは、保守性の低下を招き、長期的な開発において大きなリスクとなります。


    2. テストの信頼性と安定性を保つ

    テストは「外部から見たクラスの振る舞い」が正しいかどうかを確認するためのものです。内部実装の正しさを保証するためにprivateメソッドを直接テストするのではなく、そのロジックが反映されているpublicメソッドを通じて確認するのが理想です。

    こうすることで、実装の詳細に引きずられることなく、安定したテストスイートを維持できます。


    3. publicなインターフェースを通じたテストを徹底する

    テストの基本は「入力に対して期待される出力が得られるか」を確認することです。そのため、クラスのpublicメソッドやプロパティを通じてテストすることが推奨されます。

    たとえば、ある計算処理を行うクラスがある場合、privateなCalculateInternal()メソッドが正しいかをテストするのではなく、publicなCalculate()メソッドが正しい出力を返すかを確認すべきです。


    4. 依存性の注入(DI)とモックを活用する

    テストを容易にし、privateメンバーへの依存を減らすために、依存性の注入(DI)を活用しましょう。これにより、テスト対象のクラスの内部依存関係を柔軟に差し替えることができ、privateな処理であっても、モックオブジェクトなどを使ってテストしやすくなります。

    人気のあるモックライブラリとしては、以下があります。

    • Moq
    • NSubstitute
    • FakeItEasy

    これらのツールを活用することで、privateメソッドの間接的なテストもよりシンプルかつ堅牢に行えるようになります。


    それでもprivateメンバーをテストしたいときは?

    とはいえ、現実的にはどうしてもprivateメンバーのロジックをテストしたくなる場面もあるかもしれません。

    代表的なケース

    • 非常に複雑な内部ロジックが存在する
    • 外部I/Oに依存せず、単体で動作するprivateメソッド
    • 性能上の理由などでメソッドを分離しているが、再利用は想定していない

    こういったケースでは、PrivateObjectのようなリフレクションを使う手法や、ソースコードレベルでInternalsVisibleTo属性を使い、internalメンバーのテストを可能にするという方法もあります。

    // AssemblyInfo.cs
    [assembly: InternalsVisibleTo("YourTestProjectName")]
    

    この方法であれば、privateではなくinternalにしてアクセスを許可することで、より自然な形でテストが可能です。


    結論:テストはあくまで「振る舞い」の確認のためにある

    現代のソフトウェア開発では、「ブラックボックステスト」の考え方に則り、テストは外部から見た挙動の確認を重視します。内部実装に強く依存したテストは、技術的負債となりやすく、保守性や柔軟性を損なう原因にもなります。

    もちろん、どうしてもテストしたいprivateロジックがある場合は、PrivateObjectを再利用したり、リフレクションやInternalsVisibleToなどの手段を用いて対応することも可能です。ただし、それは「例外的な措置」であり、通常は避けるべきです。


    まとめ

    • PrivateObjectは.NET 8では廃止されたが、MITライセンス下で再利用可能。
    • 現代のベストプラクティスでは、privateメンバーへの直接アクセスは避けるべき。
    • テストはpublic APIを通じた振る舞いの検証に集中すべき。
    • DIやモックを活用することで、よりテストしやすい設計が可能。
    • どうしても必要ならInternalsVisibleToやリフレクションを利用。

    C#/.NETのテスト設計は、技術的にも思想的にも進化しています。privateメンバーへの直接的なアクセスに頼らず、健全なアーキテクチャと信頼性の高いテストコードを目指していきましょう。


    もしこの記事が参考になったら、シェアやコメントをぜひお願いします!また、PrivateObjectをどのように再活用しているか、自作のライブラリ設計などもシェアしていただけると嬉しいです。

  • 【EF Core】DbContextから接続文字列を取得する方法とその注意点

    Entity Framework Core(EF Core)は、.NETアプリケーションで広く使われているORM(Object Relational Mapper)です。データベースとのやり取りを簡潔に記述できるだけでなく、LINQを活用した型安全なクエリも実現できる強力なライブラリです。

    EF Coreを使ってアプリケーションを構築していると、開発・テスト中やトラブルシューティングの際に「DbContextから接続文字列を取得したい」という場面に遭遇することがあります。本記事では、その方法と実装例、そして実際に使う上での注意点までを丁寧に解説します。


    なぜDbContextから接続文字列を取得したいのか?

    一般的に、接続文字列は appsettings.json などの設定ファイル、または環境変数で管理されることが推奨されています。特に本番環境では、セキュリティや柔軟性の観点からこのアプローチが基本です。

    しかし、以下のようなケースではDbContextから直接接続文字列を取得したくなることがあります。

    • ログやデバッグで現在の接続先を確認したい
    • 動的に生成されたDbContextの接続先を確認したい
    • テストケースで実際に使われている接続情報を検証したい
    • 外部ライブラリとの連携で、接続文字列を使わなければならない

    では、どのようにDbContextから接続文字列を取得できるのでしょうか?


    DbContextから接続文字列を取得する方法

    EF Coreでは、DbContext.Database.GetDbConnection() メソッドを使用することで、現在のDbContextが使用している DbConnection オブジェクトを取得できます。そして、DbConnection.ConnectionString プロパティを参照することで、接続文字列を取得することが可能です。

    コード例

    以下は、MyDbContext というDbContextクラスから接続文字列を取得する基本的なサンプルコードです:

    using (var context = new MyDbContext())
    {
        // DbConnection オブジェクトを取得
        var connection = context.Database.GetDbConnection();
        
        // 接続文字列を取得
        string connectionString = connection.ConnectionString;
        
        Console.WriteLine("接続文字列: " + connectionString);
    
        // ここで接続文字列を使った処理を追加
    }
    

    このように非常に簡単なコードで接続文字列を取得することができます。とはいえ、実際の運用ではいくつかの注意点も存在します。


    注意点とベストプラクティス

    1. 実際の接続文字列と異なる可能性がある

    DbConnection.ConnectionString で取得した接続文字列は、構成ファイルに定義された元の文字列と異なることがあります

    これは、以下のような理由によるものです:

    • 接続プールの影響で接続情報が最適化されている
    • 特定のフレームワークやライブラリが接続文字列を書き換えている
    • クエリ文字列内でマスク処理や変換処理が施されている

    そのため、「元の接続文字列そのままが欲しい」という目的には、IConfigurationDbContextOptions から直接取得した方が確実な場合があります。


    2. 接続文字列の一部情報が取得できない可能性がある

    一部の接続文字列プロバイダ(特にクラウド系のセキュア接続)では、セキュリティの観点から パスワードやアクセストークンなどの機密情報が省略される ことがあります。

    たとえばAzure SQL Databaseなどでは、接続確立後にパスワード部分がマスクされることがあるため、ConnectionString プロパティを確認しても完全な文字列ではない場合があります。


    3. セキュリティリスクに注意

    接続文字列には データベースの認証情報(ユーザー名やパスワード) が含まれていることが多いため、安易に出力・ログ・外部出力するのは非常に危険です。

    具体的には以下のようなリスクが存在します:

    • ログファイルが第三者に流出し、接続情報が漏洩する
    • デバッグ情報が本番環境で有効になっていて意図せず情報を露出する
    • 内部ツールに接続文字列をハードコードしてセキュリティホールになる

    ベストプラクティスとしては、以下のように扱いましょう:

    • 接続文字列をログに出力しない
    • デバッグ用に取得する場合でも本番環境では無効にする
    • 必ず SecureString や暗号化された方法で処理することを検討する

    他の接続文字列取得方法

    前述のように、DbContextから直接取得する以外にも、より安全に接続文字列を取得する方法があります。

    1. IConfigurationから取得する

    appsettings.json や環境変数で設定された接続文字列は、以下のようにして取得可能です:

    var configuration = serviceProvider.GetRequiredService<IConfiguration>();
    var connectionString = configuration.GetConnectionString("DefaultConnection");

    こちらの方法であれば、元の定義どおりの接続文字列が取得できますし、管理もしやすくなります。


    2. DbContextOptionsから取得する

    依存性注入(DI)を活用している場合は、DbContextOptions<TContext> 経由で接続文字列を取得できます:

    public class MyService
    {
        private readonly MyDbContext _context;
    
        public MyService(MyDbContext context)
        {
            _context = context;
    
            var connectionString = _context.Database.GetDbConnection().ConnectionString;
        }
    }
    

    ただし、オプションを直接参照したい場合は、コンストラクタで DbContextOptions<MyDbContext> を受け取ることで、より柔軟に設定へアクセス可能です。


    結論

    DbContext から接続文字列を取得することは、開発やデバッグの場面で役に立ちますが、以下の点をしっかり理解しておく必要があります。

    • 取得できる接続文字列は必ずしも元の構成と一致しない
    • セキュリティ上のリスクが存在するため取り扱いに注意が必要
    • 本番コードには原則として使用せず、設定ファイルや環境変数を使うのが安全

    開発の効率化やトラブルシューティングを行う際には非常に便利な手法ではありますが、セキュリティと再現性の観点からも 「一時的な利用に留める」 のが良いでしょう。


    おわりに

    接続文字列の取り扱いは、アプリケーションのセキュリティや可搬性に直結します。安易なログ出力やハードコーディングを避け、ベストプラクティスに基づいた実装を心がけましょう。

    今後もEF Coreを活用した開発において、安全で拡張性の高い構成を目指していきましょう!

  • [C#, VB.NET]環境変数を使用したコードの動作確認方法

    [C#, VB.NET]環境変数を使用したコードの動作確認方法

    【VB.NET / C#】ClickOnceと環境変数を使ったアプリ動作の切り替えと確認方法

    アプリケーション開発において、「環境変数」を利用してアプリケーションの挙動を制御するのは、非常に一般的な手法です。特に、アプリケーションのデプロイ方法(例:ClickOnceなど)や実行環境(開発用・本番用など)に応じて処理を分岐したい場合、環境変数はとても便利な仕組みになります。

    この記事では、ClickOnceを利用した.NETアプリケーションの挙動を環境変数で制御し、それをVB.NETおよびC#の両方で実装・確認する方法を、段階を追って解説していきます。


    🔍 この記事でわかること

    • 環境変数を使った.NETアプリの挙動切り替え方法
    • Visual Studioでの環境変数設定方法
    • VB.NET / C# の実装例と動作確認
    • ClickOnceを使った本番環境に近いテスト手法
    • ユニットテストでの環境変数モック手法
    • 環境変数を使う際の注意点

    🧪 なぜ環境変数を使うのか?

    たとえば、アプリケーションがClickOnceを使ってネットワークからインストールされて実行された場合、特定の処理(バージョン表示や、ログ収集など)を行いたいケースがあります。しかし、コード上で ApplicationDeployment.IsNetworkDeployed を直接使うと、ClickOnceを使わずに実行したときに例外が発生する恐れもあるため、柔軟な方法として「環境変数を使って動作を切り替える」という手法が活躍します。


    🧾 VB.NETでの実装例

    If Environment.GetEnvironmentVariable("ClickOnce_IsNetworkDeployed")?.ToLower() = "true" Then
        Dim version = Environment.GetEnvironmentVariable("ClickOnce_CurrentVersion")
        ' TODO: version情報があっているか確かめたい
       Version_System = "Ver." + version
    End If

    このコードは、環境変数 ClickOnce_IsNetworkDeployed"true" に設定されている場合にのみ、バージョン情報を取得し、それを Version_System に代入します。


    🧾 C#での実装例

    if (Environment.GetEnvironmentVariable("ClickOnce_IsNetworkDeployed")?.ToLower() == "true")
    {
        var version = Environment.GetEnvironmentVariable("ClickOnce_CurrentVersion");
        Version_System = "Ver." + version;
    }

    VB.NETとC#では構文こそ異なるものの、基本的なロジックは共通です。


    ✅ 開発中に動作を確認する方法

    1. Visual Studioで環境変数を設定する方法

    開発中に手軽に環境変数を設定する方法として、Visual Studioのプロジェクトプロパティから設定する方法があります。

    1. プロジェクトを右クリックし、「プロパティ」を開きます。
    2. 「デバッグ」タブを選択します。
    3. 「環境変数」の項目に以下を追加します:
      • ClickOnce_IsNetworkDeployed : true
      • ClickOnce_CurrentVersion : 1.0.0.0

    こうすることで、デバッグ実行時にこれらの環境変数がプロセススコープで適用され、コードで取得できるようになります。


    2. システム環境変数として設定(注意)

    Windowsの「システム環境変数」に設定することで、OS全体のプロセスから参照できるようにもできますが、これは開発中には推奨されません。誤って他のアプリに影響を与えてしまう可能性もあるため、基本はVisual Studioのデバッグ設定で十分です。


    3. デバッグとログ出力による確認

    以下のようにログをコンソール出力することで、環境変数が正しく設定され、意図通りの分岐がされているかを確認できます。

    VB.NET版ログ付きコード

    Dim isDeployed = Environment.GetEnvironmentVariable("ClickOnce_IsNetworkDeployed")?.ToLower()
    Console.WriteLine($"IsNetworkDeployed: {isDeployed}")
    If isDeployed = "true" Then
        Dim version = Environment.GetEnvironmentVariable("ClickOnce_CurrentVersion")
        Console.WriteLine($"CurrentVersion: {version}")
        Version_System = "Ver." + version
    Else
        Console.WriteLine("ClickOnce deployment is not detected.")
    End If

    C#版ログ付きコード

    var isDeployed = Environment.GetEnvironmentVariable("ClickOnce_IsNetworkDeployed")?.ToLower();
    Console.WriteLine($"IsNetworkDeployed: {isDeployed}");
    if (isDeployed == "true")
    {
        var version = Environment.GetEnvironmentVariable("ClickOnce_CurrentVersion");
        Console.WriteLine($"CurrentVersion: {version}");
        Version_System = "Ver." + version;
    }
    else
    {
        Console.WriteLine("ClickOnce deployment is not detected.");
    }

    4. ClickOnceによる本番形式のテスト

    実際にClickOnceでアプリケーションを発行し、インストールして実行することで、環境変数を自動で設定した状態での挙動も確認できます。ClickOnceでは、ApplicationDeployment.CurrentDeployment.CurrentVersion.ToString() を使ってバージョンを取得できますが、これを環境変数に設定しておくことで、アプリケーションロジックからも取得可能になります。


    5. ユニットテストでの環境変数テスト

    ユニットテストで環境変数を使ったコードをテストするには、テスト実行前に Environment.SetEnvironmentVariable() を使って変数を設定できます。


    ⚠️ 環境変数を扱う際の注意点

    1. 大文字・小文字に注意
      環境変数はWindows環境では大文字小文字を区別しませんが、コード上の文字列比較では区別される可能性があります。.ToLower().Equals(x, StringComparison.OrdinalIgnoreCase) を使うのが安全です。
    2. nullチェックは必須
      指定した環境変数が存在しない場合、GetEnvironmentVariablenull を返します。これをそのまま使うと NullReferenceException が発生することがあるため、null条件演算子(?.)を使うか、明示的なnullチェックを行いましょう。
    3. 本番では別の設定方法も検討
      ClickOnce環境では環境変数よりも、アプリケーション設定ファイル(app.configuser.config)を利用することも検討できます。

    📝 まとめ

    環境変数を活用することで、開発と本番環境の違いに柔軟に対応でき、ClickOnceなどのデプロイ形式に応じた挙動制御が可能になります。VB.NETとC#のコードも非常に似ており、どちらでも同様の実装が行えます。

    開発中にはVisual Studioでの環境変数設定、ログ出力、ステップ実行、ユニットテストを活用し、確実に正しい動作を確認しましょう。


    📌 最後に

    環境に応じた設定をコードで判断できるようになると、より堅牢で柔軟なアプリケーションを構築できるようになります。ClickOnceや環境変数といった仕組みをうまく活用し、開発効率と信頼性を両立させていきましょう!

  • [C#]MS.DIにおけるサービスのライフタイムとスコープの関係について

    Microsoft.Extensions.DependencyInjection(以下、MS.DI)を使用すると、依存性注入によりサービスのライフタイムを制御できます。ライフタイムには主に以下の3つがあります。

    • Singleton
    • Transient
    • Scoped

    今回は、これらのライフタイムがスコープの設定によってサービスのインスタンス(Idの値)にどのような影響を与えるかを、単体テストを通じて確認します。

    単体テストのコード

    以下の単体テストコードでは、ISomeServiceというインターフェースと、それを実装するSomeServiceクラスを使用しています。SomeServiceは、インスタンスごとに異なるIdGuid)を持ちます。

    public interface ISomeService
    {
        Guid Id { get; }
    }
    
    public class SomeService : ISomeService
    {
        public Guid Id { get; } = Guid.NewGuid();
    }
    

    Singletonの場合

    Test1: スコープを設定しない場合

    [Fact]
    public void Test1()
    {
        var services = new ServiceCollection();
        services.AddSingleton<ISomeService, SomeService>();
        
        var provider = services.BuildServiceProvider();
    
        var service = provider.GetService<ISomeService>();
        var service2 = provider.GetService<ISomeService>();
        Assert.NotNull(service);
        Assert.NotNull(service2);
    
        Assert.Equal(service.Id, service2.Id);
    }
    

    Test1_1: スコープを設定する場合

    [Fact]
    public void Test1_1()
    {
        var services = new ServiceCollection();
        services.AddSingleton<ISomeService, SomeService>();
    
        var provider = services.BuildServiceProvider();
    
        var scopeFactory = provider.GetService<IServiceScopeFactory>();
        Assert.NotNull(scopeFactory);
    
        using var scope1 = scopeFactory.CreateScope();
        var service = scope1.ServiceProvider.GetService<ISomeService>();
        Assert.NotNull(service);
    
        using var scope2 = scopeFactory.CreateScope();
        var service2 = scope2.ServiceProvider.GetService<ISomeService>();
        Assert.NotNull(service2);
    
        Assert.Equal(service.Id, service2.Id);
    }
    

    Transientの場合

    Test2: スコープを設定しない場合

    [Fact]
    public void Test2()
    {
        var services = new ServiceCollection();
        services.AddTransient<ISomeService, SomeService>();
    
        var provider = services.BuildServiceProvider();
    
        var service = provider.GetService<ISomeService>();
        var service2 = provider.GetService<ISomeService>();
        Assert.NotNull(service);
        Assert.NotNull(service2);
    
        Assert.NotEqual(service.Id, service2.Id);
    }
    

    Test2_1: スコープを設定する場合

    [Fact]
    public void Test2_1()
    {
        var services = new ServiceCollection();
        services.AddTransient<ISomeService, SomeService>();
    
        var provider = services.BuildServiceProvider();
    
        var scopeFactory = provider.GetService<IServiceScopeFactory>();
        Assert.NotNull(scopeFactory);
    
        using var scope1 = scopeFactory.CreateScope();
        var service = scope1.ServiceProvider.GetService<ISomeService>();
        Assert.NotNull(service);
    
        using var scope2 = scopeFactory.CreateScope();
        var service2 = scope2.ServiceProvider.GetService<ISomeService>();
        Assert.NotNull(service2);
    
        Assert.NotEqual(service.Id, service2.Id);
    }
    

    Scopedの場合

    Test3: スコープを設定しない場合

    [Fact]
    public void Test3()
    {
        var services = new ServiceCollection();
        services.AddScoped<ISomeService, SomeService>();
    
        var provider = services.BuildServiceProvider();
    
        var service = provider.GetService<ISomeService>();
        var service2 = provider.GetService<ISomeService>();
        Assert.NotNull(service);
        Assert.NotNull(service2);
    
        Assert.Equal(service.Id, service2.Id);
    }
    

    Test3_1: スコープを設定する場合

    [Fact]
    public void Test3_1()
    {
        var services = new ServiceCollection();
        services.AddScoped<ISomeService, SomeService>();
    
        var provider = services.BuildServiceProvider();
    
        var scopeFactory = provider.GetService<IServiceScopeFactory>();
        Assert.NotNull(scopeFactory);
    
        using var scope1 = scopeFactory.CreateScope();
        var service = scope1.ServiceProvider.GetService<ISomeService>();
        Assert.NotNull(service);
        
        using var scope2 = scopeFactory.CreateScope();
        var service2 = scope2.ServiceProvider.GetService<ISomeService>();
        Assert.NotNull(service2);
    
        Assert.NotEqual(service.Id, service2.Id);
    }
    

    結果のまとめ

    AddSingletonの場合

    • スコープを設定しない場合でも、設定した場合でも、Idは変わらない。
    • 同一のインスタンスが常に提供される。

    AddTransientの場合

    • スコープを設定しない場合でも、設定した場合でも、Idは毎回変わる。
    • 新しいインスタンスが常に生成される。

    AddScopedの場合

    • スコープを設定しない場合、Idは変わらない。
      • デフォルトでは、BuildServiceProviderで生成されたルートスコープが使用されるため。
    • スコープを設定した場合、スコープ間でIdが変わる。
      • 同一スコープ内では同じインスタンスが提供されるが、異なるスコープ間では新しいインスタンスが生成される。

    詳細な解説

    1. Singletonの動作

    AddSingletonで登録されたサービスは、アプリケーション全体で単一のインスタンスが使用されます。スコープを設定しても、同じインスタンスが返されるため、Idは変わりません。

    • ポイント: スコープに関係なく、常に同じインスタンスが提供される。

    2. Transientの動作

    AddTransientで登録されたサービスは、サービスを取得するたびに新しいインスタンスが生成されます。スコープの有無にかかわらず、毎回異なるIdとなります。

    • ポイント: スコープに関係なく、常に新しいインスタンスが生成される。

    3. Scopedの動作

    AddScopedで登録されたサービスは、スコープごとに一つのインスタンスが提供されます。

    • スコープを設定しない場合:
      • BuildServiceProviderで生成されたルートスコープが使用されます。
      • そのため、同じインスタンスが返され、Idは変わりません。
    • スコープを設定した場合:
      • 新しいスコープ(IServiceScope)を作成することで、そのスコープ内では同じインスタンスが使用されますが、異なるスコープ間では別のインスタンスが生成されます。
      • したがって、Idはスコープごとに異なります。
    • ポイント: 同一スコープ内では同じインスタンス、異なるスコープ間では異なるインスタンスが提供される。

    スコープの作成と管理

    スコープは通常、WebアプリケーションではHTTPリクエストごとに自動的に作成されます。しかし、コンソールアプリケーションや単体テストでは、以下のように明示的にスコープを作成する必要があります。

    var scopeFactory = provider.GetService<IServiceScopeFactory>();
    using var scope = scopeFactory.CreateScope();
    

    実用的なポイント

    • Singletonを使う場合:
      • アプリケーション全体で共有する設定やリソースに適しています。
      • スレッドセーフである必要があります。
    • Transientを使う場合:
      • ステートレスなサービスや、一時的なデータを扱う場合に適しています。
    • Scopedを使う場合:
      • リクエストごとに状態を持たせたい場合に適しています。
      • Webアプリケーションでは特に有用です。

    まとめ

    • AddSingleton:
      • スコープに関係なく、常に同じインスタンスが提供される。
    • AddTransient:
      • スコープに関係なく、常に新しいインスタンスが生成される。
    • AddScoped:
      • 同一スコープ内では同じインスタンスが提供されるが、異なるスコープ間では別のインスタンスが生成される。

    サービスのライフタイムとスコープの理解は、正しい依存性注入の実装に不可欠です。特に、サービスが状態を持つ場合やリソースの消費を最適化したい場合、適切なライフタイムを選択することでアプリケーションのパフォーマンスと信頼性を向上させることができます。

  • [C#,VB.NET]サポートされていないShift-JISエンコーディングのエラーとその解決方法

    はじめに

    .NET Coreや.NET 5+以降の環境で、Shift-JIS(日本語エンコーディング)を使用しようとすると、次のようなエラーが発生することがあります。

    System.ArgumentException: 'Shift-JIS' is not a supported encoding name.

    これは、.NET Coreや新しい.NETランタイムでは、Shift-JISなどの特定のエンコーディングがデフォルトではサポートされていないためです。しかし、CodePagesEncodingProviderを使用して、これらのエンコーディングを有効にすることができます。

    この記事では、C#とVB.NETでの解決方法を解説します。

    エラーの原因

    従来の.NET FrameworkではShift-JISエンコーディングがデフォルトでサポートされていましたが、.NET Coreや.NET 5+では軽量化とパフォーマンスの向上を目的に、デフォルトのエンコーディングサポートが制限されています。そのため、Shift-JISなどのコードページベースのエンコーディングを使用する場合は、明示的にサポートを有効にする必要があります。

    解決方法

    Step 1: CodePagesEncodingProviderの登録

    エンコーディングを有効にするために、アプリケーションの初期化時にCodePagesEncodingProviderを登録します。これにより、Shift-JISを含む多くのエンコーディングが利用可能になります。

    C#での実装

    C#では、次のようにしてShift-JISエンコーディングを有効にできます。

    using System;
    using System.Text;
    
    class Program
    {
        static void Main(string[] args)
        {
            // CodePagesEncodingProviderを登録してShift-JISを有効にする
            Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
    
            // Shift-JISエンコーディングを取得
            Encoding encoding = Encoding.GetEncoding("Shift-JIS");
    
            // 以下でShift-JISエンコーディングが利用可能です。
        }
    }
    

    VB.NETでの実装

    VB.NETでは、同様にCodePagesEncodingProviderを登録することで、Shift-JISをサポートします。

    Imports System.Text
    
    Module Program
        Sub Main()
            ' Shift-JISをサポートするためのエンコーディングプロバイダを登録
            Encoding.RegisterProvider(CodePagesEncodingProvider.Instance)
    
            ' Shift-JISエンコーディングを取得
            Dim encoding As Encoding = Encoding.GetEncoding("Shift-JIS")
    
        ' 以下でShift-JISエンコーディングが利用可能です。
        End Sub
    End Module
    

    コードの説明

    Encoding.RegisterProvider(CodePagesEncodingProvider.Instance)

    この一行がポイントです。CodePagesEncodingProvider.InstanceEncoding.RegisterProviderに渡すことで、Shift-JISを含む追加のコードページベースのエンコーディングがサポートされるようになります。このメソッドを呼ばなければ、Shift-JISを使おうとした際に、ArgumentExceptionが発生します。

    Encoding.GetEncoding("Shift-JIS")

    GetEncodingメソッドでShift-JISエンコーディングを取得します。このメソッドはエンコーディングの名前を引数として受け取り、そのエンコーディングを使用するオブジェクトを返します。

    よくある質問 (FAQ)

    1. なぜShift-JISがデフォルトでサポートされていないのですか?

    .NET Coreや.NET 5+では、軽量かつ高速なランタイムを目指しており、一般的でないエンコーディングはデフォルトで省かれています。日本語環境でよく使われるShift-JISもその一部です。

    2. Encoding.GetEncodingで他のエンコーディングも使用できますか?

    はい、CodePagesEncodingProviderを登録すれば、Shift-JIS以外にもEUC-JPやISO-2022-JPなど、さまざまなエンコーディングが使用可能です。

    3. すべての.NETバージョンでこの方法は使えますか?

    この解決方法は、.NET Core 3.0以降および.NET 5+で利用可能です。従来の.NET Frameworkでは、特にエンコーディングプロバイダの登録は不要です。

    まとめ

    .NET Coreや.NET 5+では、Shift-JISエンコーディングを使用するためにCodePagesEncodingProviderの登録が必要です。C#やVB.NETでShift-JISを利用する場合、アプリケーションの初期化時にこのプロバイダを登録することで、スムーズにエンコーディングを使えるようになります。

    Shift-JISエンコーディングの対応に困っている方は、ぜひこの記事の方法を試してみてください。

  • [C#,.NET8]DataContextが使えない

    .NET Framework 4.8.1から.NETへ移行する際にDataContextが使えません。

    DataContext クラス (System.Data.Linq) | Microsoft Learn

    このため、.NETでは、別のORMを採用する必要があります。

    候補として挙がるORMは、

    • Entity Framework Core: .NETの標準的なORMとして広く利用されています。Entity Framework Coreは、リレーショナルデータベースとのやり取りを容易にし、LINQを利用してデータベースクエリを記述することができます。また、コードファーストやデータベースファーストのアプローチもサポートしています。
    • Dapper
    • NHibernate
    • Linq to DB

    です。

    個人的には、EF Coreが.NETの標準的なORMとして普及している点や、Linqを使って直感的に操作でき使いやすい点、アップデートも頻繁に行われている点で、お勧めです。

  • Intersect メソッドの活用法

    コレクション操作において、ある2つのリストや配列の共通要素を取得したい場合、C#の Intersect メソッドは非常に便利です。本記事では、Intersect メソッドの基本的な使い方から、実際の活用方法や注意点までを詳しく解説します。

    1. Intersect メソッドとは?

    Intersect メソッドは、2つのコレクションの共通部分を抽出するためのメソッドです。具体的には、あるリストや配列の要素のうち、もう一方のコレクションにも含まれている要素のみを返します。例えば、以下の2つのリストがあるとします。

    var list1 = new List<int> { 1, 2, 3, 4, 5 };
    var list2 = new List<int> { 3, 4, 5, 6, 7 };

    この2つのリストに対して Intersect メソッドを使うと、共通部分である {3, 4, 5} を取得できます。

    var intersection = list1.Intersect(list2);

    この結果、intersection には共通要素の列挙が含まれます。

    2. Intersect の使い方

    Intersect の基本的な使い方は非常にシンプルです。以下に典型的な使用例を示します。

    var list1 = new List<string> { "apple", "banana", "cherry" };
    var list2 = new List<string> { "banana", "cherry", "date" };
    
    var commonFruits = list1.Intersect(list2);
    
    foreach (var fruit in commonFruits)
    {
        Console.WriteLine(fruit);
    }

    このコードを実行すると、bananacherry が出力されます。

    3. Intersect の応用例

    実際のアプリケーションでは、Intersect は次のような場面で役立ちます。

    • データフィルタリング: 複数の条件に一致するデータセットを取得する際に使用できます。
    • 権限管理: ユーザーが複数のグループに属している場合、それらのグループが持つ共通の権限を取得する際に便利です。

    たとえば、次のようにユーザーの権限をチェックすることができます。

    var userRoles = new List<string> { "Admin", "Editor" };
    var requiredRoles = new List<string> { "Admin", "User" };
    
    var hasRequiredRole = userRoles.Intersect(requiredRoles).Any();
    
    if (hasRequiredRole)
    {
        Console.WriteLine("ユーザーは必要な権限を持っています。");
    }

    4. パフォーマンス

    Intersect メソッドはシンプルで強力ですが、大規模なデータセットに対して使用する場合には注意が必要です。内部的に HashSet を使用しているため、Intersect の計算量は一般的に O(n) です。これは効率的ですが、入力コレクションのサイズやデータの内容によってはパフォーマンスに影響を与える可能性があります。

    Intersect メソッドの計算量

    1. 初期化とセットアップ: 最初に、一方のコレクション(例えば list2)を HashSet に変換します。この変換には、リストの要素数に応じて O(n) の時間がかかります。
    2. Intersect 操作: 次に、もう一方のコレクション(例えば list1)の各要素に対して、HashSet 内にその要素が存在するかを確認します。この検索操作自体は O(1) ですが、これを list1 の全要素に対して行うため、最終的に O(m) の時間がかかります(ここで m は list1 の要素数)。

    これらを合わせると、Intersect メソッド全体の計算量は以下のようになります。

    • O(n) + O(m) = O(m + n)

    ここで、nlist2 の要素数、mlist1 の要素数です。

    runtime/src/libraries/System.Linq/src/System/Linq/Intersect.cs at main · dotnet/runtime · GitHub

    5.注意点

    特に、データの順序が重要な場合は、Intersect が順序を保持しないことに注意してください。順序を考慮する場合には、追加の処理が必要です。

    6. 結論

    Intersect メソッドは、コレクション操作において簡単に共通要素を取得できる便利なツールです。データのフィルタリングや権限管理など、さまざまな用途に応用できるため、ぜひ活用してみてください。

    効率的なコーディングと適切なデータ処理を行うためには、Intersect のようなメソッドを理解し、状況に応じて適切に使用することが重要です。データセットの規模や用途に応じた最適なアプローチを選択し、効果的なコーディングを実現しましょう。

  • [C#]Any関数の中にAny関数がある場合のリファクタリング

    あるリストの中に別のリストの要素が存在するかを確認したい場合、以下のようなコードを書くことができます。

    var flag = list.Any(x => list2.Any(y => y == x));

    しかし、Any関数の中にAny関数が入っており、可読性がかなり悪いです。ぱっと見は何をやっているのか全く分かりません。あと別の記事で書いていますが、かなり遅いです。

    [C#]Any関数の速度 – TOMATONOTE (tomato-note.com)

    今回は備忘録的に修正案を三つ提示します。

    Any の中で Contains を使用する

    Any の中で Contains メソッドを使うことで、より簡潔なコードにできます。O(n)で探索してくれます。

    var flag = list.Any(x => list2.Contains(x));

    Intersect を使用する

    Intersect メソッドを使うことで、両リストの共通要素が存在するかどうかをチェックできます。Any を使って共通要素が1つでもあるか確認します。

    var flag = list.Intersect(list2).Any();

    HashSet を使用して効率化

    ToHashSetの計算量が無視できるくらいlist2 が大きい場合、HashSet を使うことで、Contains 操作を高速化できます。

    var set = list2.ToHashSet();
    var flag = list.Any(x => set.Contains(x));
  • [C#]Any関数の速度

    ChatGPT大先生にとあるAny関数を使ったソースコードを添削していただくと、大体HashSetにしたほうがO(1)だから、こっちの方がいいよと仰います。

    本当なのかいろいろと試してみたいと思います。

    素のままの検索速度の差

    順序がランダムな10000の数字の配列を用意します。その中から0を探すことを1000回行ったときの速度を計測しました。

    Any関数の場合

    var list = Enumerable.Range(0,10000).OrderBy(i=>Guid.NewGuid()).ToArray();
    var stopwatch = Stopwatch.StartNew();
    foreach(var i in Enumerable.Range(0,1000))
    {
        var flag = list.Any(x => x == 0);
    }
    stopwatch.Stop();
    Console.WriteLine($"処理時間:{stopwatch.ElapsedMilliseconds}ms");
    処理時間:43ms
    処理時間:41ms
    処理時間:41ms
    処理時間:41ms
    処理時間:42ms
    処理時間:17ms
    処理時間:14ms
    処理時間:14ms
    処理時間:13ms
    処理時間:13ms

    HashSetの場合

    var list = Enumerable.Range(0,10000).OrderBy(i=>Guid.NewGuid()).ToArray();
    var stopwatch = Stopwatch.StartNew();
    foreach(var i in Enumerable.Range(0,1000))
    {
        var hashSet = list.ToHashSet();
        var flag = hashSet.Contains(0);
    }
    stopwatch.Stop();
    Console.WriteLine($"処理時間:{stopwatch.ElapsedMilliseconds}ms");
    処理時間:131ms
    処理時間:121ms
    処理時間:110ms
    処理時間:123ms
    処理時間:169ms
    処理時間:114ms
    処理時間:120ms
    処理時間:153ms
    処理時間:143ms
    処理時間:134ms

    個人的には既に意外ですが、まさかのAny関数の勝利でした。

    毎回ToHashSetする方がパフォーマンスとしては悪いという結果になりました。

    ちなみにToHashSet()をタイマーの外で行うと、さすがO(1) の実行速度です。

    var list = Enumerable.Range(0,10000).OrderBy(i=>Guid.NewGuid()).ToArray();
    var hashSet = list.ToHashSet();
    var stopwatch = Stopwatch.StartNew();
    foreach(var i in Enumerable.Range(0,1000))
    {
        var flag = hashSet.Contains(0);
    }
    stopwatch.Stop();
    Console.WriteLine($"処理時間:{stopwatch.ElapsedMilliseconds}ms");
    処理時間:0ms
    処理時間:0ms
    処理時間:0ms
    処理時間:0ms
    処理時間:0ms
    処理時間:0ms
    処理時間:0ms
    処理時間:0ms
    処理時間:0ms
    処理時間:0ms

    配列の中に別の配列の要素が含まれるか

    とある大きい配列の中に、別で用意した配列が含まれているかのチェックをよくやることがあり、これをChatGPT先生に聞くと、HashSetを使ったほうが良いという回答をよく貰います。

    文章だと分かりづらいので、例をソースコードで示すと、下記のようになります。

    var list1 = [0,1,2,3,4,5,6,7,8,9];
    var list2 = [2,5];
    // 自分が提案した方法
    var list3 = list1.Where(x => list2.Any(y => x == y));
    
    // ChatGPT先生のリファクタリング結果
    var hashSet = list2.ToHashSet();
    var list4 = list1.Where(x => hashSet.Contains(x));

    実際の実務で使うコードは、オブジェクトの配列となるため、これよりも複雑ですが、大雑把に言えば、このようになります。もちろん配列の大きさ、検索するオブジェクトの位置によって実行速度は大きく変わってきますが、ここでは、大量のオブジェクトの中を検索するという前提で考えていきます。

    Any関数を使う場合

    var list = Enumerable.Range(0,10000).OrderBy(i=>Guid.NewGuid()).ToArray();
    var list2 = Enumerable.Range(0,1000);
    var stopwatch = Stopwatch.StartNew();
    foreach(var i in Enumerable.Range(0,1000))
    {
        var flag = list.Any(x => list2.Any(y => y == x));
    }
    stopwatch.Stop();
    Console.WriteLine($"処理時間:{stopwatch.ElapsedMilliseconds}ms");
    処理時間:182ms
    処理時間:174ms
    処理時間:172ms
    処理時間:171ms
    処理時間:181ms
    処理時間:176ms
    処理時間:175ms
    処理時間:176ms
    処理時間:181ms
    処理時間:190ms

    Any関数が二つあって一見、何をやってるのか非常に分かりにくいので、出来れば、これは避けたいです。

    HashSetを使う場合

    var list = Enumerable.Range(0,10000).OrderBy(i=>Guid.NewGuid()).ToArray();
    var list2 = Enumerable.Range(0,1000);
    var stopwatch = Stopwatch.StartNew();
    foreach(var i in Enumerable.Range(0,1000))
    {
        var hash = list2.ToHashSet();
        var flag = list.Any(x => hash.Contains(x));
    }
    stopwatch.Stop();
    Console.WriteLine($"処理時間:{stopwatch.ElapsedMilliseconds}ms");
    処理時間:12ms
    処理時間:7ms
    処理時間:7ms
    処理時間:7ms
    処理時間:7ms
    処理時間:7ms
    処理時間:6ms
    処理時間:6ms
    処理時間:8ms
    処理時間:10ms

    !!

    list2が大きい場合、list2をHashSetにする方が速くなりました。

  • [C#, VB.NET] 警告CA1416の意味

    CA1416は、.NETにおけるプラットフォームの互換性に関連する警告メッセージです。この警告は、コードが特定のプラットフォーム(例えば、Windows、Linux、macOSなど)でのみ実行される可能性があることを示しています。

    CA1416 警告の意味

    この警告は、あなたが書いたコードが現在実行中のプラットフォームで動作しない可能性があることを知らせるためのものです。たとえば、Windows固有のAPIを使用しているコードをクロスプラットフォームアプリケーションに含めると、この警告が表示されることがあります。

    対策方法

    条件付きコンパイルディレクティブを使用する

    特定のプラットフォームでのみ実行されるコードを囲むために、条件付きコンパイルディレクティブを使用できます。

    #if WINDOWS
    // Windows固有のコード
    #endif

    プラットフォームのチェック

    実行時にプラットフォームをチェックし、対応するコードを実行することもできます。

    if (OperatingSystem.IsWindows())
    {
        // Windows固有のコード
    }
    else if (OperatingSystem.IsLinux())
    {
        // Linux固有のコード
    }

    プラットフォーム互換性属性を使用する

    特定のプラットフォームでのみ利用可能なAPIを示すために、プラットフォーム互換性属性を使用できます。

    [SupportedOSPlatform("windows")]
    public void WindowsSpecificMethod()
    {
        // Windows固有のコード
    }

    プラットフォーム互換性の確認

    .NET 5以降では、プラットフォーム互換性を確認するための新しいAPIが導入されました。これにより、特定のプラットフォームでのみ使用可能なAPIや機能を明示的に指定できます。これにより、開発者はコードが異なるプラットフォームでどのように動作するかをより正確に制御できます。

    まとめ

    CA1416警告は、クロスプラットフォームアプリケーションの開発者がコードの互換性を確保するために重要な指針となります。この警告を無視せず、適切な対策を講じることで、アプリケーションの信頼性と移植性を向上させることができます。