5

BACKGROUND

We've got some links on our site that have formats that look like:

http://oursite.com/books/c_sharp_in_depth_12345.

To handle this, we use a simple property called Url:

public class Book
{
    public string Url { get; set;}
}

Basically, the URL comes from our website's domain, a section of the site (i.e. books), the name of the page and a unique id to identify the resource. The full URL is being stored in the database.

We don't like the fact that our database is storing the section name. That's a property of web layer property, not a property of the book. The database should not be dependent upon the web layer.

So we remove the section from the URL and get the following:

public class Book
{
    public string UrlWithoutSection { get; set;}
}

OK, that works for this URL. But then our company's SEO czar says that our URL is wrong and that Google, our-one-true-love, will only love us if we re-write our urls like this:

http://oursite.com/programming-books/c-sharp-in-depth-12345

Uh, oh, I thought that we had removed the database dependency to the web layer, but we hadn't. Turns out, we had removed the dependency to the section, but the format of the URL still exists in the database. We fix this by abstracting the URL into an object:

public class OurUrl 
{
    public string title { get; set; }
    public string id { get; set; }
}

Cool, now the dependency to the web layer is gone. Uh oh, here this time our CEO comes to us. We just bought a new company and now we're selling magazines. Uh, great? The magazine URLs are going to look like this:

http://oursite.com/magazines/computers/stack-overflow-the-magazine/2012/01/01/12345

OK, no problem, just create another object.

public class OurMagazineUrl : OurUrl
{
    public DateTime PublishedDate { get; set; }

    // Magazine enum will have to be created.
    public MagazineType Type { get; set; }  
} 

It works, except then I begin to realize that we have plans for a big site. Lots of URLs. Lots of differently formatted URLs. Creating a new class every time seems like a major headache.

THE PROBLEM IN A NUTSHELL

How do you handle URLs so that the web layer is properly decoupled from the business layer and the data layer? I've come up with several thoughts about solutions:

MORE ABOUT THE PROBLEM

I'm hoping that this helps clarify some confusion.

We are using ASP.Net MVC. We use routes. We use helpers. We pass flattened DTOs to our web layer, not business objects. This problem concerns the service layer and an explosion of DTOs.

This is mainly a high traffic news site, not a line of business site. It can have many different urls and the urls can change at any time. They can be complex and arbitrarily determined by management.

URL Examples (not real, made up for example purposes).

 1. http://oursite.com/news/wi/politics/supreme-court/recent-ruling-someid
 2. http://oursite.com/news/wi/politics/election-2012/candidate-x-takes-stand-on-issue-y-someid
 3. http://oursite/com/news/politics/mayor-says-things-are-a-ok-someid
 4. http://oursite.com/news/milwaukee/local-bar-named-to-HOF-someid
 5. http://oursite.com/news/wi/politics/supreme-court-someid
 6. http://oursite.com/news/whatever-cat-our-CEO-wants/subcat1/subcat2/etc/2011/10/31/some-story-someid

All of the above are "articles" and we have an Article class. An article has a number of navigation properties, such as AuthorObject, RelatedLinksCollection, etc. Business objects are far too heavy to pass to a client, so we pass DTOs that flatten information (e.g. AuthorName). The above links, however can require different information, even though they are all "articles".

  1. needs Category, Subcategory, Title and Id
  2. needs Category,Subcategory, PoliticsCategory, Title and Id
  3. needs Category, Title and Id
  4. needs Category, Title and Id
  5. needs Category, Subcategory, Title and Id
  6. needs CeoCategory, CeoSubcategory, PublishedDate,Title and Id

In static programming languages, such as c#, the normal way would be to handle this would be to create separate DTO classes. You could add inheritance to reduce some of the code, but you still end up with multiple "article" dto classes.

public class IArticleDto { 
  public string title { get; set; } 
  public string body { get; set; } 
  public string Category { get; set; }}

public class StateArticleDto: IArticleDto { 
  public string title { get; set; } 
  public string body { get; set; } 
  public string Category { get; set; }}
  public string StateCode { get; set; } 
  public string Subcategory { get; set; } 
}

public class SupremeCourtArticleDto: IArticleDto { 
  public string title { get; set; } 
  public string body { get; set; } 
  public string Category { get; set; }}
  public string Subcategory { get; set; } 
}

public class ArbitraryCeoArticleDto: IArticleDto { 
//who knows
}

etc.

The ability to write custom urls in any way possible in not negotiable. If an article relates to something (state, Category, etc.), it can become part of the url.

Solutions?

  1. Continue to add Url objects as needed. How many? At least a dozen, but naming them will be troublesome. Doing one per business object solves the name issue, but that means dozens or hundreds of new objects. Yuck.

  2. IOC - Pass in the route pattern to the Data Access layer via configuration. The data access layer can then create a full url. The url pattern name is still a problem.

  3. Use a Dictionary<TKey, TValue>, KeyValuePair<TKey, TValue>, etc. to pull in.

  4. Use an Expando or DynamicObject for the url details. So url will contain a couple basic properties (name and id), but other properties could be added when necessary.

I'm thinking about using 4), because it seems like something that dynamic programming does better than static languages. However, it may just be that I'm looking at it most, because I like new toys to play with (I haven't used expando before).

It's bettern than 1), because of the object explosion. I'm not sure that 2) will work for complex scenarios. You could pass in simple route name + route information to the data layer using DI, but it seems harder to accomplish with no additional gain. And it probably wouldn't work for complex routes -- for that, I think that you need to have a rules engine on the UI side.

As compared to 3), I think that 4) is slightly better. Someone correct me if I'm mistaken, but dynamic types seem to be no more than syntatic sugar on top of a dictionary, but with the advantage of cleaner code. Just a thought.

John
  • 3,133
  • 5
  • 29
  • 50

4 Answers4

2

I'm going to be blunt and say that the fact that you're asking this question and based on the solutions you're proposing, you're not taking a birds-eye architectural view of the problem at hand. I don't know the intimate details of your application, but being able to have a bit outlook and the ability to picture what it might be used to in the future will help with the URI design.

The solutions you're proposing are all revolving around very concrete, programmatic details, while the problem is abstract and not related to programming at all. Solving the problem is all about writing down the entities in your domain, the actions that are possible to perform on them and how they relate to each other.

Bill de hÓra has a blog post regarding Web resource mapping criteria for frameworks (here's a cached version, should the real one still give HTTP 500 errors) and Joe Gregorio has written about how to RESTify DayTrader that should give you the right idea around how to envision the complete URI space of your application. Planning, drawing, thinking and writing is what it sounds like you and your team need to do.

When the complete scope and space of the URIs in your application is designed, you're ready to implement it. However you then do it is up to you, and then I'd recommend using URI Templates to define the URIs themselves and regular code to map the code that will handle the URIs (be it Controllers, Handlers or whatever). In ASP.NET MVC, the configuration code is written against the RouteTable class, while in OpenRasta it is done against a ResourceSpace class.

Asbjørn Ulsberg
  • 8,212
  • 2
  • 41
  • 60
  • In the services layer, DTOs are used to pass the article information to the web layer. The article dto information includes information related to the information presented on the page, but it also includes "meta" information, such as Author, NewsCategory, Subcategory, State, etc. that is used to construct a url. Article urls can be constructed very differently, even if they are both articles. Ex. /news/wi/politics/supreme-court/recentcase-12345 vs /news/milwaukee/recentstory-456. Other than the url information, these articles have exactly the same properties. – John Oct 31 '11 at 01:04
  • So the "concrete" problem that I am facing is how to best generate these DTOs being passed to the web layer. Multiple DTOs comes at a cost that a dynamic property seems to handle with utterly ease, doesn't it? – John Oct 31 '11 at 01:04
  • Indeed, `dynamic` can work great as an unstructured, (perhaps) volatile DTO if you think static typing yields too much overhead. The important lesson is to decouple the generation of the URIs from the domain like it's done in the above mentioned frameworks. – Asbjørn Ulsberg Nov 04 '11 at 15:06
0

You are correct to see the need to decouple your domain entities (books, magazines, etc) from any awareness of their URL.

The only indentifier you should need to access a book (for example) it its Id - in your example, 12345. Any other taxonomy elements should be handled in the presentation logic, as might you want a different URI structure for non-WWW channels? And if you launch the site multi-lingual? Having /magazines/computers/ persisted in the database would be a hinderance.

The requirements of your SEO czar may change over time, as the techniques to rank higher in search engines change also.

As such, this is a matter of URL routing.

In an ASP.NET Webforms solution, you would add the following entries to your Global.asax file:

void Application_Start(object sender, EventArgs e) 
{
    RegisterRoutes(System.Web.Routing.RouteTable.Routes);
}

public static void RegisterRoutes(System.Web.Routing.RouteCollection routes)
{
    routes.MapPageRoute("Book",
        "{productType}/{categoryName}/{productName}/{productId}",
        "~/Books.aspx");

    routes.MapPageRoute("Magazine",
        "{productType}/{categoryName}/{productName}/{year}/{month}/{day}/{productId}",
        "~/Magazines.aspx");
}

Books.aspx and Magazines.aspx would then collect the relevant parts of the URL, with:

var categoryName = Page.RouteData.Values["categoryName"];

and when you have collected enough information to uniquely identify the product you want to display, you can then query your domain / data tier to get the information you need.

When new product types become available (or a new URL structure is requested by your stakeholders), you simply need to add another route.

christofr
  • 2,619
  • 1
  • 15
  • 19
  • "Any other taxonomy elements should be handled in the presentation logic" – John Oct 30 '11 at 23:56
  • "Any other taxonomy elements should be handled in the presentation logic" -- except that urls often have a /grandfather/father/item_itemid hierarchical structure. The father and grandfather are details stored in the database that the presentation layer has no prior knowledge of Ex. /news/politics/election-2012/latestUSPolls_9876543 – John Oct 30 '11 at 23:59
  • Yes, but you don't need to know the hierarchy to get the item from the database - I don't know your exact model, but it seems like you only need the ID. Why does an object need to know its own URL? What if you wanted multiple URLs for a single product? You need to decouple the URL awareness from your domain objects. – christofr Oct 31 '11 at 00:03
  • You don't need it to get the item from the database of the page that you're on. You need the full details when the item is a link on another page, say an Index page on as related articles on a different story. You might have a list of the most recent stories, but they could be a mix of different stories. A list of supreme court stories might be /news/politics/supreme-court/recent-case-123, whereas a local story might be /news/Milwaukee/local-story-456. – John Oct 31 '11 at 00:42
  • Usually, you would build the links dynamically rather than persist a link in the database or have methods in the object to produce a URL, for the reasons I've gone into above. Consider a helper method which can build the relevant URL from a product or story object. You need to keep in mind that a URL is only relevant to one presentation channel, namely WWW. If you wanted to create a Winforms presentation layer (for instance), you would quickly find your domain objects polluted with URL properties which were no longer relevant. – christofr Oct 31 '11 at 08:56
  • We are building urls dynamically, not storing the absolute url. Please see my section "More about the problem" that I recently added. The problem is how to keep the services layer from looking like gunk due to the number of different potential urls that can be created. – John Oct 31 '11 at 14:46
  • Thanks for the extra information. Dynamic objects and ExpandoObject are geared towards situations where you do not (or can not) know the type of object you're working with until runtime. Your problem is slightly different- you have a potentially large number of similar, but not identical objects. At time of needing to generate a link, you will presumably know which type of article you're dealing with? If this is the case, my feeling is that you should grab the relevant route pattern template from config, then populate it from a Dictionary<..> of article URL elements populated from your DAL. – christofr Oct 31 '11 at 15:30
0

maybe a state machine you could maintain an external DSL.

Main
.*=Product
nj|ny=State
([a-z]*-)*=Title
20[1-9][1-9]->ExpectMonth =Year 
ExpectMonth
[0|1][0-9] = Month -> ExpectDay

ect....

And a T4 to generate struct code

struct Url {
 string Product {get;set}
 string State {get;set;}
}

I think the "monolithic class" antipattern is mitigated somewhat by code generation.

Aaron Anodide
  • 16,154
  • 14
  • 59
  • 117
0

Here is my first go at #4. It works and the dynamic object type works elegantly.

public class ArticleDto 
{
//normal Article properties
//code goes here

//new dyamic property for the Uri Details
public dynamic UriDetails { get; set; }
}

Then I created a custom class to handle the URI details. I added a constructor with name + id, because I always want a resource name + id to be passed with no exceptions.

public class UriDetails : DynamicObject
{
    //TODO put error handling in here to ensure that they're not empty?
    public string ResourceName { get; set; }
    public int ResourceId { get; set; }

    public UriDetails(int resourceId, string resourceName)
    {
        ResourceId = resourceId;
        ResourceName = resourceName;
    }
//other code that you need to override
}

Then, in my DataAccess code

ArticleDto = new ArticleDto();
//Set the regular properties
//code goes here

//Set the mandatory uri properties
ArticleDto.UriDetails = new UriDetails(id, articleTitle);

//Set any other properties specific to this call
ArticleDto.Date = publishedDate;

One note: this solves the problem of the service layer, but the web layer still needs to know how to build the URL. That will be some type of helper class / rules engine that will determine what's used based on the property names & types in UriDetails.

Let me know what you think.

John
  • 3,133
  • 5
  • 29
  • 50