diff --git a/src/.dockerignore b/src/.dockerignore
new file mode 100644
index 0000000..fe1152b
--- /dev/null
+++ b/src/.dockerignore
@@ -0,0 +1,30 @@
+**/.classpath
+**/.dockerignore
+**/.env
+**/.git
+**/.gitignore
+**/.project
+**/.settings
+**/.toolstarget
+**/.vs
+**/.vscode
+**/*.*proj.user
+**/*.dbmdl
+**/*.jfm
+**/azds.yaml
+**/bin
+**/charts
+**/docker-compose*
+**/Dockerfile*
+**/node_modules
+**/npm-debug.log
+**/obj
+**/secrets.dev.yaml
+**/values.dev.yaml
+LICENSE
+README.md
+!**/.gitignore
+!.git/HEAD
+!.git/config
+!.git/packed-refs
+!.git/refs/heads/**
\ No newline at end of file
diff --git a/src/ApiTest/ApiTest.csproj b/src/ApiTest/ApiTest.csproj
new file mode 100644
index 0000000..9191e8d
--- /dev/null
+++ b/src/ApiTest/ApiTest.csproj
@@ -0,0 +1,20 @@
+
+
+
+ net8.0
+ enable
+ enable
+ 9a4c2a0c-d9b0-47d9-8bf6-156509435f06
+ Linux
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/ApiTest/ApiTest.http b/src/ApiTest/ApiTest.http
new file mode 100644
index 0000000..ba81544
--- /dev/null
+++ b/src/ApiTest/ApiTest.http
@@ -0,0 +1,6 @@
+@ApiTest_HostAddress = http://localhost:5025
+
+GET {{ApiTest_HostAddress}}/weatherforecast/
+Accept: application/json
+
+###
diff --git a/src/ApiTest/Controllers/WeatherForecastController.cs b/src/ApiTest/Controllers/WeatherForecastController.cs
new file mode 100644
index 0000000..8151157
--- /dev/null
+++ b/src/ApiTest/Controllers/WeatherForecastController.cs
@@ -0,0 +1,37 @@
+using ApiTest.Services;
+using Microsoft.AspNetCore.Mvc;
+
+namespace ApiTest.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 _logger;
+ private readonly IParentClassTest _parentTest;
+
+ public WeatherForecastController(ILogger logger, IParentClassTest parentClassTest)
+ {
+ _logger = logger;
+ _parentTest = parentClassTest;
+ }
+
+ [HttpGet(Name = "GetWeatherForecast")]
+ public IEnumerable Get()
+ {
+ _parentTest.PrintSomething();
+ return Enumerable.Range(1, 5).Select(index => new WeatherForecast
+ {
+ Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
+ TemperatureC = Random.Shared.Next(-20, 55),
+ Summary = Summaries[Random.Shared.Next(Summaries.Length)]
+ })
+ .ToArray();
+ }
+ }
+}
diff --git a/src/ApiTest/Dockerfile b/src/ApiTest/Dockerfile
new file mode 100644
index 0000000..eaff08e
--- /dev/null
+++ b/src/ApiTest/Dockerfile
@@ -0,0 +1,30 @@
+# See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.
+
+# This stage is used when running from VS in fast mode (Default for Debug configuration)
+FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
+USER app
+WORKDIR /app
+EXPOSE 8080
+EXPOSE 8081
+
+
+# This stage is used to build the service project
+FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
+ARG BUILD_CONFIGURATION=Release
+WORKDIR /src
+COPY ["ApiTest/ApiTest.csproj", "ApiTest/"]
+RUN dotnet restore "./ApiTest/ApiTest.csproj"
+COPY . .
+WORKDIR "/src/ApiTest"
+RUN dotnet build "./ApiTest.csproj" -c $BUILD_CONFIGURATION -o /app/build
+
+# This stage is used to publish the service project to be copied to the final stage
+FROM build AS publish
+ARG BUILD_CONFIGURATION=Release
+RUN dotnet publish "./ApiTest.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
+
+# This stage is used in production or when running from VS in regular mode (Default when not using the Debug configuration)
+FROM base AS final
+WORKDIR /app
+COPY --from=publish /app/publish .
+ENTRYPOINT ["dotnet", "ApiTest.dll"]
\ No newline at end of file
diff --git a/src/ApiTest/Program.cs b/src/ApiTest/Program.cs
new file mode 100644
index 0000000..92c8924
--- /dev/null
+++ b/src/ApiTest/Program.cs
@@ -0,0 +1,33 @@
+using ApiTest.Services;
+using Samhammer.DependencyInjection;
+
+var builder = WebApplication.CreateBuilder(args);
+// Add services to the container.
+
+builder.Services.AddControllers();
+// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
+builder.Services.AddEndpointsApiExplorer();
+builder.Services.AddSwaggerGen();
+
+builder.Host.ConfigureServices(services => services.ResolveDependencies());
+builder.Host.UseDefaultServiceProvider(options =>
+{
+ options.ValidateOnBuild = true;
+});
+
+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();
diff --git a/src/ApiTest/Services/ChildClassTest.cs b/src/ApiTest/Services/ChildClassTest.cs
new file mode 100644
index 0000000..b56a45c
--- /dev/null
+++ b/src/ApiTest/Services/ChildClassTest.cs
@@ -0,0 +1,10 @@
+using Samhammer.DependencyInjection.Attributes;
+
+namespace ApiTest.Services
+{
+ public class ChildClassTest : IChildClassTest
+ {
+ }
+
+ public interface IChildClassTest { }
+}
diff --git a/src/ApiTest/Services/ParentClassTest.cs b/src/ApiTest/Services/ParentClassTest.cs
new file mode 100644
index 0000000..80534d4
--- /dev/null
+++ b/src/ApiTest/Services/ParentClassTest.cs
@@ -0,0 +1,23 @@
+using Samhammer.DependencyInjection.Attributes;
+
+namespace ApiTest.Services
+{
+ [InjectAb]
+ public class ParentClassTest : IParentClassTest
+ {
+ public ParentClassTest(IChildClassTest childClassTest)
+ {
+ Console.WriteLine("Creating test class");
+ }
+
+ public void PrintSomething()
+ {
+ Console.WriteLine("Printing in test class");
+ }
+ }
+
+ public interface IParentClassTest
+ {
+ public void PrintSomething();
+ }
+}
diff --git a/src/ApiTest/WeatherForecast.cs b/src/ApiTest/WeatherForecast.cs
new file mode 100644
index 0000000..293ad0b
--- /dev/null
+++ b/src/ApiTest/WeatherForecast.cs
@@ -0,0 +1,13 @@
+namespace ApiTest
+{
+ public class WeatherForecast
+ {
+ public DateOnly Date { get; set; }
+
+ public int TemperatureC { get; set; }
+
+ public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
+
+ public string? Summary { get; set; }
+ }
+}
diff --git a/src/ApiTest/appsettings.Development.json b/src/ApiTest/appsettings.Development.json
new file mode 100644
index 0000000..0c208ae
--- /dev/null
+++ b/src/ApiTest/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/src/ApiTest/appsettings.json b/src/ApiTest/appsettings.json
new file mode 100644
index 0000000..738f12a
--- /dev/null
+++ b/src/ApiTest/appsettings.json
@@ -0,0 +1,10 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*",
+ "EnabledTest": true
+}
diff --git a/src/Samhammer.DependencyInjection.Test/DependencyResolverTest.cs b/src/Samhammer.DependencyInjection.Test/DependencyResolverTest.cs
index 97213fc..f128b66 100644
--- a/src/Samhammer.DependencyInjection.Test/DependencyResolverTest.cs
+++ b/src/Samhammer.DependencyInjection.Test/DependencyResolverTest.cs
@@ -23,6 +23,24 @@ public DependencyResolverTest()
serviceCollection.AddLogging(builder => builder.ClearProviders());
}
+ [Fact]
+ private void GetClassContains_WithDefaultLifetime_Scoped()
+ {
+ // act
+ serviceCollection.ResolveDependencies();
+ serviceProvider = serviceCollection.BuildServiceProvider();
+
+ IClassContainsDefaultLifetime service;
+
+ using (var scope = serviceProvider.CreateScope())
+ {
+ service = scope.ServiceProvider.GetService();
+ }
+
+ // assert
+ service.Should().NotBeNull().And.BeOfType();
+ }
+
[Fact]
private void GetClass_WithDefaultLifetime_Scoped()
{
diff --git a/src/Samhammer.DependencyInjection.Test/TestData/InjectedClass/ClassContainsDefaultLifetime.cs b/src/Samhammer.DependencyInjection.Test/TestData/InjectedClass/ClassContainsDefaultLifetime.cs
new file mode 100644
index 0000000..ae6824a
--- /dev/null
+++ b/src/Samhammer.DependencyInjection.Test/TestData/InjectedClass/ClassContainsDefaultLifetime.cs
@@ -0,0 +1,22 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Samhammer.DependencyInjection.Attributes;
+
+namespace Samhammer.DependencyInjection.Test.TestData.InjectedClass
+{
+ [Inject]
+ public class ClassContainsDefaultLifetime : IClassContainsDefaultLifetime
+ {
+ public ClassContainsDefaultLifetime(IClassDefaultLifetime classDefault)
+ {
+ Console.WriteLine($"Creating service {classDefault.ToString()}");
+ }
+ }
+
+ public interface IClassContainsDefaultLifetime
+ {
+ }
+}
diff --git a/src/Samhammer.DependencyInjection.Test/TestData/InjectedClass/ClassDefaultLifetime.cs b/src/Samhammer.DependencyInjection.Test/TestData/InjectedClass/ClassDefaultLifetime.cs
index 5fa4d65..9d4a139 100644
--- a/src/Samhammer.DependencyInjection.Test/TestData/InjectedClass/ClassDefaultLifetime.cs
+++ b/src/Samhammer.DependencyInjection.Test/TestData/InjectedClass/ClassDefaultLifetime.cs
@@ -1,10 +1,15 @@
-using Samhammer.DependencyInjection.Attributes;
+using System;
+using Samhammer.DependencyInjection.Attributes;
namespace Samhammer.DependencyInjection.Test.TestData.InjectedClass
{
[Inject]
public class ClassDefaultLifetime : IClassDefaultLifetime
{
+ public ClassDefaultLifetime()
+ {
+ Console.WriteLine("Creating service");
+ }
}
public interface IClassDefaultLifetime
diff --git a/src/Samhammer.DependencyInjection.sln b/src/Samhammer.DependencyInjection.sln
index 0bf8098..695a436 100644
--- a/src/Samhammer.DependencyInjection.sln
+++ b/src/Samhammer.DependencyInjection.sln
@@ -12,9 +12,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
..\README.md = ..\README.md
EndProjectSection
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Samhammer.DependencyInjection.Override", "Samhammer.DependencyInjection.Override\Samhammer.DependencyInjection.Override.csproj", "{3229D5A2-838F-433A-BC7D-32D76A08CCD2}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Samhammer.DependencyInjection.Override", "Samhammer.DependencyInjection.Override\Samhammer.DependencyInjection.Override.csproj", "{3229D5A2-838F-433A-BC7D-32D76A08CCD2}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Samhammer.DependencyInjection.Override.Test", "Samhammer.DependencyInjection.Override.Test\Samhammer.DependencyInjection.Override.Test.csproj", "{E5197D26-077D-46AD-92BE-32C3DB192C44}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Samhammer.DependencyInjection.Override.Test", "Samhammer.DependencyInjection.Override.Test\Samhammer.DependencyInjection.Override.Test.csproj", "{E5197D26-077D-46AD-92BE-32C3DB192C44}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ApiTest", "ApiTest\ApiTest.csproj", "{6A652F59-57F5-4CA3-B552-4A57EC9A42F9}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -38,6 +40,10 @@ Global
{E5197D26-077D-46AD-92BE-32C3DB192C44}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E5197D26-077D-46AD-92BE-32C3DB192C44}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E5197D26-077D-46AD-92BE-32C3DB192C44}.Release|Any CPU.Build.0 = Release|Any CPU
+ {6A652F59-57F5-4CA3-B552-4A57EC9A42F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {6A652F59-57F5-4CA3-B552-4A57EC9A42F9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6A652F59-57F5-4CA3-B552-4A57EC9A42F9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {6A652F59-57F5-4CA3-B552-4A57EC9A42F9}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/src/Samhammer.DependencyInjection/Attributes/InjectAbAttribute.cs b/src/Samhammer.DependencyInjection/Attributes/InjectAbAttribute.cs
new file mode 100644
index 0000000..b19106d
--- /dev/null
+++ b/src/Samhammer.DependencyInjection/Attributes/InjectAbAttribute.cs
@@ -0,0 +1,22 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.Extensions.DependencyInjection;
+using Samhammer.DependencyInjection.Attributes;
+
+namespace Samhammer.DependencyInjection.Attributes
+{
+ [AttributeUsage(AttributeTargets.Class)]
+ public class InjectAbAttribute : DependencyInjectionAttribute
+ {
+ public Target Target { get; }
+
+ public InjectAbAttribute(Target target = Target.Matching, ServiceLifetime lifetime = ServiceLifetime.Scoped)
+ : base(lifetime)
+ {
+ Target = target;
+ }
+ }
+}
diff --git a/src/Samhammer.DependencyInjection/Attributes/InjectAbWithFlagAttribute.cs b/src/Samhammer.DependencyInjection/Attributes/InjectAbWithFlagAttribute.cs
new file mode 100644
index 0000000..7d34d5f
--- /dev/null
+++ b/src/Samhammer.DependencyInjection/Attributes/InjectAbWithFlagAttribute.cs
@@ -0,0 +1,25 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.Extensions.DependencyInjection;
+using Samhammer.DependencyInjection.Attributes;
+
+namespace Samhammer.DependencyInjection.Attributes
+{
+ [AttributeUsage(AttributeTargets.Class)]
+ public class InjectAbWithConfigsAttribute : DependencyInjectionAttribute
+ {
+ public Target Target { get; }
+
+ public string ConfigSections { get; set; }
+
+ public InjectAbWithConfigsAttribute(string configSections, Target target = Target.Matching, ServiceLifetime lifetime = ServiceLifetime.Scoped)
+ : base(lifetime)
+ {
+ Target = target;
+ ConfigSections = configSections;
+ }
+ }
+}
diff --git a/src/Samhammer.DependencyInjection/Handlers/InjectAbMatchingServiceDescriptorHandler.cs b/src/Samhammer.DependencyInjection/Handlers/InjectAbMatchingServiceDescriptorHandler.cs
new file mode 100644
index 0000000..39a8725
--- /dev/null
+++ b/src/Samhammer.DependencyInjection/Handlers/InjectAbMatchingServiceDescriptorHandler.cs
@@ -0,0 +1,49 @@
+using System;
+using System.Collections.Generic;
+using System.Reflection;
+using Microsoft.Extensions.DependencyInjection;
+using Samhammer.DependencyInjection;
+using Samhammer.DependencyInjection.Attributes;
+using Samhammer.DependencyInjection.Handlers;
+
+namespace Samhammer.DependencyInjection.Handlers
+{
+ public class InjectAbMatchingServiceDescriptorHandler : AttributeServiceDescriptorHandler
+ {
+ private DependencyResolverOptions Options { get; }
+
+ public InjectAbMatchingServiceDescriptorHandler(DependencyResolverOptions options)
+ {
+ Options = options;
+ }
+
+ public override bool MatchAdditionalCriteria(InjectAbAttribute attribute)
+ {
+ return attribute.Target == Target.Matching;
+ }
+
+ public override IEnumerable ResolveServices(Type implementationType, InjectAbAttribute injectAttribute)
+ {
+ var serviceType = GetMatchingInterfaceType(implementationType, injectAttribute);
+ var serviceDescriptor = new ServiceDescriptor(serviceType, provider =>
+ {
+ var instance = ActivatorUtilities.CreateInstance(provider, implementationType);
+ return instance;
+ }, injectAttribute.LifeTime);
+ yield return serviceDescriptor;
+ }
+
+ private Type GetMatchingInterfaceType(Type implementationType, InjectAbAttribute injectAttribute)
+ {
+ var matchingInterfaceName = $"I{implementationType.GetTypeInfo().Name}";
+ var serviceType = implementationType.GetInterface(matchingInterfaceName);
+
+ if (serviceType == null)
+ {
+ throw new ArgumentException($"Class {implementationType} has no matching interface {matchingInterfaceName} defined", nameof(implementationType));
+ }
+
+ return serviceType;
+ }
+ }
+}
diff --git a/src/Samhammer.DependencyInjection/Handlers/InjectAbValidateConfigsMatchingServiceDescriptorHandler.cs b/src/Samhammer.DependencyInjection/Handlers/InjectAbValidateConfigsMatchingServiceDescriptorHandler.cs
new file mode 100644
index 0000000..8e6f9b2
--- /dev/null
+++ b/src/Samhammer.DependencyInjection/Handlers/InjectAbValidateConfigsMatchingServiceDescriptorHandler.cs
@@ -0,0 +1,60 @@
+using System;
+using System.Collections.Generic;
+using System.Reflection;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Samhammer.DependencyInjection;
+using Samhammer.DependencyInjection.Attributes;
+using Samhammer.DependencyInjection.Handlers;
+
+namespace Samhammer.DependencyInjection.Handlers
+{
+ public class InjectAbValidateConfigsMatchingServiceDescriptorHandler : AttributeServiceDescriptorHandler
+ {
+ private DependencyResolverOptions Options { get; }
+
+ public InjectAbValidateConfigsMatchingServiceDescriptorHandler(DependencyResolverOptions options)
+ {
+ Options = options;
+ }
+
+ public override bool MatchAdditionalCriteria(InjectAbWithConfigsAttribute attribute)
+ {
+ return attribute.Target == Target.Matching;
+ }
+
+ public override IEnumerable ResolveServices(Type implementationType, InjectAbWithConfigsAttribute injectAttribute)
+ {
+ var serviceType = GetMatchingInterfaceType(implementationType, injectAttribute);
+ var serviceDescriptor = new ServiceDescriptor(serviceType, provider =>
+ {
+ var configuration = provider.GetService();
+ var requiredConfigs = injectAttribute.ConfigSections.Split(',');
+ foreach (var requiredConfig in requiredConfigs)
+ {
+ if (string.IsNullOrWhiteSpace(configuration[requiredConfig]))
+ {
+ throw new InvalidOperationException($"Please setup config section {requiredConfig} to enable this service");
+ }
+ }
+
+ var instance = ActivatorUtilities.CreateInstance(provider, implementationType);
+ return instance;
+ }, injectAttribute.LifeTime);
+ yield return serviceDescriptor;
+ }
+
+ private Type GetMatchingInterfaceType(Type implementationType, InjectAbWithConfigsAttribute injectAttribute)
+ {
+ var matchingInterfaceName = $"I{implementationType.GetTypeInfo().Name}";
+ var serviceType = implementationType.GetInterface(matchingInterfaceName);
+
+ if (serviceType == null)
+ {
+ throw new ArgumentException($"Class {implementationType} has no matching interface {matchingInterfaceName} defined", nameof(implementationType));
+ }
+
+ return serviceType;
+ }
+ }
+}
diff --git a/src/Samhammer.DependencyInjection/Samhammer.DependencyInjection.csproj b/src/Samhammer.DependencyInjection/Samhammer.DependencyInjection.csproj
index 7b4df4a..ba31d1f 100644
--- a/src/Samhammer.DependencyInjection/Samhammer.DependencyInjection.csproj
+++ b/src/Samhammer.DependencyInjection/Samhammer.DependencyInjection.csproj
@@ -17,6 +17,7 @@
+
diff --git a/src/Samhammer.DependencyInjection/ServiceCollectionExtensions.cs b/src/Samhammer.DependencyInjection/ServiceCollectionExtensions.cs
index c72209c..16e3275 100644
--- a/src/Samhammer.DependencyInjection/ServiceCollectionExtensions.cs
+++ b/src/Samhammer.DependencyInjection/ServiceCollectionExtensions.cs
@@ -44,6 +44,8 @@ private static DependencyResolverOptions BuildDefaultOptions()
options.AddAttributeHandler(logger => new InjectAllServiceDescriptorHandler());
options.AddAttributeHandler(logger => new InjectAsServiceDescriptorHandler());
options.AddAttributeHandler(logger => new InjectMatchingServiceDescriptorHandler(options));
+ options.AddAttributeHandler(logger => new InjectAbMatchingServiceDescriptorHandler(options));
+ options.AddAttributeHandler(logger => new InjectAbValidateConfigsMatchingServiceDescriptorHandler(options));
options.AddProvider((logger, o) => new AttributeServiceDescriptorProvider(logger, o));
return options;