Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to do this tricky down-casting with generic constraints?

I'm trying to downcast a controller instance inside an action filter, and I'm having issue doing so.

I have a DefaultController class:

public abstract partial class DefaultController<T> : Controller where T : IBaseEntity
{

}

IBaseEntity is:

public interface IBaseEntity
{
    int Id { get; set; }
    DateTime CreatedOn { get; set; }
    DateTime ModifiedOn { get; set; }
    int CreatedBy { get; set; }
    int ModifiedBy { get; set; }
    int OwnerId { get; set; }

}

I have an instance of a controller, inheriting the DefaultController:

public class WorkflowController : DefaultController<Workflow>
{
}

Workflow is inheriting BaseEntity which implements IBaseEntity.

Now, inside my action filter, code wise, it's impossible to know on which controller the request is running on so I'm trying to downcast it to DefaultController

public class AddHeaders : ActionFilterAttribute
{
    public override void OnActionExecuted(ActionExecutedContext filterContext)
    {

        var defaultControllerGenericType = controller.GetType().BaseType.GenericTypeArguments.FirstOrDefault();
        //this retrieve with which type DefaultController was initiated with...
        var controller = filterContext.Controller as DefaultController<BaseEntity>; 
        //this returns null...
        var controller2 = filterContext.Controller as DefaultController<IBaseEntity>;
        //this does so as well.

    }
}

I've tried using defaultControllerGenericType but I can't pass it anywhere, or at least I'm lacking the correct syntax.

Is there any way to do this ?

like image 545
Francis Ducharme Avatar asked Oct 01 '18 21:10

Francis Ducharme


2 Answers

It is instructive to understand why this is impossible.

public abstract partial class DefaultController<T> : Controller where T : IBaseEntity { ... }
public class WorkflowController : DefaultController<Workflow> { ... }

Here, let me rewrite that:

class Container { }
class Animal { }
class Fish : Animal { }
class Cage<T> : Container where T : Animal 
{ 
  public T Contents { get; set; }
}
class Aquarium : Cage<Fish> { }

The question now is, what happens when I have:

Aquarium a = new Aquarium(); // Fine
Cage<Fish> cf = a; // Fine
Container c = a; // Fine
Cage<Animal> ca1 = a; // Nope
Cage<Animal> ca2 = a as Cage<Animal>; // Null!

Why is this disallowed? Well, suppose it were allowed.

Cage<Animal> ca = a; // Suppose this is legal
Tiger t = new Tiger(); // Another kind of animal.
ca.Contents = t; // Cage<Animal>.Contents is of type Animal, so this is legal

And we just put a tiger into an aquarium. That's why this is illegal.

As Jon Skeet says, a bowl of apples is not a bowl of fruit. You can put a banana into a bowl of fruit, but you can't put a banana into a bowl of apples, and therefore they are different types!

like image 151
Eric Lippert Avatar answered Nov 10 '22 08:11

Eric Lippert


You can't do this with a class (i.e. DefaultController), but if you want to extract out the parts you need into an interface, then it can be made variant. Here's a covariant example:

public interface IEntityController<out T> where T : IBaseEntity
{

}

public abstract partial class DefaultController<T>: Controller, IEntityController<T> 
    where T : IBaseEntity
{

}

Use it like this:

    var controller = filterContext.Controller as IEntityController<IBaseEntity>; 

However, note that you'll be forced to decide whether your interface is covariant or contravariant: do you only have methods that take IBaseEntity values as parameters, or do you only have methods that return them? If you do both, then it simply isn't safe to assume that you can call those methods for subclasses of your default controller.

If, on the other hand, you don't even need the generic parameter in any of your method signatures, you could simplify things further and make your interface non-generic:

public interface IEntityController
{

}

public abstract partial class DefaultController<T>: Controller, IEntityController
    where T : IBaseEntity
{

}

...

    var controller = filterContext.Controller as IEntityController; 
like image 38
StriplingWarrior Avatar answered Nov 10 '22 09:11

StriplingWarrior