8
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2023-05-25

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. 実行結果

  • SwaggerよりExcecuteボタンをクリックする
    image.png

  • デバッグウィンドウでDIしたクラスが実行できたことを確認
    image.png

6. ソースコード

7. 参考文献

8
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?