C#でDI（依存性注入）を属性を使って自動登録する


# 1. はじめに
* JavaのSpring BootでDIをアノテーションを使ってできるように、C#でも同じようにやりたい
* ASP.NET CoreでService～DaoまでをDIできることを確認したい

# 2. 開発環境
* C#
* .NET 6 (ASP.Net Core)
* Visual Studio 2022
* Windows 11
* Microsoft.Extensions.DependencyInjection(NuGet)  

# 3. DIの実装
こちらの記事に掲載しているソースを使用させてもらいました。

https://blog.jbs.co.jp/entry/2022/09/07/120000

## 3.1. 属性の作成
```Csharp:ComponentAttribute.cs
using System;

namespace NX.DependencyInjection
{
    /// <summary>
    /// <see cref="Microsoft.Extensions.DependencyInjection.IServiceCollection" />にコンポーネントを自動登録するための属性
    /// </summary>
    [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
    public class ComponentAttribute : Attribute
    {
        /// <summary>
        /// コンポーネントのライフサイクル。未設定の場合は<see cref="ComponentScope.Scoped"/>。
        /// </summary>
        public ComponentScope Scope { get; set; } = ComponentScope.Scoped;

        /// <summary>
        /// コンポーネントの登録対象の型。未設定の場合はコンポーネント自身の型。
        /// </summary>
        public Type? TargetType { get; set; }

        public ComponentAttribute()
        {
        }

        public ComponentAttribute(ComponentScope scope, Type? targetType = null)
        {
            Scope = scope;
            TargetType = targetType;
        }
    }
}
```

## 3.2. リフレクションの実装
```Csharp:ComponentLoader.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

namespace NX.DependencyInjection
{
    public static class ComponentLoader
    {
        public static IEnumerable<ComponentInfo> Load(string assemblyName)
        {
            try
            {
                return Load(Assembly.Load(assemblyName));
            }
            catch (Exception e)
            {
                throw new AssemblyLoadException($"Failed to load assembly \"{assemblyName}\"", e);
            }
        }

        public static IEnumerable<ComponentInfo> Load(Assembly assembly)
        {
            // アセンブリ内のクラスから指定の属性を持つもののみを抽出
            return assembly.GetTypes()
                .SelectMany(type =>
                    type.GetCustomAttributes<ComponentAttribute>().Select(attr => (Type: type, Attr: attr)))
                .Select(x => new ComponentInfo(x.Attr.Scope, x.Attr.TargetType ?? x.Type, x.Type));
        }

        // ComponentAttributeの情報を保持しておくための箱
        public class ComponentInfo
        {
            public ComponentScope Scope { get; }
            public Type TargetType { get; }
            public Type ImplementType { get; }

            public ComponentInfo(ComponentScope scope, Type targetType, Type implementType)
            {
                Scope = scope;
                TargetType = targetType;
                ImplementType = implementType;

                if (!TargetType.IsAssignableFrom(ImplementType))
                {
                    throw new ArgumentException($"Type \"{TargetType.FullName}\" is not assignable from \"{ImplementType.FullName}\"");
                }
            }
        }
    }
}
```

## 3.3. 依存性注入機能への登録
```Csharp:ServiceCollectionExtensions.cs
using System;
using System.Linq;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace NX.DependencyInjection
{
    public static class ServiceCollectionExtensions
    {
        /// <summary>
        /// 指定のアセンブリからコンポーネントを検索して自動登録します。
        /// </summary>
        /// <param name="services">コンポーネントを登録するServiceCollection</param>
        /// <param name="assemblyNames">コンポーネントを検索するアセンブリの名前一覧</param>
        public static IServiceCollection RegisterComponents(this IServiceCollection services, params string[] assemblyNames)
        {
            foreach (var ci in assemblyNames.SelectMany(ComponentLoader.Load))
            {
                var lifetime = ci.Scope switch
                {
                    ComponentScope.Singleton => ServiceLifetime.Singleton,
                    ComponentScope.Scoped => ServiceLifetime.Scoped,
                    ComponentScope.Transient => ServiceLifetime.Transient,
                    _ => throw new InvalidOperationException("Unknown ComponentScope value")
                };

                services.Add(new ServiceDescriptor(ci.TargetType, ci.ImplementType, lifetime));
            }

            return services;
        }
    }
}
```

## 3.4. 依存性注入機能への登録
* ```// Add services to the container.```の下に、DIを自動登録するように記述を追加

```Csharp:Program.cs
using NX.DependencyInjection;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
// ---
// DIするサービスを自動登録する
AppDomain.CurrentDomain.GetAssemblies();
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
{
    builder.Services.RegisterComponents(asm.FullName!);
}
// ---

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();
```

# 4. DIの使用
## 4.1. フォルダ構成
```
DiSample                           # ASP.Net Core 
  /Controllers         
    WeatherForecastController.cs   # コントローラ 
  /Services         
    IDiService.cs                  # サービスI/F 
  /Service /Impl         
    DiServiceImpl.cs               # サービス実装 
  /Logics         
    DiLogic.cs                     # ロジック 
  /Daos         
    DiDaoImpl.cs                   # Dao I/F 
  /Daos /Impl         
    DiDaoImpl.cs                   # Dao 実装 
            
  Program.cs                       # プログラムファイル 
```

## 4.2. Controller
・コンストラクタにDIするクラスをパラメータで受け取り、Private変数に格納する
```Csharp:WeatherForecastController.cs
using DiSample.Services;
using Microsoft.AspNetCore.Mvc;
using System.Diagnostics;

namespace DiSample.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class WeatherForecastController : ControllerBase
    {
        private static readonly string[] Summaries = new[]
        {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
        };

        private readonly ILogger<WeatherForecastController> _logger;
        private readonly IDiService _diService;

        public WeatherForecastController(ILogger<WeatherForecastController> logger, IDiService diService)
        {
            _logger = logger;
            _diService = diService;
        }

        [HttpGet(Name = "GetWeatherForecast")]
        public IEnumerable<WeatherForecast> Get()
        {
            // DIしたサービスを実行
            Debug.Print(_diService.Get());

            return Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = DateTime.Now.AddDays(index),
                TemperatureC = Random.Shared.Next(-20, 55),
                Summary = Summaries[Random.Shared.Next(Summaries.Length)]
            })
            .ToArray();
        }
    }
}
```

## 4.3. Services/ServiceImpl
・サービス実装クラスに属性を追加する   
```Csharp:
 [Service(Scope = ComponentScope.Singleton, TargetType = typeof(IDiService))]
```
・サービス実装クラスのコンストラクタにDIするクラスをパラメータで受け取り、Private変数に格納する

```Csharp:IDiService.cs
namespace DiSample.Services
{
    public interface IDiService
    {
        public string Get();
    }
}
```

```Csharp:DiServiceImpl.cs
using DiSample.Logics;
using NX.DependencyInjection;
using System.Diagnostics;

namespace DiSample.Services.Impl
{
    [Service(Scope = ComponentScope.Singleton, TargetType = typeof(IDiService))]
    public class DiServiceImpl : IDiService
    {
        private readonly DiLogic _diLogic;

        // コンストラクタ
        public DiServiceImpl(DiLogic diLogic)
        {
            _diLogic = diLogic;

        }

        public string Get()
        {
            // 実行確認
            Debug.Print("Run DiServiceImpl");

            return _diLogic.Get();
        }
    }
}
```

## 4.4. Logic
・ロジッククラスに属性を追加する   
```Csharp:
 [Logic(Scope = ComponentScope.Singleton, TargetType = typeof(DiLogic))]
```
・ロジッククラスのコンストラクタにDIするクラスをパラメータで受け取り、Private変数に格納する

```Csharp:DiLogic.cs
using DiSample.Daos;
using DiSample.Services;
using NX.DependencyInjection;
using System.Diagnostics;

namespace DiSample.Logics
{

    [Logic(Scope = ComponentScope.Singleton, TargetType = typeof(DiLogic))]
    public class DiLogic
    {
        private readonly IDiDao _diDao;

        // コンストラクタ
        public DiLogic(IDiDao diDao)
        {
            _diDao = diDao;
        }
        public string Get()
        {
            // 実行確認
            Debug.Print("Run DiLogic");

            return _diDao.Get();
        }
    }
}
```

## 4.5. Dao/DaoImpl
・Dao実装クラスに属性を追加する   
```Csharp:
 [Dao(Scope = ComponentScope.Singleton, TargetType = typeof(IDiDao))]
```

```Csharp:IDiDao.cs
using DiSample.Services;

namespace DiSample.Daos
{
    public interface IDiDao
    {
        public string Get();
    }
}
```

```Csharp:DiDaoImpl.cs
using NX.DependencyInjection;
using System.Diagnostics;

namespace DiSample.Daos.Impl
{

    [Dao(Scope = ComponentScope.Singleton, TargetType = typeof(IDiDao))]
    public class DiDaoImpl : IDiDao
    {
        public string Get()
        {
            // 実行確認
            Debug.Print("Run DiDaoImpl");

            // ダミー実装
            return "DaoImpl:Get()";
        }
    }
}
```


```Csharp:
[Logic(Scope = ComponentScope.Singleton, TargetType = typeof(IDiLogic))]
```

```Csharp:
[Dao(Scope = ComponentScope.Singleton, TargetType = typeof(IDiDao))]
```
    
* ScopeはMicrosoft.Extensions.DependencyInjectionの設定と同じ  
    * ComponentScope.Singleton: シングルトンのスコープつまり、インスタンスは１回だけ生成される 
    * ComponentScope.Scoped: １回のリクエストに対して、インスタンスが１回生成される 
    * ComponentScope.Transient: 毎回新しいインスタンスが生成される 
* TargetTypeはサービスのI/Fを設定する

# 5. 実行結果
* SwaggerよりExcecuteボタンをクリックする
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2852081/a2bbd3af-8cd8-e5f6-b049-e73183d8a40e.png)

* デバッグウィンドウでDIしたクラスが実行できたことを確認
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2852081/eb8bc78f-9c2c-7b19-d1bd-1e1123de671c.png)

# 6. ソースコード

https://github.com/YoshijiGates/CSharp/tree/main/DiSample

# 7. 参考文献

https://blog.jbs.co.jp/entry/2022/09/07/120000