Skip to content
This repository has been archived by the owner on Mar 20, 2018. It is now read-only.

RESTful URL generation from models #79

Open
ricec opened this issue Mar 7, 2014 · 6 comments
Open

RESTful URL generation from models #79

ricec opened this issue Mar 7, 2014 · 6 comments

Comments

@ricec
Copy link

ricec commented Mar 7, 2014

One of the more powerful side effects of having RESTful routes is that no longer do you have to pull values from your models and format them into a URL. Your routing should do this for you. All you need to do is supply the model. This can be seen in the Rails url_for method.

Instead of...

url_for controller: 'users', action: 'show', id: @user.id

You have...

url_for @user

.NET MVC is helpful in this scenario by filling in route parameters from the original request. If I'm on users/1 and I want to get to users/1/edit,

@Html.ActionLink("Edit", "Edit")

will work just fine. This breaks down when these "ambient" values do you no good. For example, if I'm on users/1 and I want to link to a nested resource at users/1/things/1, I'm left with this nasty bit of code...

@Html.ActionLink("Show", "Show", "Things", new { id = thing.Id, userId = thing.UserId }, null)

Ideally, we could instead write...

@Html.LinkTo("Show", thing)

To accomplish this, we need to do a few things:

  • Allow devs to tie a model type to the routes.
  • Allow devs to specify a lambda that retrieves the ID value from the model.
  • Write some HTML helpers that make use of this route to model relationship to generate URLs, links, forms, etc.

Something like this might work...

map.Resources<UsersController>(users =>
{
    // Associate the Users model with these resource
    // routes and specify how to retrieve the model's ID.
    users.ResourceType<Users>(u => u.Id);
});

So, I have two questions:

  1. Has this feature already been considered?
  2. If I were to build it, would it be a welcomed contribution?
@khalidabuhakmeh
Copy link
Collaborator

Hi @ricec,

Sorry for the late reply. I am in the process of moving but I wanted to make sure to answer your question.

I normally (and assume many other MVC devs) use ViewModels for each view. This means you don't have a particular Model that is tied to a resource. Instead I like to treat Resources as conceptual rather than exact. The users controller deals with the users domain model, but might have multiple viewmodels: IndexModel, ShowModel, EditModel. The reason I have this approach is I can clearly set values required for each of those view. IndexModel is the best example since the index action is normally where I page the collection. Here is what one of my actions looks like.

public ActionResult Index(SearchModel search)
{
    using (var session = DocumentStore.OpenSession())
    {
        var model = new IndexModel(search);
        model.Items = session
            .Query<Businesses_Search.Result, Businesses_Search>()
            .If(model.HasQuery, q => q.Search(m => m.Content, model.Query))
            .As<Business>()
            .ToPagedList(model.Page, model.Size);

        return View(model);
    }
}

And generally what my index model looks like.

public abstract class IndexModelBase<T>
{
    protected IndexModelBase()
    {
        Items = new PagedList<T>(new List<T>(),1, 1);
        Page = 1;
        Size = 10;
    }

    protected IndexModelBase(SearchModel search)
        : this()
    {
        Page = search.Page.HasValue ? Math.Max(1, search.Page.Value) : 1;
        Query = search.Query;
    } 

    public IPagedList<T> Items { get; set; }
    public int Page { get; set; }
    public int Size { get; set; }
    public string Query { get; set; }

    public bool HasQuery { get { return !string.IsNullOrWhiteSpace(Query); } }
}

I have created a library called WhatRoute that has this construct. I also have a library called Fluorescent that helps generate routes (based off of restful routing) for clientside use. Take a look at both of them :)

Url.PathTo(string routeName, object routeValues = null)

I know this is a long response, but I wanted to give you some background as to how a lot of devs operate in the .NET space. I also know routing is really annoying :). So let me answer your questions.

Has this feature already been considered?

Sorta... WhatRoute was an attempt to move towards using the names on routes, but not to leverage the implicit values carried over from the previous request. Fluorescent looks at routes and tries to create a template that can be used clientside. Both still have the issue of needing explicit values passed in.

If I were to build it, would it be a welcomed contribution?

I like the idea of the LinkTo taking a model, but I don't like the idea of tying routes to a particular model. I think it would cause more issues than solve. A happy medium would be this.

@Html.LinkTo("Edit", model)

LinkTo would take these steps:

  1. Look (reflection) at the model and see if there is an associated controller name with the model. This can be done through a property or attribute. If not, we can use the name of the model.
[ResourceName("Users")
public class ShowModel {
       public string Id {get;set;}
       public string UserId {get;set;}
 }
// or
public class ShowModel { 
    public string Id {get;set;}
    public string UserId {get;set;}
    public string ResourceName { get { return "Users" } }
}
  1. Search through all the routes and find the one that matches.
  Url.Action(action,ResourceName, new RouteValueDictionary(model))
 // then remove querystring info because of the extra properties on the model
  1. Return the url

What do you think?

@ricec
Copy link
Author

ricec commented Mar 15, 2014

Hi, @khalidabuhakmeh. Thanks for the response.

I definitely agree that view models can be quite useful for decoupling your domain models from your views and for storing additional view-specific data. I might have misled you by all my Rails-related talk, but I actually do have quite a bit of experience with .NET MVC, so I'm very familiar with this pattern. I've just come off a year-long stint of Rails work, though, so my mind is still transitioning back into the .NET world.

I think the source of our disagreement on this is our opinions of what a resource is. You say that resources are conceptual, but I believe that they can be more concrete. Since in REST, a URI uniquely identifies a resource, let's look at a URI from the users example, http://example.com/users/1. This URI very clearly identifies an instance of our User domain model, and the HTTP verbs represent actions performed on this concrete resource. If I am correct, this means that a domain model that has been exposed as a resource could be used to retrieve its associated URI, as I have suggested. Since the routing layer is where we determine whether or not to expose something as a resource, it seems logical to identify what the resource is in this same layer.

I'm not saying that all resources are domain models, but I believe they can be. That's my view on things, but I would welcome some debate on how a resource is defined. I believe that the answer to that question determines how this feature is implemented and whether it even makes sense.

How to resolve my perspective with the MVVM pattern depends on the nature of your view model:

  • If your view model is your domain model, then there are no issues. Simply call...
@Html.LinkTo("Show", Model)
  • If your view model references a domain model through one of its properties, as in your index example, there are still no issues. Simply call...
@Html.LinkTo("Show", Model.DomainModelProperty)
  • If your view model is a projection of your domain model, providing optimal decoupling of the view and domain model, then things are a little trickier. In this case, the responsibility of generating URLs needed by the view falls on the view model. I'm not sure I like this level of decoupling, though. If your view is primarily a representation of your domain model, I believe that tight coupling is permissible.

All of this to say that I still think a RESTful route should be directly tied to the resource it is exposing, and I think that in many situations, that resource is a domain model.

What do you think?

@khalidabuhakmeh
Copy link
Collaborator

Hi @ricec,

So let's step back and define what the issue is and work our way to a solution.

Problem

If I understand correctly:

We would like to be able to generate routes based off of a model, which in turn will help reduce noise in our views.

So here are some of the questions we have to answer:

Do we need to register the model with routing?

  • My feeling is probably no, and I'd prefer we don't. We don't gain much from this and we make the routing more brittle imho.

What data do we need to pass to the extension method to resolve a url?

  • to resolve a url we need the controller, action, id, and any related resource ids. ex. controller:orders, action:show, id:1, userId:1

Where can we get the data from?

The model and an action string is where we would like to pull that extra information from, the issue is we don't want to clutter our "domain" models with extra information.

@Html.LinkTo("show", model)

The ultimate question, where the heck does the information come from?

RouteData holds the following information:

  • Current Controller
  • Current Action
  • Current Ids (possibly)

Model (possibly) holds the following information:

  • Destination Controller name
  • Destination Id(s)

User input provides:

  • Destination action name

Solution

Possible algorithm for getting the above information.

  1. First check to see if a ResourceAttribute exists on the model. If so, use the value as the name of the controller. ResourceAttribute might also contain area (default: "").
  2. If the attribute does not exist, take the name of the model and pluralize it. For example: User -> Users.
  3. Option 1: Pass the model in with the controller/resource name, action (user provided), into Url.Action. Strip the query string and we should have resolved a URL, with no extra values at the end.
  4. Option 2: Find the route that has a controller, action, and area match. Then find what other values are necessary. Then read those values from the model that was passed in. If those values are not found throw a helpful exception.

Attribute Example:

[Resource("Orders")]
public class ShowModel { 
     public string Id {get;set;}
     public string UserId {get;set;}
     // more info goes here
}
@Html.LinkTo("Edit", Model)

Non-Attribute Example:

public class Order {
    public string Id {get;set;}
    public string UserId {get;set;}
    // more info goes here
}
@Html.LinkTo("Edit", Model)

Thoughts

The interesting thing about the algorithm provided above is it can work outside of Restful Routing and we can spike it without having to do it in Restful Routing. If it ends up working we can merge it in as an added feature of Restful Routing.

What do you think? Do I make a compelling argument or do you still want to link the resource to the controller?

@ricec
Copy link
Author

ricec commented Mar 18, 2014

Hi, @khalidabuhakmeh.

Thanks for humoring me with this discussion even though I'm clearly wrong. :)

I've done a little more reading (specifically, the Fielding dissertation on REST), and it has helped to reshape my opinions. From the dissertation:

A resource is a conceptual mapping to a set of entities, not the entity that corresponds to the mapping at any particular point in time.

This directly contradicts what I've been saying! So here I am, doing a 180. I resource is abstract, as you said, and in the case of RESTful Routing, we are defining the resource in the routes. Sorry for wasting your time with my faulty argument, and thank you for pushing me in the right direction.

Anyway, back to the problem... With my new perspective, I like your approach, but I am a little concerned over the nomenclature. Giving a class a "Resource" attribute seems like we are tagging it as a resource, but that's not what we're doing. I actually think that in this case, it's best to move away from REST terminology, since all we are really doing is flagging a class as something that can be used to generate a URL. Maybe we call it the RoutableAttribute?

For implementation, we could probably use the model binders to fill in URL parameters when reverse routing. Regardless, I think we're aligned on direction now. Let's start working on some code.

@khalidabuhakmeh
Copy link
Collaborator

@ricec No need to apologize, I am just happy someone is using Restful Routing :) I love this library and hope you grow to love it too!

I actually think that in this case, it's best to move away from REST terminology, since all we are really doing is flagging a class as something that can be used to generate a URL. Maybe we call it the RoutableAttribute?

I can dig it 👍. Naming stuff is hard :P

Do you want to branch off of main and give it a try? I can help you figure out the attribute scanning stuff or any other part if you like. I've done a lot of routing in WhatRoute so I have extensive knowledge in resolving routes.

Also, what do you think about some "safe" or "sane" defaults?

@Html.LinkTo(Model)

is equivalent to

@Html.LinkTo("Show", Model)

@ricec
Copy link
Author

ricec commented Mar 18, 2014

@khalidabuhakmeh, I like the defaults. I'll work that in...

I'll try to put some work in on this tonight or tomorrow, and I'll let you know if I could use some guidance.

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

No branches or pull requests

2 participants