admin管理员组文章数量:1134576
I started implementing something like "strict" mode for IServiceCollection
which will throw an Exception
(or even AggregateException
if possible) when an AddXXX
extension method would overwrite an already registered dependency. Main motivation is to be sure that with all the AddXXX
methods in my codebase which others contribute to, some depedency doesn't get accidentally overwritten.
What I have so far is an extension method AsStrict()
on IServiceCollection
which returns an instance of a custom IStrictServiceCollection
interface (extending IServiceCollection
for chaining) which has its only method AsNormal()
. The purpose of the method is to turn off the validation logic. It uses the captured IServiceCollection
instance to forward to for the ICollection
methods with the exception of Add
where I base the exception logic on TryAddEnumerable
with a couple of additional if
clauses in the body of the loop:
int count = services.Count;
for (int i = 0; i < count; i++) {
ServiceDescriptor service = services[i];
if (service.ServiceType == descriptor.ServiceType &&
service.GetImplementationType() == implementationType &&
object.Equals(service.ServiceKey, descriptor.ServiceKey)) {
// Already added
// no return but
throw new InvalidOperationException(...)
}
}
services.Add(descriptor);
The use would be:
services.AddX()
.AsStrict()
.AddY()
.AddZ()
.AsNormal()
.AddXXX
so I could capture exceptions from part of the service registration chain or the whole. I've already come across some "normal" edge cases that made me post this question in the hopes that somebody has encountered most/if not all and has a more robust implementation.
I started implementing something like "strict" mode for IServiceCollection
which will throw an Exception
(or even AggregateException
if possible) when an AddXXX
extension method would overwrite an already registered dependency. Main motivation is to be sure that with all the AddXXX
methods in my codebase which others contribute to, some depedency doesn't get accidentally overwritten.
What I have so far is an extension method AsStrict()
on IServiceCollection
which returns an instance of a custom IStrictServiceCollection
interface (extending IServiceCollection
for chaining) which has its only method AsNormal()
. The purpose of the method is to turn off the validation logic. It uses the captured IServiceCollection
instance to forward to for the ICollection
methods with the exception of Add
where I base the exception logic on TryAddEnumerable
with a couple of additional if
clauses in the body of the loop:
int count = services.Count;
for (int i = 0; i < count; i++) {
ServiceDescriptor service = services[i];
if (service.ServiceType == descriptor.ServiceType &&
service.GetImplementationType() == implementationType &&
object.Equals(service.ServiceKey, descriptor.ServiceKey)) {
// Already added
// no return but
throw new InvalidOperationException(...)
}
}
services.Add(descriptor);
The use would be:
services.AddX()
.AsStrict()
.AddY()
.AddZ()
.AsNormal()
.AddXXX
so I could capture exceptions from part of the service registration chain or the whole. I've already come across some "normal" edge cases that made me post this question in the hopes that somebody has encountered most/if not all and has a more robust implementation.
Share Improve this question edited Jan 8 at 11:50 Ivan Petrov asked Jan 7 at 21:10 Ivan PetrovIvan Petrov 3,9162 gold badges11 silver badges23 bronze badges 11 | Show 6 more comments2 Answers
Reset to default 1Not so quite sure what your definition of invalid registration is (so I left it out).
Here is some executable code sample (Visual Studio 2022 .NET8 Web API template).
Feel free to edit the answer to provide the some reproducable error scenario.
using System.Collections;
namespace WebApplication1
{
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
var normal = builder.Services;
var strict = builder.Services.AsStrict();
normal.AddControllers();
normal.AddEndpointsApiExplorer();
normal.AddSwaggerGen();
normal.AddX().AddY().AddZ();
// so far only fails on strict.AddZ()
strict.AddX().AddY().AddZ();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseAuthorization();
app.MapControllers();
app.Run();
}
}
public class ServiceX { }
public class ServiceY { }
public class ServiceZ { }
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddX(this IServiceCollection services)
{
services.AddSingleton<ServiceX>();
return services;
}
public static IServiceCollection AddY(this IServiceCollection services)
{
services.AddScoped<ServiceY>();
return services;
}
public static IServiceCollection AddZ(this IServiceCollection services)
{
services.AddTransient<ServiceZ>();
return services;
}
public static IServiceCollection AddXXX(this IServiceCollection services)
{
services.AddSingleton<ServiceX>();
services.AddScoped<ServiceY>();
services.AddTransient<ServiceZ>();
return services;
}
}
public static class StrictServiceCollectionExtensions
{
public static IStrictServiceCollection AsStrict(this IServiceCollection services)
{
if (services == null) throw new ArgumentNullException();
if (services is IStrictServiceCollection) return (IStrictServiceCollection)services;
return new StrictServiceCollection(services);
}
public static IServiceCollection AsNormal(this IServiceCollection services)
{
if (services == null) throw new ArgumentNullException();
if (services is not IStrictServiceCollection strictServiceCollection) return services;
return strictServiceCollection.AsNormal();
}
}
public interface IStrictServiceCollection : IServiceCollection
{
IServiceCollection AsNormal();
}
public class StrictServiceCollection : IStrictServiceCollection
{
protected readonly IServiceCollection NormalServiceCollection;
public StrictServiceCollection(IServiceCollection normalServiceCollection)
{
if (normalServiceCollection == null) throw new ArgumentNullException(nameof(normalServiceCollection));
if (normalServiceCollection is IStrictServiceCollection) throw new ArgumentException("nesting strict service collections.");
NormalServiceCollection = normalServiceCollection;
}
public IServiceCollection AsNormal()
{
return NormalServiceCollection;
}
public void CheckInvalidRegistration(ServiceDescriptor item)
{
int count = NormalServiceCollection.Count;
for (int i = 0; i < count; i++)
{
ServiceDescriptor service = NormalServiceCollection[i];
if (service.ServiceType == item.ServiceType &&
service.ImplementationType == item.ImplementationType &&
object.Equals(service.ServiceKey, item.ServiceKey))
{
// Already added
// no return but
throw new InvalidOperationException($"service {service.ServiceType.FullName} already added as {service.Lifetime}");
}
}
}
// add your checks in this methods
public void Insert(int index, ServiceDescriptor item)
{
CheckInvalidRegistration(item);
NormalServiceCollection.Insert(index, item);
}
public void Add(ServiceDescriptor item)
{
CheckInvalidRegistration(item);
NormalServiceCollection.Add(item);
}
public ServiceDescriptor this[int index]
{
get { return NormalServiceCollection[index]; }
set
{
CheckInvalidRegistration(value); // edge case if value is already at index
NormalServiceCollection[index] = value;
}
}
// the other methods are just passed
public int Count => NormalServiceCollection.Count;
public bool IsReadOnly => NormalServiceCollection.IsReadOnly;
public IEnumerator<ServiceDescriptor> GetEnumerator() => NormalServiceCollection.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => NormalServiceCollection.GetEnumerator();
public void Clear() => NormalServiceCollection.Clear();
public bool Contains(ServiceDescriptor item) => NormalServiceCollection.Contains(item);
public void CopyTo(ServiceDescriptor[] array, int arrayIndex) => NormalServiceCollection.CopyTo(array, arrayIndex);
public bool Remove(ServiceDescriptor item) => NormalServiceCollection.Remove(item);
public int IndexOf(ServiceDescriptor item) => NormalServiceCollection.IndexOf(item);
public void RemoveAt(int index) => NormalServiceCollection.RemoveAt(index);
}
}
The actual registration implementation is handled by the ServiceCollection class, not the interfaces or extension methods. The only way to intercept all registrations is to WRAP that class and override its IList<ServiceDescriptor>.Add
and maybe IList<ServiceDescriptor>.Insert
methods. The IServiceCollection
interface itself is just public interface IServiceCollection : IList<ServiceDescriptor>{}
.
The Microsoft.Extension.Http
package uses a custom service collection to ensure the order of configuration option registration, in DefaultHttpClientBuilderServiceCollection class by providing its own IList<>.Add
while delegating all other methods to the outer ServiceCollection
:
internal sealed class DefaultHttpClientBuilderServiceCollection : IServiceCollection
{
private readonly IServiceCollection _services;
private readonly bool _isDefault;
private readonly DefaultHttpClientConfigurationTracker _tracker;
public DefaultHttpClientBuilderServiceCollection(IServiceCollection services, bool isDefault, DefaultHttpClientConfigurationTracker tracker)
{
_services = services;
_isDefault = isDefault;
_tracker = tracker;
}
public void Add(ServiceDescriptor item)
{
//Custom registration code
...
}
public ServiceDescriptor this[int index]
{
get => _services[index];
set => _services[index] = value;
}
public int Count => _services.Count;
public bool IsReadOnly => _services.IsReadOnly;
public void Clear() => _services.Clear();
public bool Contains(ServiceDescriptor item) => _services.Contains(item);
public void CopyTo(ServiceDescriptor[] array, int arrayIndex) => _services.CopyTo(array, arrayIndex);
public IEnumerator<ServiceDescriptor> GetEnumerator() => _services.GetEnumerator();
public int IndexOf(ServiceDescriptor item) => _services.IndexOf(item);
public void Insert(int index, ServiceDescriptor item) => _services.Insert(index, item);
public bool Remove(ServiceDescriptor item) => _services.Remove(item);
public void RemoveAt(int index) => _services.RemoveAt(index);
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
Adapted to the question, this would be something like :
internal sealed class SingleRegistrationServiceCollection : IServiceCollection
{
...
void ValidateRegistration(ServiceDescriptor item)
{
for (int i = 0; i < Count; i++) {
var service = services[i];
if (service.ServiceType == item.ServiceType &&
service.GetImplementationType() == item.ImplementationType
&&
object.Equals(service.ServiceKey, item.ServiceKey)) {
// Already added
// no return but
throw new InvalidOperationException(...)
}
}
}
public void Add(ServiceDescriptor item)
{
ValidateRegistration(item);
_services.Add(item);
}
public void Insert(int index, ServiceDescriptor item)
{
ValidateRegistration(item);
_services.Insert(index, item);
}
...
The class is internal and can only be accessed indirectly through the AddHttpClient
extension method, through the DefaultHttpClientBuilder. This avoids leaking the custom service collection or mixing up registrations :
public static IHttpClientBuilder AddHttpClient(this IServiceCollection services, string name, Action<HttpClient> configureClient)
{
ThrowHelper.ThrowIfNull(services);
ThrowHelper.ThrowIfNull(name);
ThrowHelper.ThrowIfNull(configureClient);
AddHttpClient(services);
var builder = new DefaultHttpClientBuilder(services, name);
builder.ConfigureHttpClient(configureClient);
return builder;
}
Adapting this to the question :
public static IServiceCollection AddStrict(this IServiceCollection outerServices, Action<IServiceCollection> configureStrict)
{
....
var services=new SingleRegistrationServiceCollection(outerServices);
configureStrict(services);
return outerServices;
}
This will result in registration code like this :
services.AddX()
.AddStrict(svc=>
.AddY()
.AddZ()
)
.AddXXX()
The configuration action pattern allows more complex validation too. Eg some validations may be possible only once all "strict" services are added. This can by done by adding a Validate
method to the custom service collection and calling it after the configuration action:
public static IServiceCollection AddStrict(this IServiceCollection outerServices, Action<IServiceCollection> configureStrict)
{
....
var services=new SingleRegistrationServiceCollection(outerServices);
configureStrict(services);
services.Validate();
return outerServices;
}
本文标签: cHow do I implement a quotstrictquot mode for adding to IServiceCollectionStack Overflow
版权声明:本文标题:c# - How do I implement a "strict" mode for adding to IServiceCollection - Stack Overflow 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.betaflare.com/web/1736775249a1952306.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
AggregateException
or some info where things fail – Ivan Petrov Commented Jan 7 at 22:17TryAddEnumerable
and notTryAdd
) overwrites an existing one. – Ivan Petrov Commented Jan 8 at 7:43.Add
and.TryAdd
don't overwrite registrations and the code you provided proves this. What you seem to ask for is enforcing a single implementation per type, not "strict" mode. To what actual problem would that be a solution?some "normal" edge cases
that includes keyed services which isn't edge. You can't modify the built-in extension methods anyway, so you can't make clients throw if eg they register two different repository implementations for the same interface. Interfaces don't have code anyway soIStrictServiceCollection
won't help – Panagiotis Kanavos Commented Jan 8 at 8:08