In your architecture, how do you decouple a url from the database layer & business objects layers

576 views Asked by At

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.

4

There are 4 answers

0
John On

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.

1
Aaron Anodide On

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.

7
christofr On

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.

3
Asbjørn Ulsberg On

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.