Mike Marynowski
Mike Marynowski

Reputation: 3429

Fluent Interface for Generic Type Hierarchy

I believe this is the only way to achieve what I want but I wanted to put this out there to see if there's a solution that doesn't require using dynamic/reflection.

I have the following hierarchy of types, stripped to the bare essentials to demonstrate the point:

// Validators:

public abstract class Validator<T> { }

public class RequiredValidator<T> : Validator<T> { }

// Fields:

public abstract class Field { }

public abstract class Field<T> : Field
{
    public void AddValidator(Validator<T> validator) => 
        Console.WriteLine($"Added validator {validator.GetType()}");
}

public sealed class ValueField<T> : Field<T> { }
public sealed class ComputedField<T> : Field<T> { }
...many other field types that inherit Field<T>

This is an example usage of the fluent interface I want to achieve:

ValueField<string> field1 = new ValueField<string>().Required();

The Required() method must be available on all types that inherit Field<T>.

This is what I've come up with:

public static class Extensions
{
    public static TField Required<TField, T>(this TField field) where TField : Field<T>
    {
        field.AddValidator(new RequiredValidator<T>());
        return field;
    }

    public static TField DynamicRequired<TField>(this TField field) where TField : Field
    {
        DynamicAddRequiredValidator((dynamic)field);
        return field;
    }

    private static void DynamicAddRequiredValidator<T>(Field<T> field)
    {
        field.AddValidator(new RequiredValidator<T>());
    }
}

void Main()
{   
    // This is desired API usage but results in error:
    // The type arguments for method 'Extensions.Required<TField,T>(TField)' cannot be inferred from the usage.
    ValueField<string> field1 = new ValueField<string>().Required();

    // This works but the user shouldn't have to specify types like this, makes it very annoying to use:
    ValueField<string> field2 = new ValueField<string>().Required<ValueField<string>, string>();

    // This works but requires dynamic:
    ValueField<string> field3 = new ValueField<string>().DynamicRequired();
}

Am I missing a way of achieving this which avoids using dynamic based code?

Upvotes: 2

Views: 526

Answers (2)

Ondřej Fischer
Ondřej Fischer

Reputation: 411

For "extendable" fluent interface, we use following trick in Java (you can try, if it's possible in C# too):

public class Field<L extends Field<L, V>, V> {
    public L required() {
        //...
        return (L) this;
    }
}

public class ValueField<V> extends Field<ValueField<V>, V> {
}

Now you can call what you need:

ValueField<String> v = new ValueField<String>().required();

It's thanks to additional type parameter of Field which delegates specific return type of the fluent methods to children.

Upvotes: 0

Michael Puckett II
Michael Puckett II

Reputation: 6749

Generics in C# are an all or nothing. You either pass all, like you've done, or none. It must be designed in a way that all arguments can be inferred. For what you're doing you can just use Field<T> instead of TField<T>, removing that generic type parameter; although it may not be as ideal. There are other ways... Some FLUENT designs return new types that contain the generics as properties allowing you to move forward but your continuation would need the logic of using that continue type as well. It gets a bit confusing but I feel you understand. If not let me know.

It would be nice if the where constraint could also help infer the types but it doesn't. Eric Lippert recently helped me understand that C# tries to infer the generic parameters only and if that can't be inferred it fails. The where constraint is only to limit the generic type to a base and inform developers. Although it does feel like we could also infer based on constraints, since we are basing the types, C# doesn't. Eric has an opinion about not doing so, which I'm sure is more than I understand ATM. Either way, there you have it.

Upvotes: 2

Related Questions