An easy way to invoke function by http method and url pattern in WebPages Razor

We’re used to simply add a method named “get” or “post” in WebApi to do something for that particular http method, with a particular signature (url pattern). Can we do something similar in Razor (WebPages)?

Edit: I created a small nuget package using this approach + adding the aspnet data binder to WebPages razor, the result is an easy way to add json endpoints to your WebPages codebases “WebPagesApi”. WIP/WoMM Check it out.

Let’s say we like to handle a get requests to /products/:id (int) and /products and post requests to /products/:id separately. We can do that by letting all requests go through a chunk of code to check method and url data. Can we do that in a more elegant and safer way with Razor? Just like this:

    public object get(int id)
    {
        // get data from somewhere
        return someSerializableObject;
    }

Sure we can. Remember that a Razor page is also a class, and you can add methods to it by using the functions keyword. You can also override some built in methods, like for example InitializePage.

With this knowledge, and a bit understanding of Reflection, we can make the page try find a suitable method to invoke, based on http method with just a couple lines of code:

@using System.Reflection
@functions{

    public object get()
    {
        return "hello world";
    }
    public object get(int id)
    {
        return id;
    }
    public object get(string id)
    {
        return id + " is a string";
    }    
    public object post(int id){
        return "you posted something";
    }
    
    protected override void InitializePage()
    {
        base.InitializePage();

        var methodName = System.Web.HttpContext.Current.Request.HttpMethod.ToLower();
        var urlArgs = UrlData.Select<string, object>(itm => { int iItm; if (int.TryParse(itm, out iItm)) return iItm; return itm; }).ToArray();

        Response.Write(Json.Encode(this.GetType().InvokeMember(methodName, BindingFlags.Default | BindingFlags.InvokeMethod, null, this, urlArgs)));
    }

}

Save this as products.cshtml and you have a nicely syntax separated, easy to update, code file for get on “/products” and “/products/:id” + post on “/products/:id”

a call to /products will return

hello world

a call to /products/123 will return

123

as the invoker is intelligent enough to figure out the signature with an integer. Where a call to /products/foo will return

foo is a string

As you notice I did not add any error handling. It can easily be done, but preferrably somewhere else. Or why not just rely on the built in error messages? At least if it’s for api calls.

RPC style urls
If you like the first part of the url be the method name instead of using http method names (/products/dosomething/123) you can change the code to this:

@using System.Reflection
@functions{

    public object index()
    {
        return "hello world";
    }
    public object dosomething(int id)
    {
        return "do something for id: " + id;
    }
    protected override void InitializePage()
    {
        base.InitializePage();

        var methodName = UrlData.Count==0?"index":UrlData[0].ToLower();
        var urlArgs = UrlData.Skip(1).Select<string, object>(itm => { int iItm; if (int.TryParse(itm, out iItm)) return iItm; return itm; }).ToArray();

        Response.Write(Json.Encode(this.GetType().InvokeMember(methodName, BindingFlags.Default | BindingFlags.InvokeMethod, null, this, urlArgs)));
    }

}

And if we move it to a separate class (save it in App_Code if you like) we can shorten the razor file even more:

RazorApiRpc.cs:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Web;
using System.Web.Helpers;
using System.Web.WebPages;

public class RazorApiRpc : WebPage
{
    protected override void InitializePage()
    {
        base.InitializePage();
        var methodName = UrlData.Count == 0 ? "index" : UrlData[0].ToLower();
        var urlArgs = UrlData.Skip(1).Select<string, object>(itm => { int iItm; if (int.TryParse(itm, out iItm)) return iItm; return itm; }).ToArray();
        Response.Write(Json.Encode(this.GetType().InvokeMember(methodName, BindingFlags.Default | BindingFlags.InvokeMethod, null, this, urlArgs)));
    }
    public override void Execute()    {    }
}

rpcsample.cshtml:

@inherits RazorApiRpc
@functions{
    public object index()
    {
        return "hello world";
    }
    public object dosomething(int id)
    {
        return "do something for id: " + id;
    }        
}
Advertisements

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s