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
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="Spectre.Console.Cli" Version="0.50.0" />
<PackageReference Include="xunit" Version="2.7.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.7" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
Expand Down
55 changes: 55 additions & 0 deletions src/DependencyInjection.Tests/Regressions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Moq;
using Spectre.Console.Cli;

namespace Tests.Regressions;

public class Regressions
{
[Fact]
public void CovariantRegistrationSatisfiesIntefaceConstraints()
{
var collection = new ServiceCollection();
collection.AddServices(typeof(ICommand));

var provider = collection.BuildServiceProvider();

var command = provider.GetRequiredService<MyCommand>();

Assert.Equal(0, command.Execute(new CommandContext([], Mock.Of<IRemainingArguments>(), "my", null),
new MySetting { Base = "", Name = "" }));
}
}

public interface ISetting
{
string Name { get; set; }
}

public class BaseSetting : CommandSettings
{
[CommandArgument(0, "<BASE>")]
public required string Base { get; init; }
}

public class MySetting : BaseSetting, ISetting
{
[CommandOption("--name")]
public required string Name { get; set; }
}

public class MyCommand : BaseCommand<MySetting> { }

public abstract class BaseCommand<TSettings> : Command<TSettings> where TSettings : BaseSetting, ISetting
{
public override int Execute(CommandContext context, TSettings settings)
{
Console.WriteLine($"Base: {settings.Base}, Name: {settings.Name}");
return 0;
}
}
60 changes: 60 additions & 0 deletions src/DependencyInjection/ConstraintsChecker.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection.Metadata;
using System.Text;
using Microsoft.CodeAnalysis;

namespace Devlooped.Extensions.DependencyInjection;

static class ConstraintsChecker
{
public static bool SatisfiesConstraints(this ITypeSymbol typeArgument, ITypeParameterSymbol typeParameter)
{
// Check reference type constraint
if (typeParameter.HasReferenceTypeConstraint && !typeArgument.IsReferenceType)
return false;

// Check value type constraint
if (typeParameter.HasValueTypeConstraint && !typeArgument.IsValueType)
return false;

// Check base class and interface constraints
foreach (var constraint in typeParameter.ConstraintTypes)
{
if (constraint.TypeKind == TypeKind.Class)
{
if (!typeArgument.GetBaseTypes().Any(baseType => SymbolEqualityComparer.Default.Equals(baseType, constraint)))
return false;
}
else if (constraint.TypeKind == TypeKind.Interface)
{
if (!typeArgument.AllInterfaces.Any(interfaceSymbol => SymbolEqualityComparer.Default.Equals(interfaceSymbol, constraint)))
return false;
}
}

// Constructor constraint (optional, not typically needed here)
if (typeParameter.HasConstructorConstraint)
{
// Check for parameterless constructor (simplified)
var hasParameterlessConstructor = typeArgument.GetMembers(".ctor")
.OfType<IMethodSymbol>()
.Any(ctor => ctor.Parameters.Length == 0);
if (!hasParameterlessConstructor)
return false;
}

return true;
}

static IEnumerable<ITypeSymbol> GetBaseTypes(this ITypeSymbol typeSymbol)
{
var currentType = typeSymbol.BaseType;
while (currentType != null && currentType.SpecialType != SpecialType.System_Object)
{
yield return currentType;
currentType = currentType.BaseType;
}
}
}
9 changes: 7 additions & 2 deletions src/DependencyInjection/IncrementalGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,9 @@ void AddServices(IEnumerable<INamedTypeSymbol> services, Compilation compilation

foreach (var iface in type.AllInterfaces)
{
if (!compilation.HasImplicitConversion(type, iface))
continue;

var ifaceName = iface.ToFullName(compilation);
if (!registered.Contains(ifaceName))
{
Expand All @@ -476,9 +479,11 @@ void AddServices(IEnumerable<INamedTypeSymbol> services, Compilation compilation
baseType = baseType.BaseType;
}

foreach (var candidate in candidates.Select(x => iface.ConstructedFrom.Construct(x))
foreach (var candidate in candidates
.Where(x => x.SatisfiesConstraints(iface.TypeParameters[0]))
.Select(x => iface.ConstructedFrom.Construct(x))
.Where(x => x != null && compilation.HasImplicitConversion(type, x))
.ToImmutableHashSet(SymbolEqualityComparer.Default)
.Where(x => x != null)
.Select(x => x!.ToFullName(compilation)))
{
if (!registered.Contains(candidate))
Expand Down