pyeve / Eve.NET

Portable HTTP and REST Client for Web Services powered by the Eve Framework
http://python-eve.org
Other
21 stars 5 forks source link

Fluent API #6

Open nicolaiarocci opened 6 years ago

nicolaiarocci commented 6 years ago

Would be nice to have a fluent API, whereas now we are just passing arguments to the GET method. I guess what I have in mind is something like client.Get(url).Where(customer => customer.Name=='John').Sort(...).

That's going to be quite a lot of work, and I am not sure it is really worth it. Also, I don't have the time to work on this right now. Up for grabs!

pedro2555 commented 6 years ago

Mongo driver for .NET implements such a thing, by the name of Builders or DefinitionBuilder (depending where you're reading).

Specifically the FilterDefinitionBuilder seems to to do exactly as you suggest. Not really sure how one could leverage that for this project, but figured it might be helpful when someone does it.

MongoDB.Driver/FilterDefinitionBuilder.cs

Disclaimer, I've never used the mongodb driver for windows, it just seemed obvious it had this.

nicolaiarocci commented 6 years ago

screen shot 2018-04-18 at 14 56 40

Making progress.

pedro2555 commented 6 years ago

Just gave a read to the fluent_api branch, and I'm a bit confused about what your trying to achieve exactly. And maybe you're too.

The method chaining syntax one usually sees goes something like Get<T>().Where("_updated").GreaterThan(DateTime.Now).Asc();

But I believe what you're trying to achieve is different, you want proper C# like query syntax. Get<T>().Where(x => x._updated > DateTime.Now).Sort();

You might be mixing the two. The former is easier to get right.

nicolaiarocci commented 6 years ago

What I really want to achieve is clear separation between Eve internals and the client API. In your first example, you are passing "_updated" which is an internal json/mongo field name. Most likely your DTO class is exposing an Updated property, and the client user should not know about the underlying data structure. Right now, if you want to perform a filter-like query you have to pass a raw eve query string to one of the GetAsync overloads. I want that to go away (or stay, but reserved for advanced use cases). Expressing a query in C#/LINQ terms is ideal in my opinion.

I am not a fan of "very fluent" queries like the one in your first example. They soon get very long, hence hard to parse (at least for me). Simply exposing a Where method to which you pass a lambda with your search criteria sounds more concise and appropriate to me. Take the example in the test above: it is immediately clear that I am looking for something which has been modified recently. The intent is very clear. Then I can go into the When and parse what exactly I am looking for.

Right now, with the current API, achieving something like that would mean passing raw, Eve-understandable strings to the GetAsync method, which is very limiting if, say, the client developer knows nothing about the server internals (which is my use case, by the way).

pedro2555 commented 6 years ago

Yes, _updated was exactly the wrong field to discuss this, but I think we can ignore that it is that field for this matter.

But, I guess we're in sync on the goal.

I am not a big fan of the "very fluent" queries either, but the truth is they are very good for this kind of thing, for a number of good reasons. Where expressions will always leave you short.

A quick example I can come up that lambda expressions won't make it is mongo geo queries, but there are plenty of others I'm sure.

The problem I originally noticed in your example, is that your kinda mixing the APIs together.

Get<T>()
    .Where(x => (x.StateOrProvince != "tag" || x.Name == "nik") && x.Password == "pw")
    .IfModifiedSince(date)
    .IncludeDeleted()
    .BuildQuery();

is really not much different to:

var a = new Get<T>() {
    Where = new Expression(x => (x.StateOrProvince != "tag" || x.Name == "nik") && x.Password == "pw")),
    IfModifiedSince = DateTime.Now,
    IncludeDeleted = True
};
a.Execute();

Parsing

As for parsing the "very fluent" APIs, not sure I'm following you on that one.

I'm not going to be able to explain an implementation of such API in this post, but to put it quickly I would define a set of interfaces (that would basically enforce static semantics, like after Where() call only Equal(), NotEqual(), etc).

All those methods just have to return this with a type cast to the correct interface, and your class should implement them all.

I have to disagree when you say:

the client user should not know about the underlying data structure

I say different, if the client is filtering data he should be very aware of the data structure he is querying, otherwise how would he build the query in the first place? I've worked SQL database for years, the first thing you do is a select top 1 to see the columns, then you build your amazing unreadable query.

--

One improvement would be not using strings to pass fields, but rather use a more strongly typed approach, like:

Get<T>().Where(x => x.SomeField).GreaterThan(DateTime.Now).Asc();

Do note:

nicolaiarocci commented 6 years ago

What I meant by parsing, I guess, is "readable/processable" to the eye. You have a good point on complex queries (geo, etc.). "Client not knowing underlying data structure": I mean eve internals. Like, I do not want the client programmer to be forced to use _myfield if the DTO class has a MyField property tagged with the JsonProperty attribute, that sort of thing (but I think we already agreed on this one).

On the interfaces yes, right now I don't have them in place but once things get more complex they should probably be there.

Keep in mind, this is just an experiment at the time being, and this discussion is exactly what I was looking forward to.

pedro2555 commented 6 years ago

About the readable part, I get it. But I was making a point when I said

amazing unreadable query

database queries end up being amazingly unreadable. Thats not to say one should not improve the situation.

And yes, the Eve internals are Eve internals, and should be dealt with internally. The point I was actually subtly making is that .IfModifiedSince() simply leverages a .Where("_updated")

Keep in mind, this is just an experiment at the time being, and this discussion is exactly what I was looking forward to.

Love it, never done this sort of query chaining thing, but always seemed a fun challenge.

pedro2555 commented 6 years ago

for some thoughts https://nosqlbooster.com/FluentQueryAPI

Just gave a better read, looks like a fantastic guideline/starting point, what do you think?

And actually the syntax you propose is not that difficult to be leveraged in the API I propose as syntax sugaring.

nicolaiarocci commented 6 years ago

I'm looking at how the mongdb driver addresses 'fluent' (linq), and it looks they went more less the same direction as I did in my first experiment: http://mongodb.github.io/mongo-csharp-driver/2.5/reference/driver/crud/linq/

pedro2555 commented 6 years ago

Interesting.

But they didn't solve the geo queries issue I've pointed out; it's just left out.

And they still have the weird reflection hacking I've mentioned to get the properties names rather than values: as I've said there's no way to do it statically. When they have (p => new { p.Name, p.Age }), that new keyword just creates an instance (probably Name and Age are null, not sure on that), but the point is that the instance is just used to grab the property names, which are static; that doesn't look clean to me, hence the string solution. Let me point out I see this as just a very minor thing, but I still think one should know about it. One more note on this, Eve allows unkown fields, with a strongly typed approach that would also be left out.

I think we have 3 proposals on hand:

// loosely typed
Get<T>().Where("born").GreaterThan(DateTime.Now).Asc();

// strongly typed
Get<T>().Where(t => t.Born).GreaterThan(DateTime.Now).Asc();

// expression like
Get<T>().Where(t => t.Born > DateTime.Now).Asc();

Both strongly type and expression like syntax have their appreciable advantages from a developer perspective, and see no reason why they can't be leverage onto the loosely type syntax. Its just 3 different overloads on the Where() method.

Probably some signature like:

// loosely typed
ICallSomething Where(string property);
//    magic happens

// strongly typed
ICallSomethingElse Where(Func<TSource, TKey> property);
//    return Where(nameof(TSource.TKey))

// expression like
ICallSomethingElse Where(Expression expression);
//    good luck.. :)
//    and probably return Where(nameof(LeftTSource.LeftTKey).Equal(RightValue)

I'm sort of on a chalenge to code only on an Android tablet, and I'm not getting much luck with mono on Android. Thats my struggle right now to start coding on this.

pedro2555 commented 6 years ago

To be honest, the difficult part in leveraging those 3 looks to be enforced semantics with interfaces, which is something I find very nice.