diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 2ca02e4..3f2b098 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -26,6 +26,8 @@ + + diff --git a/src/Everything2Everything.Core/Everything2Everything.Core.csproj b/src/Everything2Everything.Core/Everything2Everything.Core.csproj index 8d815f3..6a28fb8 100644 --- a/src/Everything2Everything.Core/Everything2Everything.Core.csproj +++ b/src/Everything2Everything.Core/Everything2Everything.Core.csproj @@ -26,6 +26,8 @@ + + diff --git a/src/Everything2Everything.Core/Everything2EverythingBootstrap.cs b/src/Everything2Everything.Core/Everything2EverythingBootstrap.cs index b13cff2..65c8ef6 100644 --- a/src/Everything2Everything.Core/Everything2EverythingBootstrap.cs +++ b/src/Everything2Everything.Core/Everything2EverythingBootstrap.cs @@ -1,31 +1,19 @@ -using Everything2Everything.Core.Providers; +using Microsoft.Extensions.DependencyInjection; namespace Everything2Everything.Core; public static class Everything2EverythingBootstrap { + /// + /// 기본 변환 엔진을 구성한다. 내부적으로 DI 컨테이너()로 + /// 모든 Provider를 어셈블리 스캔 자동 등록하고 ConversionEngine을 해소한다. + /// 기존 호출부(App/CLI/테스트) 하위호환을 위한 얇은 파사드 — 시그니처·동작 불변. + /// public static ConversionEngine CreateDefault(ISettingsStore? settings = null) { - settings ??= new DpapiSettingsStore(); - var magick = new Converters.MagickProvider(); - var pdf = new Converters.PdfProvider(); - var providers = new IConverterProvider[] - { - magick, - new Converters.HeicProvider(magick), - pdf, - new Converters.PdfToolProvider(), - new Converters.DocxProvider(pdf), - new Converters.HtmlProvider(), - new Converters.HwpxProvider(), - new Converters.OcrProvider(pdf), - new Converters.DocumentProvider(), - new Converters.DataProvider(), - new Converters.VectorProvider(), - new Converters.ImageOptimProvider(), - new Converters.LlmProvider(settings), - new Converters.FfmpegProvider(), - }; - return new ConversionEngine(new ProviderRegistry(providers)); + var services = new ServiceCollection(); + services.AddEverything2Everything(settings); + var provider = services.BuildServiceProvider(); + return provider.GetRequiredService(); } } diff --git a/src/Everything2Everything.Core/ServiceCollectionExtensions.cs b/src/Everything2Everything.Core/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..f5b60f8 --- /dev/null +++ b/src/Everything2Everything.Core/ServiceCollectionExtensions.cs @@ -0,0 +1,41 @@ +using Everything2Everything.Core.Providers; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Everything2Everything.Core; + +/// +/// 변환 엔진의 DI 컴포지션 루트. 모든 IConverterProvider 구현을 어셈블리 스캔(Scrutor)으로 자동 등록해 +/// 컴파일타임 하드코딩(Bootstrap의 14개 new + 수동 생성자 와이어링)을 제거한다. +/// 새 Provider는 클래스만 추가하면 자동 등록되고, 그래프가 그 변환 엣지를 자동 흡수한다(확장성 토대). +/// +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddEverything2Everything( + this IServiceCollection services, + ISettingsStore? settings = null) + { + // 설정 저장소: 호출자가 공유 인스턴스(App의 DPAPI 저장소)를 주면 그걸 등록 — 키 변경이 LlmProvider에 즉시 반영. + // 없으면 기본 DPAPI 저장소를 단 1회 등록. + if (settings is not null) + services.AddSingleton(settings); + else + services.TryAddSingleton(); + + // IConverterProvider 구현 전수 자동 등록. AsSelfWithInterfaces = + // 구체 타입을 단일 싱글턴으로 등록 + IConverterProvider는 그 인스턴스로 포워드한다. + // → Heic(MagickProvider)/Docx·Ocr·Hwpx(PdfProvider)/Llm(ISettingsStore) 생성자 의존이 + // 동일 싱글턴 인스턴스로 자동 해소된다(다중 생성자는 MS.DI가 해소 가능한 최다 매개변수 생성자 선택). + services.Scan(scan => scan + .FromAssembliesOf(typeof(IConverterProvider)) + .AddClasses(c => c.AssignableTo(), publicOnly: false) + .AsSelfWithInterfaces() + .WithSingletonLifetime()); + + // 그래프 빌더: 등록된 모든 IConverterProvider로부터 변환 그래프를 합성. + services.AddSingleton(sp => new ProviderRegistry(sp.GetServices())); + services.AddSingleton(); + + return services; + } +} diff --git a/src/Everything2Everything.Tests/DependencyInjectionTests.cs b/src/Everything2Everything.Tests/DependencyInjectionTests.cs new file mode 100644 index 0000000..a0969f5 --- /dev/null +++ b/src/Everything2Everything.Tests/DependencyInjectionTests.cs @@ -0,0 +1,71 @@ +using System.Linq; +using Everything2Everything.Core; +using Everything2Everything.Core.Converters; +using Everything2Everything.Core.Providers; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Everything2Everything.Tests; + +/// +/// DI 컴포지션 루트(AddEverything2Everything) 등록 규약 검증. Scrutor 자동 등록이 하드코딩 14개와 +/// 동일하게 모든 Provider를 해소하고, 생성자 의존(Magick/Pdf/Settings)이 자동 와이어링되는지 보증한다. +/// 적대적 리뷰 지적: AsSelf 누락 시 Heic/Docx/Ocr 의존 해소가 silent 실패 → 일부 Provider 런타임 누락. +/// +public class DependencyInjectionTests +{ + [Fact] + public void AddEverything2Everything_RegistersAll14Providers() + { + var services = new ServiceCollection(); + services.AddEverything2Everything(); + using var sp = services.BuildServiceProvider(); + + var providers = sp.GetServices().ToList(); + Assert.Equal(14, providers.Count); // 하드코딩 14개와 동일 — Scrutor 누락/초과 방어 + + Assert.NotNull(sp.GetRequiredService()); + Assert.NotNull(sp.GetRequiredService()); + } + + [Fact] + public void ConstructorDependencies_AreAutoWired() + { + var services = new ServiceCollection(); + services.AddEverything2Everything(); + using var sp = services.BuildServiceProvider(); + + // 이들이 해소되면 생성자 의존이 DI로 연결된 것(AsSelf 미등록 시 여기서 실패). + Assert.NotNull(sp.GetRequiredService()); // ← MagickProvider 주입 + Assert.NotNull(sp.GetRequiredService()); // ← PdfProvider 주입 + Assert.NotNull(sp.GetRequiredService()); // ← PdfProvider 주입 + Assert.NotNull(sp.GetRequiredService()); // ← PdfProvider 주입 + Assert.NotNull(sp.GetRequiredService()); // ← ISettingsStore 주입 + + // AsSelfWithInterfaces: 구체 타입과 인터페이스가 동일 싱글턴 인스턴스를 공유. + var magickAsSelf = sp.GetRequiredService(); + var magickAsInterface = sp.GetServices().OfType().Single(); + Assert.Same(magickAsSelf, magickAsInterface); + } + + [Fact] + public void SharedSettings_AreRegisteredInstance() + { + // App이 공유 ISettingsStore를 주입하면 그 인스턴스가 등록되어야 한다(LlmProvider 키 공유). + var settings = new DpapiSettingsStore(); + var services = new ServiceCollection(); + services.AddEverything2Everything(settings); + using var sp = services.BuildServiceProvider(); + Assert.Same(settings, sp.GetRequiredService()); + } + + [Fact] + public void CreateDefault_Facade_StillWorks() + { + // 파사드 하위호환: DI 내부전환 후에도 CreateDefault가 동일하게 엔진/그래프를 구성. + var engine = Everything2EverythingBootstrap.CreateDefault(); + Assert.Equal(14, engine.Providers.All.Count); + Assert.NotNull(engine.Providers.Graph.FindBestPath(".png", ".jpg")); + Assert.NotNull(engine.Providers.Graph.FindBestPath(".svg", ".jpg", maxHops: 3)); + } +}