Search code examples
c#asp.netasp.net-corerazor

Razor Pages - Passing abstract class to Html.Partial expecting an interface throws an error


I have:

  1. An interface with 2 generics
  2. An abstract class that implements the interface and PageModel
  3. A class for the razor page that implements the abstract class
  4. A partial that expects the interface

When I try @await Html.PartialAsync("_InterfacePartial", Model) it throws the below

InvalidOperationException: The model item passed into the ViewDataDictionary is of type 'IndexPageModel', but this ViewDataDictionary instance requires a model item of type 'IPage`2[System.Object,IRow]'.

Microsoft.AspNetCore.Mvc.ViewFeatures.ViewDataDictionary.EnsureCompatible(object value)

IPage.cs

public interface IPage<T, T2>
    where T : class
    where T2 : IRow
{
    // STRIPPED FOR BREVITY

    IQueryable<T> GetQueryable();

    IEnumerable<T2> GetResults(Func<T, bool> predicate);
}

AbstractPage.cs

public abstract class AbstractPage<T, T2> : PageModel, IPage<T, T2>
    where T : class
    where T2 : IRow
{
    // STRIPPED FOR BREVITY

    public abstract IQueryable<T> GetQueryable();

    public IEnumerable<T2> GetResults(Func<T, bool> predicate)
    {
        var fields = ... // Edit after answer => `var fields = GetTableFields();` returns Func<T, T2>

        return GetQueryable().Where(predicate).ToList().Select(fields);
    }
}

IndexPageModel.cshtml.cs

public class IndexPageModel : AbstractPage<Thing, ThingRow>
{
    public override IQueryable<Order> GetQueryable()
    {
        return MyDbContext.Things.Include(x => x.AnExtraThing);
    }
}

Am I doing something wrong or am I just a mad man trying to do the impossible?


Solution

  • Your problem is related to some concepts: assignment compatibility, covariance and contravariance.

    Covariance preserves assignment compatibility whereas contravariance reverses assign compatibility. You can learn more about that here Covariance & Contravariance

    So in this case, your page's declared model type is an interface with generic arguments but to support accepting (assignment) value from some instance with arguments of more derived (more concrete) type, you need to support covariance for that interface type by using the keyword out, as follows:

    public interface IPage<out T, out T2>
    where T : class
    where T2 : IRow
    { ... }
    

    That luckily matches with the return types used in the methods of your interface. So you can see that both T and T2 are returned types. If one of them is an argument type, you will be stuck and should consider about your design of the interface. Here is an example of the interface that cannot be declared to support covariance:

    //has compile-time error
    public interface IPage<out T, out T2>
    where T : class
    where T2 : IRow
    {    
       IQueryable<T> GetQueryable();
       IEnumerable<T2> GetResults(Func<T, bool> predicate);
       //just an example, this will violate the covariance rule
       //because T2 cannot be used as an input (argument) type once declared with out
       int ComputeSomeThing(T2 row);
    }
    

    So behind the scene, your page model will be assigned to IndexPageModel OK like this (for demonstrative purpose only, not exactly what happens):

    IPage<object,IRow> model = new IndexPageModel(...);
    

    UPDATE:

    About a more complicated scenario in which you have the generic type T not used directly as argument type or return type but instead in another generic type that also has variant generic types, such as Func. Here is an example of such complicated example:

    public interface IComplicatedVariant<in T, out TResult> {
         Func<T, TResult> GetFunc();
         void SetFunc(Func<TResult, T> f);
    }
    

    As you can see, the return type Func<T, TResult> accepts generic argument types T (contravariant) and TResult (covariant) of the same variance (like cascading down from the type IComplicatedVariant). However when using an argument type of Func, we need to reverse the variance of generic types passed in Func. So instead of Func<T,TResult> (which is invalid and will have design-time error), we can only pass Func<TResult,T> or in short, T cannot be used in Func<T, ...> and TResult cannot be used in Func<...,TResult>.

    The second method SetFunc is harder to be explained without concrete example. The generic arguments' variance seems to be reversed. Suppose T can be used in Func<T,...> for SetFunc, we have:

    //T1, T2 here are concrete types, not generic argument types
    var a = new ComplicatedVariant<object,T2>();
    IComplicatedVariant<T1, T2> o = a;
    Func<T1,T2> f = ...;
    //OK
    o.SetFunc(f);
    //BUT this is not OK
    a.SetFunc(f);
    

    The above example is an assumption to show that what will become wrong without reversing the variance (as mentioned above). You can see that a.SetFunc requires a Func<object,T2> but the passed-in argument is a Func<T1,T2>. The first argument type in Func<,> is contravariant (declared with in T), so it reverses the assignment compatibility, which means the first generic argument type on the left side should be equal or more derived than the type on the right side. So we cannot assign the func like this (pseudo-code):

    //cannot be set like this, the reverse is however OK
    Func<object,T2> = Func<T1,T2>
    

    The argument cannot be accepted (in the call a.SetFunc) meaning it violates the variance designed for the interface IComplicatedVariant. So our original assumption is invalid. As I said, we use a concrete example to show an invalid case (by assumption). It's fairly complicated but luckily that we have the design-time compiler which can help report the error right at the design time. If you're not sure, you can just try it out and see if it works right at the design time.