khellang/Scrutor

[Question] Decoration of closed interface over generic parent

Closed this issue · 2 comments

Hello. Apologies for a rethread of what appears to be a beaten question, but i'm a bit lost and existing issues don't quite fit

Suppose we have a following hierarchy:

// Base handler interface
public interface IHandler<TReq, TRes> { /* snip */ }
// This one is supposed to be used as shorthand  in controllers and other entry points
public interface ISomeHandler : IHandler<string, int> { /* snip */ } 
// Implementation
public class SomeHandler : ISomeHandler { /* snip */ }
// Decorator
public class Decorator<TReq, TRes> : IHandler<TReq, TRes> { /* snip */ }
// Example controller signature
public Task<ActionResult<SomeResult>> Test(ISomeHandler handler, SomeRequest request) { /* snip */ }

Base configuration:

// For controllers both folloving version work, as expected
//  - Test(ISomeHandler handler, string input)          -> Works
//  - Test(IHandler<string, int> handler, string input) -> Works
builder.Services.Scan(scan =>
{
    scan.FromAssembliesOf(typeof(IHandler<,>))
        .AddClasses(c => c.Where(t => t != typeof(Decorator<,>)))
        .AsSelfWithInterfaces().WithScopedLifetime(); 
    // AsImplementedInterfaces would work as well
});

Now for the main problem: how do i decorate intermediate closed interface (ISomeHandler)?

// Attempt 1
//  - Test(ISomeHandler handler, string input)          -> Does not work (yields SomeHandler directly)
//  - Test(IHandler<string, int> handler, string input) -> Works (yields decorator instance)
builder.Services.Decorate(typeof(IHandler<,>), typeof(Decorator<,>));
// Attempt 2
// Blows up at runtime: A suitable constructor for type 'ScrutorTest.Decorator`2[TReq,TRes]' could not be located
//builder.Services.Decorate(typeof(ISomeHandler), typeof(Decorator<,>));
// Or
//builder.Services.Decorate(typeof(SomeHandler), typeof(Decorator<,>));
// Attempt 3
// Blows up at runtime: Unable to cast object of type 'ScrutorTest.Decorator`2[System.String,System.Int32]' to type 'ScrutorTest.ISomeHandler'
//builder.Services.Decorate(typeof(ISomeHandler), typeof(Decorator<string, int>));
// Or
//builder.Services.Decorate(typeof(SomeHandler), typeof(Decorator<string, int>));

Is what i'm trying to do even possible with C# generics?
If it's possible with Scrutor, what am i missing?
If not possible with Scrutor + base .NET DI, would it be possible with other container, like Autofac? (I'm going to try that anyway in meantime)

Minimal repro:
ScrutorTest.zip

Hello @schmellow! 👋🏻

It's definitely a case that makes my head hurt a bit 😅

For the decorator pattern to work, the decorator needs to implement the same interface as it's accepting in its constructor, e.g.

public class Decorator<TReq, TRes> : IHandler<TReq, TRes>
{
    public Decorator(IHandler<TReq, TRes> inner) { /* snip */ }
}

or

public class Decorator : ISomeHandler
{
    public Decorator(ISomeHandler inner) { /* snip */ }
}

I don't see how you could decorate ISomeHandler using Decorator<string, int> as Decorator<string, int> doesn't implement the ISomeHandler interface. Sure, they both have a common interface IHandler<string, int>, but in order to get that to work, you'd need something like structural typing in C#.

This definitelly smells like a case where I'd take a step or two back and question what I'm really trying to accomplish. Maybe there's a simple(r) solution? 🤔

Turns out you actually test against this case in DecoratingOpenGenericTypeBasedOnGrandparentInterfaceDoesNotDecorateParentInterface :)

Anyway...

This definitelly smells like a case where I'd take a step or two back and question what I'm really trying to accomplish. Maybe there's a simple(r) solution?

I think you are right. After trying out different options with containers and whatnot - it's just not possible OOTB.

It's kinda like "colored functions" - there is an impasse betwen generic and non-generic context you can't cross without reflection trickery

I can think of a solution through factory + reflection, but it's just impractical at this point. Which points to the fact, that this does not belong on a DI container level.

What actually fits my specific case is external dispatcher + pipeline (like asp.net does with middleware chains or MediatR).

So closing