From d377a95ef0720d25b64da24a52ddfc30a5baec08 Mon Sep 17 00:00:00 2001 From: Alessandro Giorgetti Date: Mon, 5 Oct 2026 15:53:36 +0200 Subject: [PATCH] fix: apply precise framework interface filtering (#60) Use ordinal System namespace boundaries and the exact mscorlib assembly name. Cover application assembly prefixes, namespace and case boundaries, and actual async disposal contracts across supported targets. Co-authored-by: Codex --- Changelog.md | 2 + README.md | 2 + ...mblyInspector.FrameworkInterfaces.Tests.cs | 105 ++++++++++++++++++ .../Inspector/IServiceSelector.cs | 4 +- .../TypeExtensions.cs | 10 +- 5 files changed, 116 insertions(+), 7 deletions(-) create mode 100644 src/Mammoth.Extensions.DependencyInjection.Tests/AssemblyInspector.FrameworkInterfaces.Tests.cs diff --git a/Changelog.md b/Changelog.md index c85efbb..9a6883f 100644 --- a/Changelog.md +++ b/Changelog.md @@ -8,6 +8,8 @@ ### Bug Fixes +- Apply precise System namespace boundaries and the exact mscorlib assembly name when selecting framework interfaces, preserving application contracts from Systematic assemblies and excluding IAsyncDisposable consistently across runtimes [#60](https://github.com/PrimordialCode/Mammoth.Extensions.DependencyInjection/issues/60). + - Update the private SourceLink build dependency to 10.0.401, resolving patched Microsoft.Build.Tasks.Git for CVE-2026-62900 [#65](https://github.com/PrimordialCode/Mammoth.Extensions.DependencyInjection/issues/65). - Preserve native ValidateOnBuild graph validation when transient-disposable diagnostics are enabled, without activating user services during validation [#59](https://github.com/PrimordialCode/Mammoth.Extensions.DependencyInjection/issues/59). diff --git a/README.md b/README.md index b6b65be..8baee1d 100644 --- a/README.md +++ b/README.md @@ -311,6 +311,8 @@ It is once again inspired by the syntax used in [Castle.Windsor](https://github. It looks for classes and offers a series of methods that are pretty self explanatory to output one or more `ServiceDescriptor` that will be registered in the ServiceCollection. +`WithServiceAllInterfaces()` excludes interfaces in `System` and its child namespaces (such as `System.Collections.Generic`), plus interfaces from the assembly named exactly `mscorlib`. Comparisons are ordinal and case-sensitive. `IDisposable` and `IAsyncDisposable` are excluded on every supported runtime, regardless of their defining assembly. Application assembly names such as `Systematic.Contracts` do not affect selection; namespaces such as `Systematic` and `Systems` are not children of `System`. Use `BasedOn().WithServiceBase()` to explicitly register a framework interface. + It supports `DependsOn` for keyed services: ```csharp diff --git a/src/Mammoth.Extensions.DependencyInjection.Tests/AssemblyInspector.FrameworkInterfaces.Tests.cs b/src/Mammoth.Extensions.DependencyInjection.Tests/AssemblyInspector.FrameworkInterfaces.Tests.cs new file mode 100644 index 0000000..2f3c48f --- /dev/null +++ b/src/Mammoth.Extensions.DependencyInjection.Tests/AssemblyInspector.FrameworkInterfaces.Tests.cs @@ -0,0 +1,105 @@ +using System.Reflection; +using System.Reflection.Emit; +using Mammoth.Extensions.DependencyInjection.Inspector; +using Microsoft.Extensions.DependencyInjection; + +namespace Mammoth.Extensions.DependencyInjection.Tests +{ + [TestClass] + public class AssemblyInspectorFrameworkInterfaceTests + { + [TestMethod] + [DataRow("Systematic.Contracts", "Application.Contracts", false)] + [DataRow("System.Contracts", "Application.Contracts", false)] + [DataRow("Application.mscorlib.Contracts", "Application.Contracts", false)] + [DataRow("mscorlib.Contracts", "Application.Contracts", false)] + [DataRow("Mscorlib", "Application.Contracts", false)] + [DataRow("Ordinary.Contracts", "System", true)] + [DataRow("Ordinary.Contracts", "System.Contracts", true)] + [DataRow("Ordinary.Contracts", "System.Contracts.Child", true)] + [DataRow("Ordinary.Contracts", "Systematic.Contracts", false)] + [DataRow("Ordinary.Contracts", "Systems", false)] + [DataRow("Ordinary.Contracts", "system.Contracts", false)] + [DataRow("Ordinary.Contracts", "Application.System", false)] + [DataRow("Ordinary.Contracts", "", false)] + [DataRow("mscorlib", "Application.Contracts", true)] + public void FrameworkPolicyUsesNamespaceBoundariesAndExactAssemblyName(string assemblyName, string typeNamespace, bool excluded) + { + var (contract, implementation) = CreateFixture(assemblyName, typeNamespace); + Assert.AreEqual(assemblyName, contract.Assembly.GetName().Name); + Assert.AreEqual(excluded, contract.IsFrameworkType()); + + var descriptors = new AssemblyInspector().FromAssembly(implementation.Assembly) + .BasedOn(contract).WithServiceAllInterfaces().LifestyleTransient().ToArray(); + Assert.AreEqual(excluded ? 0 : 1, descriptors.Length); + if (!excluded) + { + Assert.AreEqual(contract, descriptors[0].ServiceType); + Assert.AreEqual(implementation, descriptors[0].ImplementationType); + } + } + + [TestMethod] + public void DisposableInterfacesAreExcludedRegardlessOfTheirDefiningAssembly() + { +#if NETFRAMEWORK + Assert.AreEqual("Microsoft.Bcl.AsyncInterfaces", typeof(IAsyncDisposable).Assembly.GetName().Name); +#else + Assert.AreEqual("System.Private.CoreLib", typeof(IAsyncDisposable).Assembly.GetName().Name); +#endif + Assert.IsTrue(typeof(IDisposable).IsFrameworkType()); + Assert.IsTrue(typeof(IAsyncDisposable).IsFrameworkType()); + var descriptors = new AssemblyInspector().FromAssemblyContaining() + .BasedOn().If(t => t == typeof(DisposableService)) + .WithServiceAllInterfaces().LifestyleSingleton().ToArray(); + Assert.HasCount(1, descriptors); + Assert.AreEqual(typeof(IApplicationContract), descriptors[0].ServiceType); + Assert.AreEqual(typeof(DisposableService), descriptors[0].ImplementationType); + } + + [TestMethod] + public void ExplicitBaseSelectionCanStillRegisterFrameworkInterfaces() + { + var descriptors = new AssemblyInspector().FromAssemblyContaining() + .BasedOn().If(t => t == typeof(DisposableService)) + .WithServiceBase().LifestyleScoped().ToArray(); + Assert.HasCount(1, descriptors); + Assert.AreEqual(typeof(IAsyncDisposable), descriptors[0].ServiceType); + Assert.AreEqual(ServiceLifetime.Scoped, descriptors[0].Lifetime); + } + + [TestMethod] + public void SelfSelectionCanStillRegisterClassesImplementingFrameworkInterfaces() + { + var descriptors = new AssemblyInspector().FromAssemblyContaining() + .BasedOn().If(t => t == typeof(DisposableService)) + .WithServiceSelf().LifestyleTransient().ToArray(); + Assert.HasCount(1, descriptors); + Assert.AreEqual(typeof(DisposableService), descriptors[0].ServiceType); + } + + private static (Type Contract, Type Implementation) CreateFixture(string assemblyName, string typeNamespace) + { +#if NETFRAMEWORK + var assembly = AppDomain.CurrentDomain.DefineDynamicAssembly(new AssemblyName(assemblyName), AssemblyBuilderAccess.Run); +#else + var assembly = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName(assemblyName), AssemblyBuilderAccess.Run); +#endif + var module = assembly.DefineDynamicModule("Contracts"); + var prefix = typeNamespace.Length == 0 ? "" : typeNamespace + "."; + var contract = module.DefineType(prefix + "IApplicationService", TypeAttributes.Public | TypeAttributes.Interface | TypeAttributes.Abstract).CreateTypeInfo()!.AsType(); + var builder = module.DefineType("Application.Implementation.Service", TypeAttributes.Public | TypeAttributes.Class); + builder.AddInterfaceImplementation(contract); + builder.DefineDefaultConstructor(MethodAttributes.Public); + return (contract, builder.CreateTypeInfo()!.AsType()); + } + + public interface IApplicationContract { } + + public class DisposableService : IApplicationContract, IDisposable, IAsyncDisposable + { + public void Dispose() { } + public ValueTask DisposeAsync() => default; + } + } +} diff --git a/src/Mammoth.Extensions.DependencyInjection/Inspector/IServiceSelector.cs b/src/Mammoth.Extensions.DependencyInjection/Inspector/IServiceSelector.cs index 8548f6d..cc6e35b 100644 --- a/src/Mammoth.Extensions.DependencyInjection/Inspector/IServiceSelector.cs +++ b/src/Mammoth.Extensions.DependencyInjection/Inspector/IServiceSelector.cs @@ -19,8 +19,8 @@ public interface IServiceSelector /// /// The lifestyle selector. /// - /// Some interfaces will be excluded: - /// - all those from the System namespace. + /// Interfaces in System or its child namespaces, and interfaces from the assembly named exactly mscorlib, + /// are excluded. Comparisons are ordinal and case-sensitive; application assembly name prefixes do not affect selection. /// ILifestyleSelector WithServiceAllInterfaces(); diff --git a/src/Mammoth.Extensions.DependencyInjection/TypeExtensions.cs b/src/Mammoth.Extensions.DependencyInjection/TypeExtensions.cs index 98fe6f3..d502f2c 100644 --- a/src/Mammoth.Extensions.DependencyInjection/TypeExtensions.cs +++ b/src/Mammoth.Extensions.DependencyInjection/TypeExtensions.cs @@ -7,16 +7,16 @@ public static class TypeExtensions { /// /// Determines whether the specified type is a framework type. - /// A type is considered a framework type if it is defined in the System namespace or if it is defined in the mscorlib assembly. + /// A type is considered a framework type if it is defined in the System namespace or one of its child namespaces, or in the assembly named exactly mscorlib. Namespace and assembly names are compared ordinally and case-sensitively. /// /// The type to check. /// true if the specified type is a framework type; otherwise, false. public static bool IsFrameworkType(this Type type) { - var assemblyFullName = type.Assembly.FullName; - return !string.IsNullOrEmpty(assemblyFullName) - && (assemblyFullName.StartsWith("System", StringComparison.InvariantCulture) - || assemblyFullName.Contains("mscorlib")); + var typeNamespace = type.Namespace; + return string.Equals(typeNamespace, "System", StringComparison.Ordinal) + || typeNamespace?.StartsWith("System.", StringComparison.Ordinal) == true + || string.Equals(type.Assembly.GetName().Name, "mscorlib", StringComparison.Ordinal); } } }