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の実装
こちらの記事に掲載しているソースを使用させてもらいました。
3.1. 属性の作成
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. リフレクションの実装
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. 依存性注入機能への登録
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を自動登録するように記述を追加
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変数に格納する
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
・サービス実装クラスに属性を追加する
[Service(Scope = ComponentScope.Singleton, TargetType = typeof(IDiService))]
・サービス実装クラスのコンストラクタにDIするクラスをパラメータで受け取り、Private変数に格納する
IDiService.cs
namespace DiSample.Services
{
public interface IDiService
{
public string Get();
}
}
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
・ロジッククラスに属性を追加する
[Logic(Scope = ComponentScope.Singleton, TargetType = typeof(DiLogic))]
・ロジッククラスのコンストラクタにDIするクラスをパラメータで受け取り、Private変数に格納する
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実装クラスに属性を追加する
[Dao(Scope = ComponentScope.Singleton, TargetType = typeof(IDiDao))]
IDiDao.cs
using DiSample.Services;
namespace DiSample.Daos
{
public interface IDiDao
{
public string Get();
}
}
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()";
}
}
}
[Logic(Scope = ComponentScope.Singleton, TargetType = typeof(IDiLogic))]
[Dao(Scope = ComponentScope.Singleton, TargetType = typeof(IDiDao))]
- ScopeはMicrosoft.Extensions.DependencyInjectionの設定と同じ
- ComponentScope.Singleton: シングルトンのスコープつまり、インスタンスは1回だけ生成される
- ComponentScope.Scoped: 1回のリクエストに対して、インスタンスが1回生成される
- ComponentScope.Transient: 毎回新しいインスタンスが生成される
- TargetTypeはサービスのI/Fを設定する
5. 実行結果
6. ソースコード
7. 参考文献