2

I'm trying to create a Fluent API that describes a nested structure and as a result I would like to use generics to type the resulting object. I would like to do something like:

var test = new Query<Entity>().Select(x => x.UniqueValue<string>())
                              .Select(x => x.UniqueValue<DateTime>())
                              .Select(x => x.UniqueValue<decimal>());

Where test would be of type:

Result<Entity, string, Result<Entity, DateTime, Result<Entity, decimal>>>

So far I have code like:

   class Program
{
    static void Main(string[] args)
    {
        // test : ResultWithResult<Entity,string, Result<Entity, DateTime, ChildLessResult<Entity, decimal>>>
        var test = new Query<Entity>().Select(x => x.UniqueValue<string>())
                                      .Select(x => x.UniqueValue<DateTime>())
                                      .Select(x => x.UniqueValue<decimal>());
    }
}

public class Entity
{
    public string Item1 { get; set; }
}

public class Aggregate<TEntity>
{
    public ChildLessResult<TEntity, TReturnValue> UniqueValue<TReturnValue>()
    {
        return new ChildLessResult<TEntity, TReturnValue>();
    }
}

public class Query<TEntity>
{
    public ChildLessResult<TEntity, TReturnValue> Select<TReturnValue>(Func<Aggregate<TEntity>, ChildLessResult<TEntity, TReturnValue>> predicate)
    {
        var aggregator = new Aggregate<TEntity>();
        return predicate(aggregator);
    }
}

public class ChildLessResult<TEntity, TReturnValue>
{
    public Result<TEntity, TReturnValue, TChild> Select<TChild>(Func<Aggregate<TEntity>, ChildLessResult<TEntity, TChild>> predicate)
    {
        return new Result<TEntity, TReturnValue, TChild>();
    }
}

public class Result<TEntity, TReturnValue, TChild>
{
    private ChildLessResult<TEntity, TChild> _child = new ChildLessResult<TEntity, TChild>();

    public ResultWithResult<TEntity, TReturnValue, Result<TEntity, TChild, ChildLessResult<TEntity, TNewChild>>> Select<TNewChild>(Func<Aggregate<TEntity>, ChildLessResult<TEntity, TNewChild>> predicate)
    {
        return new ResultWithResult<TEntity, TReturnValue,
            Result<TEntity, TChild, ChildLessResult<TEntity, TNewChild>>>();
    }
}

public class ResultWithResult<TEntity, TReturnValue, TChild>
{
}

As you can see this is pretty limited since it only allows you to go as deep as the structures you have defined and you would need a new structure for each level deeper. Is it possible to create types that would allow an unlimited number of levels (i.e. as many Selects as you would ever want without the same corresponding number of Types).

colethecoder
  • 1,139
  • 2
  • 8
  • 26
  • 1
    I am not entirely sure what your use case is. Are you trying to make a recursive tree-like structure? Why does the type parameter of `Query` require `new()`? And why does the `Select` function of each type return a different type? If you can make a type where `Select` returns the same type with a different generic argument, you can get a fluent syntax much easier with less code. What is the structure of ` Query` or `Aggregate` instance, they have no fields. – JamesFaix Apr 10 '18 at 23:23
  • @JamesFaix I'm creating a structure that then generates a JSON query for an external system before parsing the results back to C# objects, both the query and the results would be nested in the same way. There are other parts to the query which are added fluently too (Where etc) which I have left out to try and simplify the particular problem. – colethecoder Apr 10 '18 at 23:29
  • Have you looked at Newtonsoft? – JamesFaix Apr 10 '18 at 23:29
  • @JamesFaix the project is a wrapper to simplify API access, it uses Newtonsoft behind the scenes. My question is around the limitations of generating nested generics in C#. – colethecoder Apr 10 '18 at 23:33
  • I think what you may want is some form of "higher kinded types" as they're known in the Haskell/Scala world. .NET kind of makes those impossible, in any .NET language, because generics are baked into the Common Intermediate Language. You might be able to write your library with less internal boilerplate in F# due to better type inference, but I'm not sure if you can do what you're really looking for. The most maintainable route may be to simplify/flatten your design. You could also use T4 or some code generation mechanism to automate writing many similar classes. – JamesFaix Apr 10 '18 at 23:40
  • If there is any way you can express what you want such that a "doubly nested" type is just a "singly nested" type with another "singly nested" type as its type parameter, that is what I would recommend. – JamesFaix Apr 10 '18 at 23:42

1 Answers1

1

Yes, sort of.... it is possible to create some very long and deep nested types, more or less automatically. The trick is you need to use generic type inference, and since constructors don't do inference, you have to use a factory method. For example:

public class MyType<TData,TChild>
{
    public MyType(TData data, TChild child)
    {
    }
}

public class MyTypeFactory
{
   public static MyType<TData,TChild> Create<TData,TChild>(TData data, TChild child)
   {
      return new MyType<TData,TChild>(data, child);
   }
    
   public static MyType<TData,object> Create<TData>(TData data)
   {
      return new MyType<TData,object>(data, null);
   }
}

public static class Program
{
    static public void Main()
    {
        var grandchild  = MyTypeFactory.Create( 12 );
        var child       = MyTypeFactory.Create( 13D, grandchild );
        var parent      = MyTypeFactory.Create( 14M, child) ;
        var grandparent = MyTypeFactory.Create( 15F, parent );
        
        Console.WriteLine(  grandchild.GetType().FullName );
        Console.WriteLine(       child.GetType().FullName );
        Console.WriteLine(      parent.GetType().FullName );
        Console.WriteLine( grandparent.GetType().FullName );
    }
}

The output:

MyType'2[[System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.Object, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]

MyType'2[[System.Double, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[MyType'2[[System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.Object, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], uy2zelya, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null]]

MyType'2[[System.Decimal, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[MyType'2[[System.Double, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[MyType'2[[System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.Object, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], uy2zelya, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null]], uy2zelya, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null]]

MyType'2[[System.Single, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[MyType'2[[System.Decimal, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[MyType'2[[System.Double, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[MyType'2[[System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.Object, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], uy2zelya, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null]], uy2zelya, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null]], uy2zelya, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null]]

It's even type-safe, amazingly enough. If you add the necessary properties...

public class MyType<TData,TChild>
{
    protected readonly TChild _child;
    protected readonly TData _data;
    
    public MyType(TData data, TChild child)
    {
        _data = data;
        _child = child;
    }
    
    public TChild Child
    {
        get
        {
            return _child;
        }
    }
    
    public TData Data
    {
        get
        {
            return _data;
        }
    }
}

...this will work:

var grandchildValue = grandparent.Child.Child.Child.Data;
Console.WriteLine(grandchildValue); //12
Console.WriteLine(grandchildValue.GetType().Name); //int32

And of course you can use the builder pattern:

public static class ExtensionMethods
{
    static public MyType<TDataAdd,MyType<TDataIn,TResultIn>> AddColumn<TDataIn,TResultIn,TDataAdd>(this MyType<TDataIn,TResultIn> This, TDataAdd columnValue)
    {
        return MyTypeFactory.Create(columnValue, This);
    }
}

var grandparent = MyTypeFactory.Create( 12 )
    .AddColumn( 13D )
    .AddColumn( 14M )
    .AddColumn( 15F );

var grandchildValue = grandparent.Child.Child.Child.Data;
Console.WriteLine(grandchildValue);                       //12
Console.WriteLine(grandchildValue.GetType().FullName);    //System.Int32

See my code on DotNetFiddle

P.S. This is a great example of why we needed the var keyword! Otherwise you'd be typing forever.

Community
  • 1
  • 1
John Wu
  • 50,556
  • 8
  • 44
  • 80
  • 1
    grandparent.Child.Child.Child.Data brings https://stackoverflow.com/questions/35925357/what-is-law-of-demeter to mind – Emond Apr 11 '18 at 05:41
  • Thanks for the response, the problem I have is I want the hierarchy the opposite way around so in your builder example grandchildValue would be 15F in my ideal solution (and grandparent.Data == 12). So each additional AddColumn would be nested at the bottom of the type hierarchy. – colethecoder Apr 11 '18 at 08:14