C# Fluent API With Dynamic Func<> Construction

穿精又带淫゛_ 提交于 2019-12-07 10:54:18

问题


I'm fooling around with creating a small SQL library with a fluent API and want to do something like this:

var person = connection.GetOne<Person>("select * from [Person] where [Id] = 1")
                       .WithMany<Pet>("select * from [Pet] where [PersonId] = 1")
                       .WithMany<Address>("select * from [Address] where [PersonId] = 1]")
                       .Build((person, pets, addresses) =>
                       {
                           person.Pets = pets;
                           person.Addresses = addresses;

                           return person;
                       });

I've built plenty of fluent API's before but all have been much more simple and did not rely on generics so heavily. My question is specifically how to go about implementing the Build() end function. I'm not sure if it's even possible (doesn't seem like it but maybe using Expression is the key?) but how can I keep track of the generic types specified in the calls to the higher chain methods (e.g. GetOne<>(), WithMany<>()) so that when the .Build() is called the Func<> that is required is of the correct types?

In the example above I'd want the Func<> to be Func<Person, IEnumerable<Pet>, IEnumerable<Address>> so that the developer can construct the root item (person) in whatever manner they need to - in this case, populating a couple collections with the results of one->many queries.

Is there any way to do this or am I out of luck? It seems like a lot of places I've looked to for similar things do the whole:

Func<In1, TResult>
Func<In1, In2, TResult>
Func<In1, In2, In3, TResult>
...etc, etc

...type of thing which obviously limits you to a max number of parameters for the function.

Any help or pointers would be much appreciated.


回答1:


If you want strong autocomplete and preventing someone from writing .Build(person => {}) when you're expecting (person, pet) => {}, you'll need to be verbose in your builder.

Here's an example for three levels deep:

class Person { public IEnumerable<Pet> Pets { get; set;} } class Pet {} class Address{}

public static class Builder
{
    public static Level1<T> GetOne<T>(this object obj, string blah) {
        return new Level1<T>();
    }
}
public class Level1<T1> {
    public Level2<T1, T2> WithMany<T2>(string blah) { return new Level2<T1, T2>(); }
    public T1 Build(Func<T1, T1> pred) { return pred(default(T1)); }
}
public class Level2<T1, T2>
{
    public Level3<T1, T2, T3> WithMany<T3>(string blah) { return new Level3<T1, T2, T3>(); }
    public T1 Build(Func<T1, IEnumerable<T2>, T1> pred) { return pred(default(T1), default(IEnumerable<T2>)); }
}
public class Level3<T1, T2, T3>
{
    public T1 Build(Func<T1, IEnumerable<T2>, IEnumerable<T3>, T1> pred) { 
        return pred(default(T1), default(IEnumerable<T2>), default(IEnumerable<T3>)); 
    }
}

Where we get strong typing here:

obj.GetOne<Person>("select * from [Person] where [Id] = 1")
.WithMany<Pet>("select * from [Pet] where [PersonId] = 1")
.WithMany<Address>("select * from [Address] where [PersonId] = 1]")
.Build((person, pets, addresses) => {
    person.Pets = pets;
    return person;
});

Or

obj.GetOne<Person>("select * from [Person] where [Id] = 1")
.WithMany<Pet>("select * from [Pet] where [PersonId] = 1")
.Build((person, pets) => { return person; });

As to your note about a restricted amount of parameters - that's correct. I don't believe there's anyway to get around that while keeping strong typing, unfortunately.




回答2:


It depends on how many methods you want to be able to add, but every should probably return another object of type T with the same methods. That requires you to repeat all functions for every number of type parameters you will have in your build function.

Maybe not an answer to your question, but you could forgo the static type checking and use something dynamic though:

public class Builder
{
    List<Type> _types = new List<Type>();
    List<object> _values = new List<Object>();
    public Builder GetOne<T>()
    {
        _types.Add(typeof(T));
        // Do stuff
        _values.Add(someObjectYouRetrieved);
        return this;
    }

    public T Build<T1, T>(Func<T1, T2, T> func) =>
        func(_values[0] as T1);

    public T Build<T1, T2, T>(Func<T1, T2, T> func) =>
        func(_values[0] as T1, _values[1] as T2);

    public T Build<T1, T2, T3, T>(Func<T1, T2, T3, T> func) =>
        func(_values[0] as T1, _values[1] as T2, _values[2] as T3);

   // Add more for the amount of type params you want to allow
}

This doesn't give you unlimited flexibility, but at least it prevents an explosion of methods.

It also doesn't give you static type checking, but since you're doing SQL, you don't have type safety in the first place.

It's a trade-off though.



来源:https://stackoverflow.com/questions/41029481/c-sharp-fluent-api-with-dynamic-func-construction

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!