Skip to content
This repository has been archived by the owner on Jan 24, 2021. It is now read-only.

Introduced new route declaration syntax #2441

Merged
merged 1 commit into from May 19, 2016

Conversation

thecodejunkie
Copy link
Member

@thecodejunkie thecodejunkie commented May 11, 2016

Prerequisites

  • I have written a descriptive pull-request title
  • I have verified that there are no overlapping pull-requests open
  • I have verified made sure that I am following the Nancy code style guidelines
  • I have provided test coverage for your change (where applicable)

Description

In 2.0-alpha we changed the route syntax and made all the routes async which meant they all had to return Task<T>. We also temporarily introduced LegacyNancyModule which was a custom module class which mimicked the old, synchronous, route behavior with the intent to help people migrate to 2.0 and then we would remove it in a later release (perhaps even before 2.0 RTM). As it turns out, forcing Task<T> on all routes wrecked our syntax a bit it made it difficult to have routes that returned primitives such as int (status code), HttpStatusCode, string (response body) or even to call View or Negotiate. We did try some tricks, like custom awaiters and so on which made the syntax a bit nicer but it still forced you to declare all routes as async and then await

Get["/"] = async (args, ct) => await 200;

Get["/"] = async (args, ct) => await View["index"];

Not too shabby, but it still forces you to declare these simple routes, which aren't async by nature, as async which would cause the entire async state machine to be generated even though you did not need them. Clearly not ideal, and most certainly not very Super-Duper-Happy-Path

Proposal

For quite some time now, we have talked internally about a new route declaration syntax. Not a massive change, but one that would mean we would trade in the indexer syntax Get["/"] (they have been a signature mark for Nancy since day one) for a method based approach Get("/").

See #2441 (comment) for samples

Code change mentions

  • I have introduced a delegate T RouteAction<T>(dynamic parameters, CancellationToken token) which represents the body of a route
  • I have made Route abstract and introduced Route<T>. The base class Route does not contain an Action anymore, the idea is that a Route doesn't per-say need to have an action it just needs to be invocable so you could inherit different kinds of Route if you have any other desires. The NancyModule declarations uses Route<T> which has the Action property of type RouteAction<T> that in turn will be called when Route.Invoke is invoked.
  • Introduced RouteResultHelper which is used to take a route response, which may or may not be a Task, and coereces the result in a format what we can use it. It does some funky casting with the help of reflection and generic method signatures. This class is used by DefaultRouteInvoker and DiagnosticsHook + in a test where we needed to mock the IRouteInvoker and have it return a proper response.
  • All route declaration method (Get(), Post() and so on) on NancyModule have been made virtual so you can override and intercept the declarations if you'd like to do something to it before it is stored
  • dynamic is only used in the route lambdas now, from there on it should be DynamicDictionairy

Worth noting

  • Since we are inferring T you cannot return instances of a private model
  • For the same reason you need to explicitly define a model object for routes that returns an anonymous model Get<object>("/", (args, ct) => new { Name = "Nancy" });
  • I have removed LegacyNancyModule as it serves very little point now and converting to the new syntax is very fast

@thecodejunkie thecodejunkie added this to the 2.1 milestone May 11, 2016
@thecodejunkie thecodejunkie changed the title Introduced new route declaration syntax [WIP] Introduced new route declaration syntax May 11, 2016
@jchannon
Copy link
Member

Should we add an overload to remove the CancellationToken from sync routes?

Get("/", (args, ct) => {
   return "Hi"
});

So it becomes

Get("/", (args) => {
   return "Hi"
});

@thecodejunkie
Copy link
Member Author

@jchannon yeah, doing that now

@thecodejunkie thecodejunkie mentioned this pull request May 11, 2016
4 tasks
@thecodejunkie
Copy link
Member Author

@jchannon pushed + updated description

@@ -56,7 +56,7 @@ public class Route<T> : Route
/// </summary>
/// <param name="description"></param>
/// <param name="action">The action that should take place when the route is invoked.</param>
public Route(RouteDescription description, RouteAction<T> action)
public Route(RouteDescription description, Func<dynamic, CancellationToken, T> action)
Copy link
Member

@khellang khellang May 11, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't these return a Task<T>?

@jchannon
Copy link
Member

I see you made the CancellationToken optional but would there be a case where you'd want it on a non-async route?

@khellang
Copy link
Member

khellang commented May 11, 2016

I see you made the CancellationToken optional but would there be a case where you'd want it on a non-async route?

Why/how would you cancel a synchronous operation using a CancellationToken?

@jchannon
Copy link
Member

jchannon commented May 11, 2016

@khellang who's that message for?

@khellang
Copy link
Member

khellang commented May 11, 2016

@khellang who's that message for?

You 😄

@jchannon
Copy link
Member

You wouldn't that's why I'm asking why make it available on a sync route

@khellang
Copy link
Member

You wouldn't that's why I'm asking why make it available on a sync route

Oh, I thought you were asking if we could add it. The main problem right now is that all the methods accept non-async delegates (no Task<T>) return.

/// <param name="path">The path that the route will respond to</param>
/// <param name="action">Action that will be invoked when the route it hit</param>
/// <param name="condition">A condition to determine if the route can be hit</param>
protected void AddRoute<T>(string name, string method, string path, Func<NancyContext, bool> condition, Func<dynamic, CancellationToken, T> action)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems odd to swap the condition and action args around compared to the verb methods.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ha ha, well spotted! 😄

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They weren't really swapped around, but moved out of necessity. The AddRoute parameter order is left unchanged while the verb methods have been rearranged because condition and name are optional.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense. Guess I'd have swapped them in AddRoute too then.

@thecodejunkie
Copy link
Member Author

@khellang me and @grumpydev spent some time this morning verifying the behavior of it not being Task<T> under the premiss that it would be inferred + verified the behavior of the result coercion (found a bug). It all seem to work as expected in terms of asynchronicity https://gist.github.com/grumpydev/048e8ab6fb390dfa53d77d3cc68a4b19

@thecodejunkie
Copy link
Member Author

ℹ️ Update the route declaration overloads to support both sync and async routes, both with or without a cancellation token (ignore the silly sample routes, just needed compiling routes to show what it is now capable of)

public class RoutingModule : NancyModule
{
    public RoutingModule()
    {
        Get("/", args =>
        {
            return 200;
        });

        Get("/", (args, ct) =>
        {
            return Task.FromResult(200);
        });

        Get("/", async args =>
        {
            return await Task.FromResult(200);
        });

        Get("/", async (args, ct) =>
        {
            return await Task.FromResult(200);
        });
    }
}

@thecodejunkie thecodejunkie force-pushed the update-route-syntax branch 4 times, most recently from ff9e550 to b490c6b Compare May 16, 2016 11:21
@thecodejunkie thecodejunkie changed the title [WIP] Introduced new route declaration syntax Introduced new route declaration syntax May 16, 2016
/// <returns>A (hot) task of <see cref="Response"/> instance.</returns>
public override Task<object> Invoke(DynamicDictionary parameters, CancellationToken cancellationToken)
{
return this.Action.Invoke(parameters, cancellationToken).ContinueWith<object>(t => t.Result, TaskContinuationOptions.OnlyOnRanToCompletion);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you need the ContinueWith

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because this.Action returns Task<T> and we need convert it to a Task<object>

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed further up you called an action like action(...) but here you're using Action.Invoke(...) I don't believe it makes much different but, consistency? :)

@jchannon
Copy link
Member

jchannon commented May 16, 2016

:shipit:

@aidapsibr
Copy link
Contributor

Looking forward to testing this one, I would imagine this will make it simpler to add Swaggeresque metadata overloads in the future or even as an extension.

@jchannon
Copy link
Member

Yup

On Tuesday, 17 May 2016, Ovan Crone notifications@github.com wrote:

Looking forward to testing this one, I would imagine this will make it
simpler to add Swaggeresque metadata overloads in the future or even as an
extension.


You are receiving this because you were mentioned.
Reply to this email directly or view it on GitHub
#2441 (comment)


Post["/login"] = x => {
Post("/login", args => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OMG @thecodejunkie can you stop with the inconsistent braces!!! :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've not changed a single brace

@davidfowl
Copy link

I approve of this syntax 👍

@grumpydev
Copy link
Member

Fowler is agreeing with us.. Anyone else suspicious? 😂

@phillip-haydon
Copy link
Member

So we can close this PR now?

@thecodejunkie
Copy link
Member Author

@phillip-haydon if we have properly reviewed the changes around async stuff then yes.. if no, then we need to review it :P For example are we using ConfigureAwait(false); in relevant places in the change etc?

@horsdal
Copy link
Member

horsdal commented May 19, 2016

Reviewed a second time. Didn't find anything 😸

@phillip-haydon
Copy link
Member

Held up by @khellang as always. 👀

@jchannon jchannon merged commit 4d9c30e into NancyFx:master May 19, 2016
@jchannon
Copy link
Member

jchannon commented May 19, 2016

Momentous Day!!

tumblr_ln9eat2mn61qa9fb5o1_500

@aidapsibr
Copy link
Contributor

May I request a new release for this?

@thecodejunkie
Copy link
Member Author

@psibernetic we're working on it. in the meantime you can always grab it of our myget feed which gets built on each new commit to master

@aidapsibr
Copy link
Contributor

Yeap thank you, fellas in chat pointed me there, been testing away.

@xt0rted xt0rted mentioned this pull request May 27, 2016
4 tasks
@canton7
Copy link
Contributor

canton7 commented Sep 21, 2016

The wiki still needs updating, BTW

@thecodejunkie
Copy link
Member Author

@canton7 the wiki will not be updates as the 2.0 packages comes out of pre-release, until then all changes are considered as being pending =)

@canton7
Copy link
Contributor

canton7 commented Sep 21, 2016

My bad, apologies for the noise.

@thecodejunkie thecodejunkie mentioned this pull request Sep 27, 2016
3 tasks
@steve2
Copy link

steve2 commented Mar 4, 2019

Are there ever going to be doc updates that use the new syntax?

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

10 participants