Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Generic type inference, Fluent Api, with pre-declared types

I'm working on a Fluent Api for a service which is fairly configurable, and just trying to work my options for a neat solution for the following problem.

I have a class something like this

public class WindowVm : DialogResultBase<MyReturnType>

All well and good, However can any one think of a way to achieve the following without having to verbosely specify the second generic type given i.e

public IDialogWithResult<TViewModel, TSomeReturnType> DialogWithResult<TViewModel,TSomeReturnType>(object owner = null)
        where TViewModel : DialogResultBase<TSomeReturnType>

i really am just interest in the result IDialogWithResult<TViewModel, TSomeReturnType> even if i have to do this in 2 statements

So i can call

.DialogWithResult<WindowVm>()

I know all the information is there and declared at compile time, also i know this is Partial Inference and its all or nothing. However i just wondering if there is some trick without having to redeclare

.DialogWithResult<WindowVm, ResultType>(); 

Moreover i have a method that needs ResultType as (you guessed it) a result type

ResultType MyResult =  ...DialogWithResult<WindowVm, ResultType>()
                         .ShowModal(); 

I mean, ResultType is really just superfluous at this point in the game as its already been declared by WindowVm. it would be nice if the consumer didn't have to go looking for it (even if it meant more than one step)

like image 944
TheGeneral Avatar asked Feb 04 '18 05:02

TheGeneral


Video Answer


2 Answers

Yes, compiler has all the information to infer the type for TSomeReturnType when you pass WindowVm as TViewModel. But the main obstacle for allowing reduced argument list for generic (.DialogWithResult<WindowVm>()) is that it could conflict with overloaded method with the same name but just one generic type argument. For example if you have following methods in the class:

public IDialogWithResult<TViewModel, TSomeReturnType> DialogWithResult<TViewModel,TSomeReturnType>(object owner = null)
        where TViewModel : DialogResultBase<TSomeReturnType>

public IDialogWithResult<TViewModel> DialogWithResult<TViewModel>(object owner = null)
        where TViewModel : DialogResultBase<MyReturnType>

Which one should compiler call when you code .DialogWithResult<WindowVm>() ?

That's the reason why such simplified syntax will probably not be introduced in C#.

However you still have an option to make the calls as simple as .DialogWithResult<WindowVm>(). I'm not a fan of this solution but if brevity of your Fluent Api calls is important, you could use it. The solution is based on reflection and run-time extraction of TSomeReturnType type from passed TViewModel type:

public class YourClass
{
    public dynamic DialogWithResult<TViewModel>(object owner = null)
    {
        //  Searching for DialogResultBase<TSomeReturnType> in bases classes of TViewModel
        Type currType = typeof(TViewModel);
        while (currType != null && currType != typeof(DialogResultBase<>))
        {
            if (currType.IsGenericType && currType.GetGenericTypeDefinition() == typeof(DialogResultBase<>))
            {
                break;
            }

            currType = currType.BaseType;
        }
        if (currType == null)
        {
            throw new InvalidOperationException($"{typeof(TViewModel)} does not derive from {typeof(DialogResultBase<>)}");
        }

        Type returnValueType = currType.GetGenericArguments()[0];

        //  Now we know TViewModel and TSomeReturnType and can call DialogWithResult<TViewModel, TSomeReturnType>() via reflection.
        MethodInfo genericMethod = GetType().GetMethod(nameof(DialogWithResultGeneric));
        if (genericMethod == null)
        {
            throw new InvalidOperationException($"Failed to find {nameof(DialogWithResultGeneric)} method");
        }

        MethodInfo methodForCall = genericMethod.MakeGenericMethod(typeof(TViewModel), returnValueType);
        return methodForCall.Invoke(this, new [] { owner } );
    }

    public IDialogWithResult<TViewModel, TSomeReturnType> DialogWithResultGeneric<TViewModel, TSomeReturnType>(object owner = null)
        where TViewModel : DialogResultBase<TSomeReturnType>
    {
        // ...
    }
}

We declared new DialogWithResult<TViewModel>() method with just one generic type argument of TViewModel. Then we search for the base DialogResultBase<T> class. If found we extract type of TSomeReturnType with Type.GetGenericArguments() call. And finally call original DialogWithResultGeneric<TViewModel, TSomeReturnType> method via reflection. Note that I have renamed original method to DialogWithResultGeneric so that GetMethod() does not throw AmbiguousMatchException.

Now in your program you could call it as:

.DialogWithResult<WindowVm>()

The downside is that nothing prevents you from calling it on wrong type (the one does not inherit from DialogResultBase<T>):

.DialogWithResult<object>()

You won't get compilation error in this case. The problem will be identified only during run-time when exception will be thrown. You could fix this issue with a technique described in this answer. In short, you should declare non-generic DialogResultBase and set it as the base for DialogResultBase<T>:

public abstract class DialogResultBase
{
}

public class DialogResultBase<T> : DialogResultBase
{
    //  ...
}

Now you could add constraint on DialogWithResult<TViewModel>() type parameter:

public dynamic DialogWithResult<TViewModel>(object owner = null)
    where TViewModel : DialogResultBase

Now .DialogWithResult<object>() will cause compilation error.

Again, I'm not a big fan of solution that I proposed. However you can't achieve what you're asking for with just C# capabilities.

like image 78
CodeFuller Avatar answered Sep 26 '22 08:09

CodeFuller


As both you and @CodeFuller observed, partial inference isn't possible in C#.

If you're looking for something less evil than going dynamic, you can use a combination of extension methods and custom classes to get the types you need without ever referring directly to the return type.

In the example below, I use an extension method on DialogResultBase<T> to infer the return type and then I return a helper class containing a generic method for DialogWithResult<WindowVm>.

Still not pretty, but roughly fits what you asked for.

Interesting point about inference. Each parameter can only be used to infer a single type. If you were to pass the same parameter multiple times, you can infer multiple types from it. i.e. if you pass the same parameter to both parameters in (T myList, List<TItem> myListAgain) you could infer both the list type and the item type.

public class Class2
{
    public static void DoStuff()
    {
        var dialogResult = default(WindowVm).GetReturnType().DialogWithResult<WindowVm>();

    }

}


public class MyReturnType { }
public class DialogResultBase<T> : IDialogWithResult<T> { }
public interface IDialogWithResult<TSomeReturnType> { }

public class WindowVm : DialogResultBase<MyReturnType> { }

public class DialogResultHelper<TSomeReturnType>
{
    public IDialogWithResult<TSomeReturnType> DialogWithResult<TViewModel>() where TViewModel : DialogResultBase<TSomeReturnType>, new()
    {
        return new TViewModel();
    }
}

public static class Extensions
{
    public static DialogResultHelper<T> GetReturnType<T>(this DialogResultBase<T> dialogResultBase)
    {
        return new DialogResultHelper<T>();
    }

}
like image 36
Grax32 Avatar answered Sep 26 '22 08:09

Grax32