Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>().WithServiceBase()` to explicitly register a framework interface.

It supports `DependsOn` for keyed services:

```csharp
Expand Down
Original file line number Diff line number Diff line change
@@ -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<DisposableService>()
.BasedOn<IApplicationContract>().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<DisposableService>()
.BasedOn<IAsyncDisposable>().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<DisposableService>()
.BasedOn<IAsyncDisposable>().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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ public interface IServiceSelector
/// </summary>
/// <returns>The lifestyle selector.</returns>
/// <remarks>
/// 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.
/// </remarks>
ILifestyleSelector WithServiceAllInterfaces();

Expand Down
10 changes: 5 additions & 5 deletions src/Mammoth.Extensions.DependencyInjection/TypeExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,16 @@ public static class TypeExtensions
{
/// <summary>
/// <para>Determines whether the specified type is a framework type.</para>
/// <para>A type is considered a framework type if it is defined in the System namespace or if it is defined in the mscorlib assembly.</para>
/// <para>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.</para>
/// </summary>
/// <param name="type">The type to check.</param>
/// <returns><c>true</c> if the specified type is a framework type; otherwise, <c>false</c>.</returns>
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);
}
}
}