MS MVC: Simply Restful Routing

MS MVC: Simply Restful Routing

MS MVC first public code drop is available. I hope you have read through all of ScottGu’s posts to help prep you for the drop. If you haven’t I highly suggest reading about Routing first before you dive into this.

What we are going to do is build a Simply Restful Route Handler similar to what is available in rails. MS MVC supports restful routing pretty good out of the box. The only case it doesn’t pick up is when browsers cannot send real HTTP PUT and DELETE requests. To get around this we are going to build a very simple route handler to inspect a hidden field on the submitted form to set the intended action of the request.

We will be creating 8 restful routes, contrary to the 7 routes in simply restful.

 

The Eight Actions

  • Show : handles a GET request for a displaying a single resource that the controller is representing.
  • Create : handles a POST request for a creating a new resource.
  • Update : handles a PUT request for updating an existing resource.
  • Destroy : handles a DELETE request on a resource.
  • Index : handles a GET request for a collection of resources.
  • New : handles a GET request for a blank form for creating a new resource
  • Edit : handles a GET request for a form with values filled in from the resource for updating.
  • Delete : handles a GET request a confirmation / form with options for deleting a resource. * This is the extra action.

The Routes

ActionUrlHttp MethodForm Method
Show[controller]/[id]GET 
Create[controller]POST 
Update[controller]/[id]PUT 
Update[controller]/[id]POSTPUT
Destroy[controller]/[id]DELETE 
Destroy[controller]/[id]POSTDELETE
Index[controller]GET 
New[controller]/newGET 
Edit[controller]/[id]/editGET 
Delete[controller]/[id]/deleteGET 

 

The Simply Restful Routes In MS MVC

routeCollection.Add(new Route
{
  Url = “[controller]/new“,
  Defaults = new { Action = “new” },
  RouteHandler = typeof(MvcRouteHandler)
});
routeCollection.Add(new Route
{
  Url = “[controller]/[id]/[action]“,
  Validation = new
  {
    Method = “GET“,
    Id = idValidationRegex ?? MatchAny,
    Action = “[eE][dD][iI][tT]|[dD][eE][lL][eE][tT][eE]”
  },
  RouteHandler = typeof(MvcRouteHandler)
});

routeCollection.Add(new Route
{
  Url = “[controller]/[id]“,
  Validation = new
  {
    Method = “POST“,
    Id = idValidationRegex ?? MatchAny,
  },
  RouteHandler = typeof(SimplyRestfulRouteHandler)
});

routeCollection.Add(new Route
{
  Url = “[controller]/[id]“,
  Defaults = new { Action = “show” },
  Validation = new
  {
    Method = “GET“,
    Id = idValidationRegex ?? MatchAny,
  },
  RouteHandler = typeof(MvcRouteHandler)
});

routeCollection.Add(new Route
{
  Url = “[controller]/[id]“,
  Defaults = new { Action = “update” },
  Validation = new
  {
    Method = “PUT“,
    Id = idValidationRegex ?? MatchAny
  },
  RouteHandler = typeof(MvcRouteHandler)
});

routeCollection.Add(new Route
{
  Url = “[controller]/[id]“,
  Defaults = new { Action = “destroy” },
  Validation = new
  {
    Method = “DELETE“,
    Id = idValidationRegex ?? MatchAny
  },
  RouteHandler = typeof(MvcRouteHandler)
});

routeCollection.Add(new Route
{
  Url = “[controller]“,
  Defaults = new { Action = “index” },
  Validation = new { Method = “GET” },
  RouteHandler = typeof(MvcRouteHandler)
});

routeCollection.Add(new Route
{
  Url = “[controller]“,
  Defaults = new { Action = “create” },
  Validation = new { Method = “POST” },
  RouteHandler = typeof(MvcRouteHandler)
});

I am not going to explain much of what is going on above, have a look at ScottGu’s post on routing for that. What I will point out are a couple things. First we use use some hard matches like [controller]/new and map that with a Default action. Very straightforward but just a good example of how Routes and Defaults work. Second we add some validation. Scott covered the standard validation that accepts a regex. You will notice that the regex is case sensitive so we end up with a pretty wacky looking string for “Edit|Delete”. Third, we are using a special validation property named Method which will match on the HttpMethod of the Request. And finally notice we only have a single rule that will require a custom IRouteHandler.

So lets start with a story for our new custom route handler:

Story: RouteHandler Assigns Restful Actions

As a Route Handler
I want standard post requests to route to restful actions.
So that people without javascript enabled or incompatible browsers can still have a restful experience and my web server needs to issue less browser redirects.

Acceptance Criteria:

Scenario 1: Form _method PUT Triggers Update Action
GIVEN the form has a field named _method with a value of PUT
AND the url matches [controller]/[id]
AND the HTTP method is POST
WHEN the form is posted
THEN the route action should be Update

Scenario 2: DELETE Triggers Destroy Action
GIVEN the form has a field named _method with a value of DELETE
AND the url matches [controller]/[id]
AND the HTTP method is POST
WHEN the form is posted
THEN the route action should be Delete

TDD First

To make testing easier I will be using Rhino.Mocks to create dynamic mocks of the IHttpContext and IHttpRequest. Thanks Microsoft for finally giving us interfaces to make our testing easier.

[TestFixture]
[Category("SimplyRestfulSpecs“)]
public class When_The_Form_Is_Posted_With_A_Form_Field_Named_Method_And_A_Value_Of_PUT
: BaseRouteHandlerTestFixture
{
[SetUp]
protected override void GivenSetupContext()
{
  base.GivenSetupContext();
  form.Add(”_method“, “PUT“);
}

[Test]
public void Then_The_Route_Action_Should_Be_Set_To_Update()
{
  RestfulAction action = RestfulAction.None;
  IRestfulActionResolver resolver = new RestfulActionResolver();

  using(mocks.Record())
  {
    SetupResult.For(httpContext.Request).Return(httpRequest);
    SetupResult.For(httpRequest.RequestType).Return(”POST“);
    SetupResult.For(httpRequest.Form).Return(form);
    requestContext = new RequestContext(httpContext, routeData);
  }

  using(mocks.Playback())
  {
    action = resolver.ResolveAction(requestContext);
    Assert.That(action, Is.EqualTo(RestfulAction.Update));
  }
}
}

[TestFixture]
[Category(”SimplyRestfulSpecs“)]
public class When_The_Form_Is_Posted_With_A_Form_Field_Named_Method_And_A_Value_Of_DELETE
: BaseRouteHandlerTestFixture
{
[SetUp]
protected override void GivenSetupContext()
{
  base.GivenSetupContext();
  form.Add(”_method“, “DELETE“);
}

[Test]
public void Then_The_Route_Action_Should_Be_Set_To_Destroy()
{
  RestfulAction action = RestfulAction.None;
  IRestfulActionResolver resolver = new RestfulActionResolver();

  using (mocks.Record())
  {
    SetupResult.For(httpContext.Request).Return(httpRequest);
    SetupResult.For(httpRequest.RequestType).Return(”POST“);
    SetupResult.For(httpRequest.Form).Return(form);
    requestContext = new RequestContext(httpContext, routeData);
  }

  using (mocks.Playback())
  {
    action = resolver.ResolveAction(requestContext);
    Assert.That(action, Is.EqualTo(RestfulAction.Destroy));
  }
}
}
  #region BaseRouteHandlerTestFixture
  public abstract class BaseRouteHandlerTestFixture
  {
    protected MockRepository mocks;
    protected IHttpContext httpContext;
    protected IHttpRequest httpRequest;
    protected RouteData routeData;
    protected RequestContext requestContext;
    protected NameValueCollection form;

    protected virtual void GivenSetupContext()
    {
      mocks = new MockRepository();
      httpContext = mocks.DynamicMock<IHttpContext>();
      httpRequest = mocks.DynamicMock<IHttpRequest>();

      routeData = new RouteData();
      routeData.Values.Add(”controller“, “testcontroller“);
      routeData.Values.Add(”action“, “update“);

      form = new NameValueCollection();
    }
  }
  #endregion

The Code

public enum RestfulAction
{
None = 16384,
Show = 1,
Create = 2,
Update = 4,
Destroy = 8,
Index = 16,
New = 32,
Edit = 64,
Delete = 128
}

public class SimplyRestfulRouteHandler : MvcRouteHandler
{
private const string MatchAny = “*“;

private IRestfulActionResolver actionResolver;

public SimplyRestfulRouteHandler()
{
actionResolver = new RestfulActionResolver();
}

public SimplyRestfulRouteHandler(IRestfulActionResolver actionResolver)
{
this.actionResolver = actionResolver;
}

protected override IHttpHandler GetHttpHandler(RequestContext requestContext)
{
EnsureActionResolver(requestContext.HttpContext);

RestfulAction action = actionResolver.ResolveAction(requestContext);
if (action != RestfulAction.None)
{
requestContext.RouteData.Values[”action“] = action.ToString();
}
return base.GetHttpHandler(requestContext);
}

private void EnsureActionResolver(IServiceProvider serviceProvider)
{
if (actionResolver == null)
{
actionResolver = (IRestfulActionResolver)
serviceProvider.GetService(typeof(IRestfulActionResolver));
if (actionResolver == null)
actionResolver = new RestfulActionResolver();
}
}
}

public interface IRestfulActionResolver
{
RestfulAction ResolveAction(RequestContext context);
}

public class RestfulActionResolver : IRestfulActionResolver
{
public RestfulAction ResolveAction(RequestContext context)
{
if (context == null ||
context.HttpContext == null ||
context.HttpContext.Request == null)
{
throw new NullReferenceException(”Request in current HttpContext cannot be null.“);
}

if (string.IsNullOrEmpty(context.HttpContext.Request.RequestType))
{
return RestfulAction.None;
}

string requestType = context.HttpContext.Request.RequestType.ToLowerInvariant();
if (string.Equals(requestType, “post“, StringComparison.Ordinal))
{
return ResolvePostAction(context);
}

return RestfulAction.None;
}

private static RestfulAction ResolvePostAction(RequestContext context)
{
if (context.HttpContext.Request.Form == null)
{
return RestfulAction.None;
}

string formMethod = context.HttpContext.Request.Form[”_method“];
if (string.IsNullOrEmpty(formMethod))
{
return RestfulAction.None;
}

formMethod = formMethod.Trim();
if (string.Equals(”put“, formMethod, StringComparison.OrdinalIgnoreCase))
{
return RestfulAction.Update;
}
else if(string.Equals(”delete“, formMethod, StringComparison.OrdinalIgnoreCase))
{
return RestfulAction.Destroy;
}

return RestfulAction.None;
}
}

Conclusion

The only opinion I have so far on MS-MVC is it beats the heck out of WebForms and I am glad to see MS is committed to supporting a product that fits this space. In the coming weeks I should have much more about the framework. I just wanted to get something usable out to the community first.

I hope to get this stuff into the new MVC Contrib project as well.

Whats next, maybe some form helpers, templates, and a base controller to make working with the views and actions a little easier.

<script src="http://feeds.feedburner.com/~s/abombss?i=http://abombss.com/blog/2007/12/10/ms-mvc-simply-restful-routing/" type="text/javascript" charset="utf-8"></script>

9 Comments so far

  1. […] much easier. Here I will show you some tests I wrote to validate the routing rules we created for simply restful routing in the ms mvc […]

  2. Jeffrey Palermo on December 10th, 2007

    Problem with you patch. I left a message for you on the MvcContrib workspace

  3. Lance Fisher on March 4th, 2008

    Hi Adam,

    I’ve been looking around to find some examples of using the SimplyRestful routing that is now included in MvcContrib, and I can’t really find anything. Looking at the tests, I’m guessing that I just have to call:

    MvcContrib.SimplyRestful.SimplyRestfulRouteHandler.BuildRoutes(routes);

    in my Application_Start and then write the Show, Create, Update, etc. action methods on my controllers. Is this correct?

    Also, when I go to deploy this on IIS 6, am I going to have to redo the routes with a “.mvc” after the controller? Would a BuildIIS6Routes method in MvcContrib be appropriate? If so, I’d be happy to write the patch.

    Thanks,
    Lance

  4. dave on March 6th, 2008

    I tried to update the mvccontrib stuff for preview 2 and it breaks on the post constraints. Any idea how to fix this? I am having a bear of a time figuring out how to debug routes.

  5. Mrfleck on March 6th, 2008

    Nevermind got it all figured out and running in preview 2

  6. Mrfleck on March 6th, 2008

    I lied, I got my tests to pass but it still does not work, I will keep digging and if I come up with the answer I will post a patch.

  7. Adam Tybor on March 9th, 2008

    There is a preview2 branch in the mvc contrib repository that has the corrected changes. I think at this point the branch is pretty complete.

    The biggest undocumented change was the Constraint, formerly Validation, “method” was changed to “httpMethod”.

    So Contraint = new { httpMethod = “POST” } is what needed to get done.

    Sorry for the delay in the reply.

  8. Adam Tybor on March 9th, 2008

    @Lance,

    Point well taken about IIS6 routes, I will look into a resolution because technically the .mvc isn’t on the controller its the extension of the whole url which really breaks some of the new stuff I have in store.

    I will also have a sample app soon.

  9. ivan on March 18th, 2008

    How can use your code? I am using the latest preview2 code. How do I tell .net to use our routinghandler? If I use this code it tells me that there is no Route that takes 0 parameters:

    Error 1 ‘System.Web.Routing.Route’ does not contain a constructor that takes ‘0′ arguments C:/Dev/TestMVC2/TestMVC2/Global.asax.cs 34 24 TestMVC2

    the code used:

    ===========
    using MvcContrib.SimplyRestful;
    ….
    routes.Add(new Route
    {
    Url = “[controller]/new”,
    Defaults = new { Action = “new” },
    RouteHandler = typeof(MvcRouteHandler)
    });

    SimplyRestfulRouteHandler.BuildRoutes(routes, “/[controller]”, null, null); 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值