Skip to content

Commit 65e3558

Browse files
committed
Feat: Add keyed and non-keyed conditional service registration
Introduces helper methods to conditionally register services if not already present, with support for specific lifetimes (Singleton, Scoped, Transient) and keyed registrations. This enhances flexibility in dependency injection while preventing duplicate service entries. better code organization Introduce `#region` directives to group extension methods logically. This enhances readability by clearly delineating related methods like `AddServiceIfNotExists` and `AddKeyedServiceIfNotExists`.
1 parent c4ecd51 commit 65e3558

File tree

2 files changed

+201
-19
lines changed

2 files changed

+201
-19
lines changed

src/CodeOfChaos.Extensions.DependencyInjection/IServiceCollectionExtensions.cs

Lines changed: 78 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,25 +7,97 @@ namespace Microsoft.Extensions.DependencyInjection;
77
// Code
88
// ---------------------------------------------------------------------------------------------------------------------
99
public static class IServiceCollectionExtensions {
10+
#region AddServiceIfNotExists
1011
public static IServiceCollection AddServiceIfNotExists<TService, TImplementation>(
1112
this IServiceCollection services,
12-
ServiceLifetime lifetime = ServiceLifetime.Scoped)
13+
ServiceLifetime lifetime
14+
)
1315
where TService : class
14-
where TImplementation : class, TService
15-
{
16+
where TImplementation : class, TService {
1617
if (services.FirstOrDefault(x => x.ServiceType == typeof(TService)) is not null) return services;
1718

1819
switch (lifetime) {
19-
case ServiceLifetime.Singleton: services.AddSingleton<TService, TImplementation>();
20+
case ServiceLifetime.Singleton:
21+
services.AddSingleton<TService, TImplementation>();
2022
break;
21-
case ServiceLifetime.Scoped: services.AddScoped<TService, TImplementation>();
23+
case ServiceLifetime.Scoped:
24+
services.AddScoped<TService, TImplementation>();
2225
break;
23-
case ServiceLifetime.Transient: services.AddTransient<TService, TImplementation>();
26+
case ServiceLifetime.Transient:
27+
services.AddTransient<TService, TImplementation>();
2428
break;
2529
default: throw new ArgumentOutOfRangeException(nameof(lifetime), lifetime, null);
2630
}
2731

2832
return services;
2933
}
3034

35+
public static IServiceCollection AddSingletonIfNotExists<TService, TImplementation>(this IServiceCollection services)
36+
where TService : class
37+
where TImplementation : class, TService =>
38+
AddServiceIfNotExists<TService, TImplementation>(services, ServiceLifetime.Singleton);
39+
40+
public static IServiceCollection AddScopedIfNotExists<TService, TImplementation>(this IServiceCollection services)
41+
where TService : class
42+
where TImplementation : class, TService =>
43+
AddServiceIfNotExists<TService, TImplementation>(services, ServiceLifetime.Scoped);
44+
45+
public static IServiceCollection AddTransientIfNotExists<TService, TImplementation>(this IServiceCollection services)
46+
where TService : class
47+
where TImplementation : class, TService =>
48+
AddServiceIfNotExists<TService, TImplementation>(services, ServiceLifetime.Transient);
49+
#endregion
50+
51+
#region AddKeyedServiceIfNotExists
52+
public static IServiceCollection AddKeyedServiceIfNotExists<TService, TImplementation>(
53+
this IServiceCollection services,
54+
object key,
55+
ServiceLifetime lifetime
56+
)
57+
where TService : class
58+
where TImplementation : class, TService {
59+
if (services.FirstOrDefault(x => x.ServiceType == typeof(TService) && x.ServiceKey == key) is not null)
60+
return services;
61+
62+
switch (lifetime) {
63+
case ServiceLifetime.Singleton:
64+
services.AddKeyedSingleton<TService, TImplementation>(key);
65+
break;
66+
case ServiceLifetime.Scoped:
67+
services.AddKeyedScoped<TService, TImplementation>(key);
68+
break;
69+
case ServiceLifetime.Transient:
70+
services.AddKeyedTransient<TService, TImplementation>(key);
71+
break;
72+
default: throw new ArgumentOutOfRangeException(nameof(lifetime), lifetime, null);
73+
}
74+
75+
return services;
76+
}
77+
78+
// Direct keyed lifetime overloads
79+
public static IServiceCollection AddKeyedSingletonIfNotExists<TService, TImplementation>(
80+
this IServiceCollection services,
81+
object key
82+
)
83+
where TService : class
84+
where TImplementation : class, TService =>
85+
AddKeyedServiceIfNotExists<TService, TImplementation>(services, key, ServiceLifetime.Singleton);
86+
87+
public static IServiceCollection AddKeyedScopedIfNotExists<TService, TImplementation>(
88+
this IServiceCollection services,
89+
object key
90+
)
91+
where TService : class
92+
where TImplementation : class, TService =>
93+
AddKeyedServiceIfNotExists<TService, TImplementation>(services, key, ServiceLifetime.Scoped);
94+
95+
public static IServiceCollection AddKeyedTransientIfNotExists<TService, TImplementation>(
96+
this IServiceCollection services,
97+
object key
98+
)
99+
where TService : class
100+
where TImplementation : class, TService =>
101+
AddKeyedServiceIfNotExists<TService, TImplementation>(services, key, ServiceLifetime.Transient);
102+
#endregion
31103
}

tests/Tests.CodeOfChaos.Extensions.DependencyInjection/AddServiceIfNotExistsTests.cs

Lines changed: 123 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ public async Task AddServiceIfNotExists_WhenServiceDoesNotExist_AddsWithDefaultS
1414
var services = new ServiceCollection();
1515

1616
// Act
17-
services.AddServiceIfNotExists<ITestService, TestService>();
17+
services.AddServiceIfNotExists<ITestService, TestService>(ServiceLifetime.Scoped);
1818
ServiceDescriptor? descriptor = services.FirstOrDefault(x => x.ServiceType == typeof(ITestService));
1919

2020
// Assert
@@ -41,32 +41,142 @@ public async Task AddServiceIfNotExists_WithSpecificLifetime_AddsServiceCorrectl
4141
}
4242

4343
[Test]
44-
public async Task AddServiceIfNotExists_WhenServiceExists_DoesNotAddDuplicate() {
44+
public async Task AddSingletonIfNotExists_AddsServiceWithSingletonLifetime() {
4545
// Arrange
4646
var services = new ServiceCollection();
47-
services.AddScoped<ITestService, TestService>();
48-
int initialCount = services.Count;
4947

5048
// Act
51-
services.AddServiceIfNotExists<ITestService, TestService>();
49+
services.AddSingletonIfNotExists<ITestService, TestService>();
50+
ServiceDescriptor? descriptor = services.FirstOrDefault(x => x.ServiceType == typeof(ITestService));
5251

5352
// Assert
54-
await Assert.That(services.Count).IsEqualTo(initialCount);
53+
await Assert.That(descriptor).IsNotNull();
54+
await Assert.That(descriptor!.Lifetime).IsEqualTo(ServiceLifetime.Singleton);
55+
}
56+
57+
[Test]
58+
public async Task AddScopedIfNotExists_AddsServiceWithScopedLifetime() {
59+
// Arrange
60+
var services = new ServiceCollection();
61+
62+
// Act
63+
services.AddScopedIfNotExists<ITestService, TestService>();
64+
ServiceDescriptor? descriptor = services.FirstOrDefault(x => x.ServiceType == typeof(ITestService));
65+
66+
// Assert
67+
await Assert.That(descriptor).IsNotNull();
68+
await Assert.That(descriptor!.Lifetime).IsEqualTo(ServiceLifetime.Scoped);
69+
}
70+
71+
[Test]
72+
public async Task AddTransientIfNotExists_AddsServiceWithTransientLifetime() {
73+
// Arrange
74+
var services = new ServiceCollection();
75+
76+
// Act
77+
services.AddTransientIfNotExists<ITestService, TestService>();
78+
ServiceDescriptor? descriptor = services.FirstOrDefault(x => x.ServiceType == typeof(ITestService));
79+
80+
// Assert
81+
await Assert.That(descriptor).IsNotNull();
82+
await Assert.That(descriptor!.Lifetime).IsEqualTo(ServiceLifetime.Transient);
83+
}
84+
85+
[Test]
86+
public async Task AddKeyedServiceIfNotExists_WhenServiceDoesNotExist_AddsWithDefaultScoped() {
87+
// Arrange
88+
var services = new ServiceCollection();
89+
const string key = "test-key";
90+
91+
// Act
92+
services.AddKeyedServiceIfNotExists<ITestService, TestService>(key, ServiceLifetime.Scoped);
93+
ServiceDescriptor? descriptor = services.FirstOrDefault(x => x.ServiceType == typeof(ITestService) && ReferenceEquals(x.ServiceKey, key));
94+
95+
// Assert
96+
await Assert.That(descriptor).IsNotNull();
97+
await Assert.That(descriptor!.KeyedImplementationType).IsEqualTo(typeof(TestService));
98+
await Assert.That(descriptor.Lifetime).IsEqualTo(ServiceLifetime.Scoped);
99+
}
100+
101+
[Test]
102+
[Arguments(ServiceLifetime.Singleton)]
103+
[Arguments(ServiceLifetime.Scoped)]
104+
[Arguments(ServiceLifetime.Transient)]
105+
public async Task AddKeyedServiceIfNotExists_WithSpecificLifetime_AddsServiceCorrectly(ServiceLifetime lifetime) {
106+
// Arrange
107+
var services = new ServiceCollection();
108+
const string key = "test-key";
109+
110+
// Act
111+
services.AddKeyedServiceIfNotExists<ITestService, TestService>(key, lifetime);
112+
ServiceDescriptor? descriptor = services.FirstOrDefault(x => x.ServiceType == typeof(ITestService) && ReferenceEquals(x.ServiceKey, key));
113+
114+
// Assert
115+
await Assert.That(descriptor).IsNotNull();
116+
await Assert.That(descriptor!.Lifetime).IsEqualTo(lifetime);
117+
}
118+
119+
[Test]
120+
public async Task AddKeyedSingletonIfNotExists_AddsServiceWithSingletonLifetime() {
121+
// Arrange
122+
var services = new ServiceCollection();
123+
string key = "test-key";
124+
125+
// Act
126+
services.AddKeyedSingletonIfNotExists<ITestService, TestService>(key);
127+
ServiceDescriptor? descriptor = services.FirstOrDefault(x => x.ServiceType == typeof(ITestService) && ReferenceEquals(x.ServiceKey, key));
128+
129+
// Assert
130+
await Assert.That(descriptor).IsNotNull();
131+
await Assert.That(descriptor!.Lifetime).IsEqualTo(ServiceLifetime.Singleton);
55132
}
56133

57134
[Test]
58-
public async Task AddServiceIfNotExists_WithInvalidLifetime_ThrowsException() {
135+
public async Task AddKeyedScopedIfNotExists_AddsServiceWithScopedLifetime() {
59136
// Arrange
60137
var services = new ServiceCollection();
138+
const string key = "test-key";
139+
140+
// Act
141+
services.AddKeyedScopedIfNotExists<ITestService, TestService>(key);
142+
ServiceDescriptor? descriptor = services.FirstOrDefault(x => x.ServiceType == typeof(ITestService) && ReferenceEquals(x.ServiceKey, key));
61143

62-
// Act & Assert
63-
await Assert.ThrowsAsync<ArgumentOutOfRangeException>(() => {
64-
services.AddServiceIfNotExists<ITestService, TestService>((ServiceLifetime)999);
65-
return Task.CompletedTask;
66-
});
144+
// Assert
145+
await Assert.That(descriptor).IsNotNull();
146+
await Assert.That(descriptor!.Lifetime).IsEqualTo(ServiceLifetime.Scoped);
147+
}
148+
149+
[Test]
150+
public async Task AddKeyedTransientIfNotExists_AddsServiceWithTransientLifetime() {
151+
// Arrange
152+
var services = new ServiceCollection();
153+
const string key = "test-key";
154+
155+
// Act
156+
services.AddKeyedTransientIfNotExists<ITestService, TestService>(key);
157+
ServiceDescriptor? descriptor = services.FirstOrDefault(x => x.ServiceType == typeof(ITestService) && ReferenceEquals(x.ServiceKey, key));
158+
159+
// Assert
160+
await Assert.That(descriptor).IsNotNull();
161+
await Assert.That(descriptor!.Lifetime).IsEqualTo(ServiceLifetime.Transient);
162+
}
163+
164+
[Test]
165+
public async Task AddKeyedServiceIfNotExists_WhenServiceExists_DoesNotAddDuplicate() {
166+
// Arrange
167+
var services = new ServiceCollection();
168+
const string key = "test-key";
169+
services.AddKeyedScoped<ITestService, TestService>(key);
170+
int initialCount = services.Count;
171+
172+
// Act
173+
services.AddKeyedServiceIfNotExists<ITestService, TestService>(key, ServiceLifetime.Singleton);
174+
175+
// Assert
176+
await Assert.That(services.Count).IsEqualTo(initialCount);
67177
}
68178

69179
// Test interfaces and classes
70180
private interface ITestService { }
71181
private class TestService : ITestService { }
72-
}
182+
}

0 commit comments

Comments
 (0)