動的な型であるJavaScriptのスーパーセットといわれるTypeScriptと異なり、C# .Netが元々静的な型となります。C#のinterfaceをreflectでき、interfaceからpropertiesを取れるので、かなり便利です。今回はC#のエレガントなDIを簡単に紹介したいと思います。そして、TypeScriptにはないnamespace単位でのbindingもやってみたいです。
シリーズ一覧
- TypeScriptのDI、その壱:InversifyJS
- TypeScriptのDI、その貮:TSyringe
- TypeScriptのDI、その弎:TypeDI
- TypeScriptのDI、その肆:nestjs
- C# .NetFrameworkのDI ←今はここ
interfaceでconstructor injectionを行う理由
- 単体テストをすごく簡単にできるようになる。interfaceにmockをbindingすればテストを書ける
- 実装の入れ替えは簡単にできる。例えば
ILogger
の実装はbindingするとき決められる(consoleに出力するか、ログファイルにアウトプットするか) - 大量な
new
をなくして、インスタンスのライフサイクルのコントロールがDIライブラリに任せられる
実装方法
interfaceと実装classの実装方法はTypeScriptと変わりません。ただ、DIするためにわざわざdecoratorする必要がまったくなく、よりシンプルとも言えるでしょう。
- まず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
}
}
}
- 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
}
}
}
- 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#にも好きになるでしょう。では。