2021年9月13日 • ☕️ 3 min read

動的な型であるJavaScriptのスーパーセットといわれるTypeScriptと異なり、C# .Netが元々静的な型となります。C#のinterfaceをreflectでき、interfaceからpropertiesを取れるので、かなり便利です。今回はC#のエレガントなDIを簡単に紹介したいと思います。そして、TypeScriptにはないnamespace単位でのbindingもやってみたいです。

シリーズ一覧

  1. TypeScriptのDI、その壱:InversifyJS
  2. TypeScriptのDI、その貮:TSyringe
  3. TypeScriptのDI、その弎:TypeDI
  4. TypeScriptのDI、その肆:nestjs
  5. C# .NetFrameworkのDI ←← ここ

interfaceでconstructor injectionを行う理由

  • 単体テストをすごく簡単にできるようになる:interfaceにmockをbindingすればテストを書ける
  • 実装の入れ替えは簡単にできる:例えばILoggerの実装はbindingするとき決められる(consoleに出力するか、ログファイルにアウトプットするか)
  • 大量なnewをなくして、インスタンスのライフサイクルのコントロールがDIライブラリに任せられる

実装方法

interfaceと実装classの実装方法はTypeScriptと変わりません。ただ、DIするためにわざわざdecoratorする必要がまったくなく、よりシンプルとも言えるでしょう。

  1. まずinterfaceと実装部分を定義します。
ProjectService.cs
namespace DBServices {
  public interface IProjectService {
    Task<Project> getProject(string id);
  }

  public class ProjectService : IProjectService {
    public async Task<Project> getProject(string id) {
      // ...get data from db
    }
  }
}
  1. tokenの定義は一切不要で、ProjectServiceを使うところにconstructor injectionを簡単にできます。
EmployeeService.cs
namespace DBServices {
  public interface IEmployeeService {
    Task<Employee> getEmployee(int id);
  }

  public class EmployeeService : IEmployeeService {
    private readonly IProjectService projectService;

    public EmployeeService(      // decoratorがなくてすっきりでしょう      IProjectService projectService
    ) {
      this.projectService = projectService;
    }

    async Task<Employee> getEmployee(int id) {
      // inject成功!!使える!!
      const project = await this.projectService.getProject("code-secret");

      // do other work
    }
  }
}
  1. interfaceと実装classのbindingを行います。

まず一般なbinding方法ならはTypeScriptと同じです。C#のbindingはStartup.csの中で行えます。

Startup.cs
public class Startup
{
  public void ConfigureServices(IServiceCollection services)
  {
    // Singletonの場合はAddSingleton
    services.AddScoped<IProjectService, ProjectService>();
    services.AddScoped<IEmployeeService, EmployeeService>();
  }
}

namespace単位でbinding

binding対象が増えると、services.AddScopedをコピペしなければいけません。ただし、私たちはDo not repeat ourselves。C#のReflectionでこの問題を解決できます。

// AddTransient functionはnamespace単位でbindingできる
private void AddTransient(IServiceCollection services, IEnumerable<Type> types, string nameSpaceEndWith)
{
  foreach (var type in types)
  {
    if (!type.IsClass || !type.Namespace.EndsWith(nameSpaceEndWith))
    {
      continue;
    }

    // ProjectServiceのclassを見つけられると、IProjectServiceの存在を確認する、存在したらbindingする
    var interfaceType = type.GetInterfaces().FirstOrDefault(x => x.Name == $"I{type.Name}");
    if (interfaceType == null)
    {
      continue;
    }

    services.AddTransient(interfaceType, type);
  }
}

使い方:

// 名前がDBServicesで終わるnamespace中の全てのclassとinterfaceのbindingを行う
AddTransient(services, Assembly.GetExecutingAssembly().ExportedTypes, "DBServices");

これは本当に便利ですね。

感想

C# .NetFrameworkのDIはTypeScriptよりさらに使いやすいと言えるでしょう。文法的にTypeScriptがC#に似ているから、TypeScriptになれたら、C#にも好きになるでしょう。では~


関連投稿

TypeScriptのDI、その肆:nestjs

2021年9月12日

ThunderMiracle

Blog part of ThunderMiracle.com