diff --git a/.github/SECURITY.md b/.github/SECURITY.md
new file mode 100644
index 00000000..da9c516d
--- /dev/null
+++ b/.github/SECURITY.md
@@ -0,0 +1,5 @@
+## Security contact information
+
+To report a security vulnerability, please use the
+[Tidelift security contact](https://tidelift.com/security).
+Tidelift will coordinate the fix and disclosure.
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index e0a13f1e..db6546c4 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -8,6 +8,7 @@ jobs:
if: "!contains(github.event.head_commit.message, '[skip ci]')"
env:
+ CI: true
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1
DOTNET_CLI_TELEMETRY_OPTOUT: 1
DOTNET_NOLOGO: true
@@ -16,14 +17,70 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
- - name: Setup .NET 9 SDK
+ - name: Setup .NET 10 SDK
uses: actions/setup-dotnet@v4
with:
- dotnet-version: "9.x"
+ dotnet-version: "10.x"
- - name: Test
- run: dotnet test --collect:"XPlat Code Coverage"
+ - name: Install dotnet-coverage
+ run: dotnet tool install -g dotnet-coverage
+
+ - name: Test with coverage
+ run: dotnet-coverage collect --output coverage.cobertura.xml --output-format cobertura "dotnet test"
- name: Update codecov
if: startsWith(github.repository, 'khellang/')
- uses: codecov/codecov-action@v1
+ uses: codecov/codecov-action@v5
+ with:
+ files: coverage.cobertura.xml
+ token: ${{ secrets.CODECOV_TOKEN }}
+ fail_ci_if_error: false
+
+ - name: Pack NuGet package
+ run: dotnet pack src/Scrutor/Scrutor.csproj --configuration Release --output ./artifacts
+
+ - name: Upload NuGet package artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: nuget-packages
+ path: |
+ ./artifacts/*.nupkg
+ ./artifacts/*.snupkg
+
+ - name: Push to GitHub Packages
+ if: github.event_name == 'push' && startsWith(github.repository, 'khellang/')
+ run: dotnet nuget push ./artifacts/*.*nupkg --source https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json --api-key ${{ secrets.GITHUB_TOKEN }} --skip-duplicate
+
+ publish:
+ runs-on: ubuntu-latest
+ needs: build
+ if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && startsWith(github.repository, 'khellang/')
+ permissions:
+ id-token: write
+ contents: read
+
+ env:
+ DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1
+ DOTNET_CLI_TELEMETRY_OPTOUT: 1
+ DOTNET_NOLOGO: true
+
+ steps:
+ - name: Download NuGet package artifact
+ uses: actions/download-artifact@v4
+ with:
+ name: nuget-packages
+ path: ./artifacts
+
+ - name: Setup .NET 10 SDK
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: "10.x"
+
+ - name: Authenticate to NuGet.org with OIDC
+ uses: NuGet/login@v1
+ id: login
+ with:
+ user: khellang
+
+ - name: Push to NuGet.org
+ run: dotnet nuget push ./artifacts/*.nupkg --api-key ${{steps.login.outputs.NUGET_API_KEY}} --source https://api.nuget.org/v3/index.json --skip-duplicate
diff --git a/NuGet.Config b/NuGet.Config
index dbf0180d..6bd4065a 100644
--- a/NuGet.Config
+++ b/NuGet.Config
@@ -3,6 +3,6 @@
-
+
diff --git a/README.md b/README.md
index 7144c98e..48628160 100644
--- a/README.md
+++ b/README.md
@@ -66,6 +66,25 @@ collection.Scan(scan => scan
.AsImplementedInterfaces());
```
+#### Scanning compiled view (UI) types
+By default, Scrutor excludes compiler-generated types from the `.AddClasses()` type filters. When loading views from a framework such as [Avalonia UI](https://avaloniaui.net/), we need to opt in to compiler-generated types, like this:
+
+```csharp
+.AddClasses(classes => classes
+ // Opt-in to compiler-generated types
+ .WithAttribute()
+ // Optionally filter types to reduce number of service registrations.
+ .InNamespaces("MyApp.Desktop.Views")
+ .AssignableToAny(
+ typeof(Window),
+ typeof(UserControl)
+ )
+ .AsSelf()
+ .WithSingletonLifetime()
+```
+
+With some UI frameworks, these compiler-generated views implement quite a few interfaces, so unless you need them, it's probably best to register these classes `.AsSelf()`; in other words, be very precise with your filters that accept compiler generated types.
+
### Decoration
```csharp
@@ -87,3 +106,11 @@ var serviceProvider = collection.BuildServiceProvider();
// OtherDecorator -> Decorator -> Decorated
var instance = serviceProvider.GetRequiredService();
```
+
+## Sponsors
+
+[Entity Framework Extensions](https://entityframework-extensions.net/?utm_source=khellang&utm_medium=Scrutor) and [Dapper Plus](https://dapper-plus.net/?utm_source=khellang&utm_medium=Scrutor) are major sponsors and proud to contribute to the development of Scrutor.
+
+[](https://entityframework-extensions.net/bulk-insert?utm_source=khellang&utm_medium=Scrutor)
+
+[](https://dapper-plus.net/bulk-insert?utm_source=khellang&utm_medium=Scrutor)
diff --git a/global.json b/global.json
new file mode 100644
index 00000000..1d364c6a
--- /dev/null
+++ b/global.json
@@ -0,0 +1,9 @@
+{
+ "sdk": {
+ "version": "10.0.100",
+ "rollForward": "latestFeature"
+ },
+ "test": {
+ "runner": "Microsoft.Testing.Platform"
+ }
+}
diff --git a/src/Scrutor/AttributeSelector.cs b/src/Scrutor/AttributeSelector.cs
index 0026ded4..af3ee630 100644
--- a/src/Scrutor/AttributeSelector.cs
+++ b/src/Scrutor/AttributeSelector.cs
@@ -37,7 +37,9 @@ void ISelector.Populate(IServiceCollection services, RegistrationStrategy? regis
foreach (var serviceType in serviceTypes)
{
- var descriptor = new ServiceDescriptor(serviceType, type, attribute.Lifetime);
+ var descriptor = attribute.ServiceKey is null
+ ? new ServiceDescriptor(serviceType, type, attribute.Lifetime)
+ : new ServiceDescriptor(serviceType, attribute.ServiceKey, type, attribute.Lifetime);
strategy.Apply(services, descriptor);
}
@@ -47,6 +49,6 @@ void ISelector.Populate(IServiceCollection services, RegistrationStrategy? regis
private static IEnumerable GetDuplicates(IEnumerable attributes)
{
- return attributes.GroupBy(s => s.ServiceType).SelectMany(grp => grp.Skip(1));
+ return attributes.GroupBy(s => new { s.ServiceType, s.ServiceKey }).SelectMany(grp => grp.Skip(1));
}
}
diff --git a/src/Scrutor/DecoratedService.cs b/src/Scrutor/DecoratedService.cs
new file mode 100644
index 00000000..9b86bdf8
--- /dev/null
+++ b/src/Scrutor/DecoratedService.cs
@@ -0,0 +1,26 @@
+using System;
+using System.Collections.Generic;
+
+namespace Scrutor;
+
+///
+/// A handle to a decorated service. This can be used to resolve decorated services from an
+/// using .
+///
+/// The type of services which were decorated.
+public sealed class DecoratedService
+{
+ internal DecoratedService(Type serviceType, IReadOnlyList serviceKeys)
+ {
+ if (!typeof(TService).IsAssignableFrom(serviceType))
+ throw new ArgumentException($"The type {serviceType} is not assignable to the service type {typeof(TService)}");
+
+ ServiceType = serviceType;
+ ServiceKeys = serviceKeys;
+ }
+
+ internal Type ServiceType { get; }
+ internal IReadOnlyList ServiceKeys { get; } // In descending order of precedence
+
+ internal DecoratedService Downcast() => new(ServiceType, ServiceKeys);
+}
diff --git a/src/Scrutor/ILifetimeSelector.cs b/src/Scrutor/ILifetimeSelector.cs
index d473110b..9bab980d 100644
--- a/src/Scrutor/ILifetimeSelector.cs
+++ b/src/Scrutor/ILifetimeSelector.cs
@@ -1,5 +1,5 @@
-using System;
-using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection;
+using System;
namespace Scrutor;
@@ -29,4 +29,16 @@ public interface ILifetimeSelector : IServiceTypeSelector
/// Registers each matching concrete type with a lifetime based on the provided .
///
IImplementationTypeSelector WithLifetime(Func selector);
+
+ ///
+ /// Registers each matching concrete type with the specified .
+ ///
+ /// The service key to use for registration.
+ ILifetimeSelector WithServiceKey(object serviceKey);
+
+ ///
+ /// Registers each matching concrete type with a service key based on the provided .
+ ///
+ /// A function to determine the service key for each type.
+ ILifetimeSelector WithServiceKey(Func selector);
}
diff --git a/src/Scrutor/ImplementationTypeFilter.cs b/src/Scrutor/ImplementationTypeFilter.cs
index 243e64b8..5afe35d4 100644
--- a/src/Scrutor/ImplementationTypeFilter.cs
+++ b/src/Scrutor/ImplementationTypeFilter.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Runtime.CompilerServices;
namespace Scrutor;
@@ -13,6 +14,8 @@ public ImplementationTypeFilter(ISet types)
internal ISet Types { get; private set; }
+ internal bool IncludeCompilerGeneratedTypes { get; private set; }
+
public IImplementationTypeFilter AssignableTo()
{
return AssignableTo(typeof(T));
@@ -48,6 +51,7 @@ public IImplementationTypeFilter WithAttribute(Type attributeType)
{
Preconditions.NotNull(attributeType, nameof(attributeType));
+ IncludeCompilerGeneratedTypes |= attributeType == typeof(CompilerGeneratedAttribute);
return Where(t => t.HasAttribute(attributeType));
}
diff --git a/src/Scrutor/ImplementationTypeSelector.cs b/src/Scrutor/ImplementationTypeSelector.cs
index 7da2c9ae..28c7b54a 100644
--- a/src/Scrutor/ImplementationTypeSelector.cs
+++ b/src/Scrutor/ImplementationTypeSelector.cs
@@ -3,6 +3,7 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
+using System.Runtime.CompilerServices;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyModel;
@@ -29,7 +30,7 @@ public IServiceTypeSelector AddClasses()
public IServiceTypeSelector AddClasses(bool publicOnly)
{
- var classes = GetNonAbstractClasses(publicOnly);
+ var classes = GetNonAbstractClasses(publicOnly, includeCompilerGenerated: false);
return AddSelector(classes);
}
@@ -43,12 +44,15 @@ public IServiceTypeSelector AddClasses(Action action,
{
Preconditions.NotNull(action, nameof(action));
- var classes = GetNonAbstractClasses(publicOnly);
+ var classes = GetNonAbstractClasses(publicOnly, includeCompilerGenerated: true);
var filter = new ImplementationTypeFilter(classes);
action(filter);
+ if (!filter.IncludeCompilerGeneratedTypes)
+ filter.WithoutAttribute();
+
return AddSelector(filter.Types);
}
@@ -144,8 +148,8 @@ private IServiceTypeSelector AddSelector(ISet types)
return selector;
}
- private ISet GetNonAbstractClasses(bool publicOnly)
+ private ISet GetNonAbstractClasses(bool publicOnly, bool includeCompilerGenerated)
{
- return Types.Where(t => t.IsNonAbstractClass(publicOnly)).ToHashSet();
+ return Types.Where(t => t.IsNonAbstractClass(publicOnly, includeCompilerGenerated)).ToHashSet();
}
}
diff --git a/src/Scrutor/LifetimeSelector.cs b/src/Scrutor/LifetimeSelector.cs
index 595ab23d..3e07acd8 100644
--- a/src/Scrutor/LifetimeSelector.cs
+++ b/src/Scrutor/LifetimeSelector.cs
@@ -1,9 +1,9 @@
-using System;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyModel;
+using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.DependencyModel;
namespace Scrutor;
@@ -24,6 +24,8 @@ public LifetimeSelector(ServiceTypeSelector inner, IEnumerable typeMaps
public Func? SelectorFn { get; set; }
+ public Func? ServiceKeySelectorFn { get; set; }
+
public IImplementationTypeSelector WithSingletonLifetime()
{
return WithLifetime(ServiceLifetime.Singleton);
@@ -54,6 +56,22 @@ public IImplementationTypeSelector WithLifetime(Func sele
return this;
}
+ public ILifetimeSelector WithServiceKey(object serviceKey)
+ {
+ Preconditions.NotNull(serviceKey, nameof(serviceKey));
+
+ return WithServiceKey(_ => serviceKey);
+ }
+
+ public ILifetimeSelector WithServiceKey(Func selector)
+ {
+ Preconditions.NotNull(selector, nameof(selector));
+
+ Inner.PropagateServiceKey(selector);
+
+ return this;
+ }
+
#region Chain Methods
[ExcludeFromCodeCoverage]
@@ -231,6 +249,7 @@ void ISelector.Populate(IServiceCollection services, RegistrationStrategy? strat
strategy ??= RegistrationStrategy.Append;
var lifetimes = new Dictionary();
+ var serviceKeys = new Dictionary();
foreach (var typeMap in TypeMaps)
{
@@ -244,8 +263,10 @@ void ISelector.Populate(IServiceCollection services, RegistrationStrategy? strat
}
var lifetime = GetOrAddLifetime(lifetimes, implementationType);
-
- var descriptor = new ServiceDescriptor(serviceType, implementationType, lifetime);
+ var serviceKey = GetOrAddServiceKey(serviceKeys, implementationType);
+ var descriptor = serviceKey is not null
+ ? new ServiceDescriptor(serviceType, serviceKey, implementationType, lifetime)
+ : new ServiceDescriptor(serviceType, implementationType, lifetime);
strategy.Apply(services, descriptor);
}
@@ -256,8 +277,11 @@ void ISelector.Populate(IServiceCollection services, RegistrationStrategy? strat
foreach (var serviceType in typeFactoryMap.ServiceTypes)
{
var lifetime = GetOrAddLifetime(lifetimes, typeFactoryMap.ImplementationType);
+ var serviceKey = GetOrAddServiceKey(serviceKeys, typeFactoryMap.ImplementationType);
- var descriptor = new ServiceDescriptor(serviceType, typeFactoryMap.ImplementationFactory, lifetime);
+ var descriptor = serviceKey is not null
+ ? new ServiceDescriptor(serviceType, serviceKey, WrapImplementationFactory(typeFactoryMap.ImplementationFactory), lifetime)
+ : new ServiceDescriptor(serviceType, typeFactoryMap.ImplementationFactory, lifetime);
strategy.Apply(services, descriptor);
}
@@ -277,4 +301,23 @@ private ServiceLifetime GetOrAddLifetime(Dictionary lifet
return lifetime;
}
+
+ private object? GetOrAddServiceKey(Dictionary serviceKeys, Type implementationType)
+ {
+ if (serviceKeys.TryGetValue(implementationType, out var serviceKey))
+ {
+ return serviceKey;
+ }
+
+ serviceKey = ServiceKeySelectorFn?.Invoke(implementationType);
+
+ serviceKeys[implementationType] = serviceKey;
+
+ return serviceKey;
+ }
+
+ private static Func WrapImplementationFactory(Func factory)
+ {
+ return (sp, _) => factory(sp);
+ }
}
diff --git a/src/Scrutor/MaybeNullWhenAttribute.cs b/src/Scrutor/MaybeNullWhenAttribute.cs
new file mode 100644
index 00000000..6dac1a60
--- /dev/null
+++ b/src/Scrutor/MaybeNullWhenAttribute.cs
@@ -0,0 +1,19 @@
+#if NETFRAMEWORK
+
+using System;
+
+/// Specifies that when a method returns , the parameter will not be null even if the corresponding type allows it.
+[AttributeUsage(AttributeTargets.Parameter, Inherited = false)]
+internal sealed class NotNullWhenAttribute : Attribute
+{
+ /// Initializes the attribute with the specified return value condition.
+ ///
+ /// The return value condition. If the method returns this value, the associated parameter will not be null.
+ ///
+ public NotNullWhenAttribute(bool returnValue) => ReturnValue = returnValue;
+
+ /// Gets the return value condition.
+ public bool ReturnValue { get; }
+}
+
+#endif
diff --git a/src/Scrutor/ReflectionExtensions.cs b/src/Scrutor/ReflectionExtensions.cs
index e7dcde4d..5e83ebf3 100644
--- a/src/Scrutor/ReflectionExtensions.cs
+++ b/src/Scrutor/ReflectionExtensions.cs
@@ -9,7 +9,7 @@ namespace Scrutor;
internal static class ReflectionExtensions
{
- public static bool IsNonAbstractClass(this Type type, bool publicOnly)
+ public static bool IsNonAbstractClass(this Type type, bool publicOnly, bool includeCompilerGenerated)
{
if (type.IsSpecialName)
{
@@ -18,7 +18,7 @@ public static bool IsNonAbstractClass(this Type type, bool publicOnly)
if (type.IsClass && !type.IsAbstract)
{
- if (type.HasAttribute())
+ if (!includeCompilerGenerated && type.HasAttribute())
{
return false;
}
diff --git a/src/Scrutor/Scrutor.csproj b/src/Scrutor/Scrutor.csproj
index a0a63185..6dbd7fa2 100644
--- a/src/Scrutor/Scrutor.csproj
+++ b/src/Scrutor/Scrutor.csproj
@@ -1,9 +1,9 @@
Register services using assembly scanning and a fluent API.
- 6.0.1
+ v
Kristian Hellang
- net462;netstandard2.0;net8.0
+ net462;netstandard2.0;net8.0;net10.0
$(NoWarn);CS1591
latest
enable
@@ -25,6 +25,9 @@
snupkg
6.0.0.0
+
+ true
+ true
@@ -32,10 +35,11 @@
+
-
-
+
+
diff --git a/src/Scrutor/ServiceCollectionExtensions.Decoration.cs b/src/Scrutor/ServiceCollectionExtensions.Decoration.cs
index 189ffad7..db6f3383 100644
--- a/src/Scrutor/ServiceCollectionExtensions.Decoration.cs
+++ b/src/Scrutor/ServiceCollectionExtensions.Decoration.cs
@@ -1,6 +1,8 @@
using Scrutor;
using System;
+using System.Diagnostics.CodeAnalysis;
using JetBrains.Annotations;
+using System.Collections.Generic;
// ReSharper disable once CheckNamespace
namespace Microsoft.Extensions.DependencyInjection;
@@ -19,10 +21,206 @@ public static partial class ServiceCollectionExtensions
/// If the argument is null.
public static IServiceCollection Decorate(this IServiceCollection services)
where TDecorator : TService
+ {
+ return services.Decorate(out _);
+ }
+
+ ///
+ /// Decorates all registered services of type
+ /// using the specified type .
+ ///
+ /// The services to add to.
+ /// If the argument is null.
+ public static bool TryDecorate(this IServiceCollection services)
+ where TDecorator : TService
+ {
+ return services.TryDecorate(out _);
+ }
+
+ ///
+ /// Decorates all registered services of the specified
+ /// using the specified .
+ ///
+ /// The services to add to.
+ /// The type of services to decorate.
+ /// The type to decorate existing services with.
+ /// If no service of the specified has been registered.
+ /// If either the ,
+ /// or arguments are null.
+ public static IServiceCollection Decorate(this IServiceCollection services, Type serviceType, Type decoratorType)
+ {
+ return services.Decorate(serviceType, decoratorType, out _);
+ }
+
+ ///
+ /// Decorates all registered services of the specified
+ /// using the specified .
+ ///
+ /// The services to add to.
+ /// The type of services to decorate.
+ /// The type to decorate existing services with.
+ /// If either the ,
+ /// or arguments are null.
+ public static bool TryDecorate(this IServiceCollection services, Type serviceType, Type decoratorType)
+ {
+ return services.TryDecorate(serviceType, decoratorType, out _);
+ }
+
+ ///
+ /// Decorates all registered services of type
+ /// using the function.
+ ///
+ /// The type of services to decorate.
+ /// The services to add to.
+ /// The decorator function.
+ /// If no service of has been registered.
+ /// If either the
+ /// or arguments are null.
+ public static IServiceCollection Decorate(this IServiceCollection services, Func decorator) where TService : notnull
+ {
+ return services.Decorate(decorator, out _);
+ }
+
+ ///
+ /// Decorates all registered services of type
+ /// using the function.
+ ///
+ /// The type of services to decorate.
+ /// The services to add to.
+ /// The decorator function.
+ /// If either the
+ /// or arguments are null.
+ public static bool TryDecorate(this IServiceCollection services, Func decorator) where TService : notnull
+ {
+ return services.TryDecorate(decorator, out _);
+ }
+
+ ///
+ /// Decorates all registered services of type
+ /// using the function.
+ ///
+ /// The type of services to decorate.
+ /// The services to add to.
+ /// The decorator function.
+ /// If no service of has been registered.
+ /// If either the
+ /// or arguments are null.
+ public static IServiceCollection Decorate(this IServiceCollection services, Func decorator) where TService : notnull
+ {
+ return services.Decorate(decorator, out _);
+ }
+
+ ///
+ /// Decorates all registered services of type
+ /// using the function.
+ ///
+ /// The type of services to decorate.
+ /// The services to add to.
+ /// The decorator function.
+ /// If either the
+ /// or arguments are null.
+ public static bool TryDecorate(this IServiceCollection services, Func decorator) where TService : notnull
+ {
+ return services.TryDecorate(decorator, out _);
+ }
+
+ ///
+ /// Decorates all registered services of the specified
+ /// using the function.
+ ///
+ /// The services to add to.
+ /// The type of services to decorate.
+ /// The decorator function.
+ /// If no service of the specified has been registered.
+ /// If either the ,
+ /// or arguments are null.
+ public static IServiceCollection Decorate(this IServiceCollection services, Type serviceType, Func