Building a mostly real-time web-based Twitter client with ASP.NET MVC, jQuery and TweetSharp, Part 1

Back when I launched ManagedAssembly.com around MIX I put together a page to show real-time Twitter search results for ‘MIX09’. On the heels of the dvplrs.com launch this week and the lack of .NET representation, I decided to update my live Twitter page to show live updates of notable people in the .NET community. This is how I built it. You can see the finished product over at ManagedAssembly.com/Twitter.

Since I decided to use the standard Twitter API instead of the Search API, I had to build a server-side cache of updates so that I wasn’t hitting the API on every view of the page which would quickly exhaust the API rate limits. Using a database was overkill so I decided to use ASP.NET’s built-in cache mechanism to store new updates. When the client makes a request for the latest updates, the items will be served out of the cache. The cache is updated every so often with new items.

The workflow when the client makes a request for updates (via jQuery) is like so:

  • Grab the date of the last cache update from the cache. If the value doesn’t exist or it’s older than the cache expiration threshold, it’s time to get more statuses from Twitter.
  • When new updates are pulled from Twitter, store the highest ID in the cache as well so that for future requests we can get only the new tweets.
  • Pull the currently cached items and add the new items from the Twitter API call to it.
  • When updates are sent to the browser, the client-side code stores the latest ID. For future requests this ID is passed back to the server which returns items from the cached list that are greater than the locally-stored ID.

What this gives us is the ability to only call Twitter once a minute no matter how many times the site is hit. It also allows for each visitor to only get back tweets that are new to them (which is different for every visitor depending on when the first hit the page) whenever they make a request. Since the tweets are being pulled from memory on almost every request there’s no delay while the API call is being made. If the application restarts and we lose the cache it won’t really matter because we just grab all the updates it will send and repopulate the cache. It also means that the amount of data being sent to the client via JSON is the bare minimum needed. That makes updating the UI simple (just append the new ones without having to process the full list) and keeps the bandwidth requirements low.

That was a lot of words, so time for some code. We start by determining if we need to get new items from Twitter and if so, grabbing them. This uses the CacheManager from a previous post.

CacheManager cache = new CacheManager(new ShortTermProvider());
 
IEnumerable<TwitterStatus> statuses = new List<TwitterStatus>();
long maxId = 0;
 
DateTime lastFetch = cache.Get<DateTime>("last_fetch");
if (lastFetch < DateTime.Now.AddMinutes(-1)) {
    long lastCacheId = cache.Get<long>("last_id");
    if (lastCacheId == 0)
        lastCacheId = 1; // TweetSharp/twitter returns null if you use Since(0)
 
    var request = FluentTwitter.CreateRequest().AuthenticateAs("UserGoesHere", "passwordgoeshere");
    request.Configuration.UseGzipCompression();
    request.Statuses().OnFriendsTimeline().Since(lastCacheId).AsJson();
 
    statuses = request.Request().AsStatuses();
}

The first time this is hit lastFetch doesn’t exist in cache and the cache manager returns the default for DateTime (DateTime.MinValue) which I’m pretty sure is always more than a minute ago. We’ll update the fetch time in cache later. Once we’ve determined that the cache is empty or expired, we grab the last_id value from cache (which is 0 if it doesn’t exist). We then use TweetSharp to get the latest updates and store them in a list.

We also make sure that statuses isn’t null because if you’ve exceed the Twitter API rate limit, TweetSharp returns a null collection. Once we have the updates we have to process them since the text doesn’t include links for @mentions, #hashtags or links. When deciding how to approach this part I contacted Jon Galloway and he pointed me to the code in Witty which handles creating links, which I borrowed heavily from (thanks Jon!). MA also uses SubSonic 2.2 for the DAL and I used one of the methods provided in that library for determining if a word is a URL.

if (statuses != null) {
    foreach (var status in statuses) {
        string raw = status.Text;
        string[] words = Regex.Split(raw, @"([ \(\)\{\}\[\]])");
        StringBuilder output = new StringBuilder();
        foreach (string word in words) {
            if (word.StartsWith("#")) {
                string hashtag = String.Empty;
                Match foundHashtag = Regex.Match(word, @"#(\w+)(?<suffix>.*)");
                if (foundHashtag.Success) {
                    hashtag = foundHashtag.Groups[1].Captures[0].Value;
                    output.Append(string.Format(@"#<a href=""http://search.twitter.com/search?q=%23{0}"" target=""_blank"">{0}</a>", hashtag));
                }
            }
            else if (word.StartsWith("@")) {
                string userName = String.Empty;
                Match foundUserName = Regex.Match(word, @"@(\w+)(?<suffix>.*)");
                if (foundUserName.Success) {
                    userName = foundUserName.Groups[1].Captures[0].Value;
                    output.Append(string.Format(@"@<a href=""http://twitter.com/{0}"" target=""_blank"">{0}</a>", userName));
                }
            }
            else if (SubSonic.Sugar.Validation.IsURL(word)) {
                output.Append(string.Format(@"<a href=""{0}"" target=""_blank"">{0}</a>", word));
            }
            else {
                output.Append(word);
            }
        }
 
        status.Text = output.ToString();
    }
 
    cache.Store("last_fetch", DateTime.Now);
}

OK now that we’ve got the latest updates from the API and added the links, it’s time to store them in the cache.

var cachedStatuses = cache.Get<List<TwitterStatus>>("cachedStatuses");
if (cachedStatuses == null) {
    cachedStatuses = new List<TwitterStatus>();
}
 
if (statuses != null) {
    cachedStatuses.AddRange(statuses);
}
 
if (cachedStatuses.Count > 0) {
    maxId = cachedStatuses.Max(s => s.Id);
    cache.Store("last_id", maxId);
}
 
cache.Store("cachedStatuses", cachedStatuses);

Last but not least we build a JSON return value for all the tweets in the cache with an ID higher than the one sent by the client (lid is a parameter passed from the client).

long lastClientId = lid ?? 1;
 
var data = from s in cachedStatuses
           where s.Id > lastClientId
           orderby s.Id descending
           select new {
               Id = s.Id,
               ProfileImageUrl = s.User.ProfileImageUrl,
               Text = s.Text,
               Source = s.Source,
               UserName = s.User.ScreenName,
               RelativeTime = s.CreatedDate.ToRelativeTime(false),
               ClientLink = s.Source,
               IsReply = s.InReplyToStatusId != 0,
               InReplyToId = s.InReplyToStatusId,
               InReplyToUser = s.InReplyToScreenName
           };
 
return Json(new { results = data.Take(100), max_id = maxId });

In Part 2 I’ll cover building the UI.

Posted May 25th, 6:51 PM
Read more posts about .NET, ASP.NET MVC, C#, Managed Assembly, Programming, SubSonic, Tips, jQuery.

View Comments
Link

Post-MIX Thoughts

I had a hunch I’d be too tired to write up a day-by-day recap of my first MIX conference which turned out to be exactly right. You’ve probably heard by now all the stuff that was announced, so I’ll try to keep this an opinion piece instead of a news article.

The Good

Prior to attending I had read that the most valuable part of conferences like MIX was not the presentations, but the people. This was absolutely and completely true. There’s no substitute for having conversations with brilliant people and there were a ton of brilliant people at the conference. I had good conversations with Phil Haack, Jeff Atwood and the Stack Overflow devs Jarrod and Geoff, Joel Spolsky (possibly the highlight of my career), Rob Conery, Jon Galloway, Scott Hanselman, Dave Ward from Encosia, Justin Etheredge, some guys from Woot and many more. Learning the how and why of how they do things is educational and inspiring.

Thankfully, Microsoft has realized the importance of these types of interactions and does a great job of facilitating them. They set up a room called “3rd Place” with couches, snacks, free wifi and power strips. It was a great setup and worth the price of admission alone. If you’re at MIX next year and none of the sessions at any given time really inspire you, go hang out at 3rd Place and strike up conversations instead. You can always watch the session video later.

One conversation I’m particularly enthused about is the meeting I had with Rob Conery of SubSonic fame. SubSonic has gone a little stale and needs some freshening up. We discussed some issues getting in the way of progress and some ideas for going forward. I’m going to try to get it kick started again, which I will blog about when that happens. I love SubSonic and am excited to start contributing to it’s future.

I also showed off Managed Assembly when I had a chance and got some good feedback on the site both design-wise and vision-wise. Now that it’s up and running, we’ll see how it goes for a little bit before I make any significant changes.

MIX also does a good job of keeping a good balance of content for designers and developers. I think all devs should have some basic understanding of good design principles and MIX does a good job of exposing that without overdoing it for devs. Bill Buxton and Deborah Adler (she designed the new Target prescription packaging) gave insightful keynotes that had some good design insights for both designers and developers.

The Not-As-Good

One reason 3rd Place was more appealing to me than the sessions is a matter of timing. ASP.NET MVC went final, but there was very little new content that wasn’t already covered at PDC or in blog posts since. Rob Conery’s session stood out because it was the only one that had more stuff in it that I hadn’t seen than stuff that I had seen. It was great. The video has been posted so you should watch it. Almost everything else that was new was tied to Silverlight. Which brings me to…

The ‘meh’

Silverlight 3. Meh. It’s nice. It does cool stuff. It now works out of the browser, which has huge potential. Adobe AIR now has some serious competition for building Twitter clients (ba-dum-dum). I did learn some things about Silverlight I didn’t know (like support for threading), but overall, I just don’t care that much about it yet.

The Bad

IE8. OK, IE8 isn’t so bad, but until IE6 is dead, it’s completely irrelevant. Slices? Yawn. Accelerators? Yawn. I don’t want to write browser-specific code anymore. The IE intro video was pretty funny though.

Was it worth the trip?

Absolutely. If you’re a MS web dev or a designer that works with them, you should be at MIX. You’ll learn a lot from other people and the sessions. You’ll have a chance to talk directly to the people who build the tools that are the basis for your profession and give them feedback and ideas from the real world. You’ll have fun because it’s in Vegas. Try to make it next year. You won’t regret it.

Posted March 21st, 1:12 PM
Read more posts about ASP.NET MVC, Life, Programming, SubSonic, Thoughts.

View Comments
Link

How I Use SubSonic, Part 3: Caching and Query Auditing

In Part 1 I covered how I created a ControllerBase class that all my SubSonic queries run through. Once I had a central point for processing queries, I wanted to add two features. Request-length caching and the ability to see how long a query was taking to execute.

Adding Request-length Caching

First off, this will only work as shown with web applications due to a dependency on HttpContext.Items. You could easily modify this to work without this dependency.

The way my templates are structured gives me access to related objects through lazy-loaded properties on my domain objects. All of these properties use the Get(id) method on their respective Controller. Throughout the length of a single request, there’s no reason to retrieve a given entity from the database more than once. Using my CacheManager I added the following to the ControllerBase.Get() method:

public static ItemType Get(object id)
{
    if (id == null)
        return default(ItemType);

    CacheManager cache = new CacheManager(new RequestProvider());
    string cacheToken = string.Format("{0}_{1}", typeof(ItemType), id);

    ItemType item = cache.Get(cacheToken);

    if (item == null)
    {
        item = DB.Get(id);

        // subsonic returns a new item if a get by ID fails
        // but we want a null object
        if (item.IsNew)
        {
            return default(ItemType);
        }
        else
        {
            cache.Store(cacheToken, item);
        }
    }

    return item;
}

The cache is first checked for an existing object matching the type and ID. If it’s not found, the entity is retrieved from the database and stored in the cache. If it is found, the database hit is skipped. There’s also a section in there to handle how SubSonic deals with .Get() calls that don’t find a record. I prefer to return null in this case, but your preference may vary.

I don’t yet cache the results of collections, only single objects. Caching the results of collections is an intriguing problem that I may revisit in the future.

Query Auditing

After caching reduced the number of times I hit the database on each request, I wanted to find out how long individual queries were taking so I could optimize where needed. I also wanted a way to print out a list of every query called with the SubSonic-generated SQL.

First up was creating the Auditor class to handle creating and storing entries. I don’t want to spend time on the internals, so you can view the source if you want to get a sense of how it works. Briefly, the heart of the Auditor is the CreateEntry method that takes a SubSonic SqlQuery, Query or StoredProcedure and stores it for display later. There’s also Auditor.StartTiming() and Auditor.EndTiming() for determining how long the query took to execute. Each of the methods in ControllerBase that execute queries get the Auditor mixed in like so:

public static ListType GetCollection(SqlQuery q)
{
    Auditor.StartTiming();
    ListType coll = q.ExecuteAsCollection
();
    TimeSpan exTime = Auditor.StopTiming();

    Auditor.CreateEntry(q, Action.Fetch, coll.Count, exTime);

    return coll;
}

And the updated Get() method:

public static ItemType Get(object id)
{
    if (id == null)
        return default(ItemType);

    CacheManager cache = new CacheManager(new RequestProvider());
    string cacheToken = string.Format("{0}_{1}", typeof(ItemType), id);

    ItemType item = cache.Get(cacheToken);

    if (item == null)
    {
        Auditor.StartTiming();
        item = DB.Get(id);

        // subsonic returns a new item if a get by ID fails
        // but we want a null object
        if (item.IsNew)
        {
            Auditor.CreateEntry(cacheToken, Action.Fetch, 0, null, Auditor.StopTiming());
            return default(ItemType);
        }
        else
        {
            Auditor.CreateEntry(cacheToken, Action.Fetch, 1, null, Auditor.StopTiming());
            cache.Store(cacheToken, item);
        }
    }
    else
    {
        Auditor.CreateEntry(cacheToken, Action.LoadFromCache, 1, null, TimeSpan.Zero);
    }

    return item;
}

The Auditor stores the query stats in a List<Entry> in HttpContext.Current.Items. To display these entries, I wrote the following Webforms UserControl (have not yet migrated this to MVC).

<%@ Control Language="C#" AutoEventWireup="true" CodeFile="Auditor.ascx.cs" Inherits="AuditorControl" %>
<asp:Repeater ID="rItems" runat="server" Visible="false" OnItemDataBound="rItems_ItemDataBound">
    <HeaderTemplate>
        <table class="list" style="font-size: 11px;">
            <tr class="header">
                <th>Type</th>
                <th>Count</th>
                <th>Time</th>
                <th class="left">Description</th>
                <th class="left">Params</th>
            </tr>
    </HeaderTemplate>
    <ItemTemplate>
        <tr class="item">
            <td class="center top" style="width: 30px;"><%# Eval("Action") %></td>
            <td class="center top" style="width: 30px;"><%# Eval("ResultCount") %></td>
            <td class="center top" style="width: 30px;"><%# ((TimeSpan)Eval("ExecutionLength")).Milliseconds %>ms</td>
            <td class="top"><%# Eval("DescriptionHtml") %></td>
            <td class="top" style="width: 150px;">
                <asp:Repeater ID="rParams" runat="server">
                    <ItemTemplate>
                        <strong><%# Eval("Key") %>:</strong>
                        <span style="white-space: nowrap;"><%# Eval("Value") %></span><br />
                    </ItemTemplate>
                </asp:Repeater>
            </td>
        </tr>
    </ItemTemplate>
    <FooterTemplate>
        </table>
    </FooterTemplate>
</asp:Repeater>

And the code behind:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using SubSonicAuditor;

public partial class AuditorControl : System.Web.UI.UserControl
{
    protected void Page_Load(object sender, EventArgs e)
    {
        if (Auditor.Entries.Count > 0)
        {
            rItems.DataSource = Auditor.Entries;
            // or to exclude cache retrieval:
            // rItems.DataSource = Auditor.Entries.Where(en => en.Action != Action.LoadFromCache);
            rItems.DataBind();
            rItems.Visible = true;
        }
    }

    protected void rItems_ItemDataBound(object sender, RepeaterItemEventArgs e)
    {
        if (e.Item.ItemType == ListItemType.AlternatingItem || e.Item.ItemType == ListItemType.Item)
        {
            Repeater rParams = (Repeater)e.Item.FindControl("rParams");
            Entry entry = (Entry)e.Item.DataItem;

            rParams.DataSource = entry.Parameters;
            rParams.DataBind();
        }
    }
}

Put this user control on any page you want to test (or your site’s master page), make sure you’re running in Debug mode and you should get a list of all the queries executed and how long they took. Here’s a (heavily-blurred for security) example of the output.

Posted January 20th, 8:06 PM
Read more posts about .NET, SubSonic, Tips.

View Comments
Link

How I Use SubSonic, Part 2

In Part 1, I covered building a ControllerBase class to use with SubSonic 2.1’s new RepositoryRecord entity base class. In order to get the hybrid API (RepostoryRecord + Foreign Key lazy-loaded properties) I like, I had to make some changes to the code generation templates. Here’s what I do.

First off, go into Program Files\SubSonic 2.1 Final\src\CodeGeneration\Templates and copy the CS_ templates to a new folder. I put them in the root of my project so that I can keep them in under version control. Then, in your app.config or web.config, add the following to make sure that future generations will use the modified copy of the templates:

   1:  <SubSonicService defaultProvider="NorthwindProvider"
   2:                   templateDirectory="C:\Full_Path_To\SubSonicTemplates">
   3:      <providers>
   4:          <clear/>
   5:          <add ... />
   6:      </providers>
   7:  </SubSonicService>

Another thing you should know before we continue is an important naming convention I make sure to use for foreign key fields. My foreign key fields ALWAYS end in ‘ID’. That’s pretty standard. But when I have a table that has multiple foreign key fields that relate to the same table, I make sure the field name represents what the relation is, even if the field name doesn’t match the foreign table name. For example, my goal with this is to be able to do this:

   1:  // News.Author is a User object
   2:  string authorName = News.Author.DisplayName;
   3:  // News.Editor is also a User object
   4:  string editorName = News.Editor.DisplayName;

The default SubSonic templates don’t handle this situation very well. The first FK it encounters is named News.User and the second gets a funky name I don’t even recall. It can be very confusing to use. Since I always make sure my columns are named a specific way, I decided that I wanted my property names to just lop off the ‘ID’ suffix and use whatever was left. Since a table can’t have two columns with the same name, I never have naming conflicts. Updating the CS_ClassTemplate.aspx template to handle this is straightforward:

   1:  // replace this section
   2:  string fkClass = fk.ClassName;
   3:  string fkClassQualified = provider.GeneratedNamespace + "." + fkClass;
   4:  string fkMethod = fk.ClassName;
   5:  string fkID = tbl.GetColumn(fk.ColumnName).PropertyName;
   6:  string fkColumnID = fk.ColumnName;
   7:   
   8:  // with this section
   9:  string fkClass = fk.ClassName;
  10:  string fkClassQualified = provider.GeneratedNamespace + "." + fkClass;
  11:  string fkID = tbl.GetColumn(fk.ColumnName).PropertyName;
  12:  string fkMethod = fkID.Substring(0, fkID.Length - 2);
  13:  string fkColumnID = fk.ColumnName;

There’s a few code blocks that immediately follow this to check for relations to itself and duplicate method/property names. I remove those checks because my naming convention never allow for those conflicts to exist.

Because I’m using the RepositoryRecord base class, when these FK properties are called, I need to make sure they call the .Get method on the correct controller. Again, SubSonic makes this easy.

Replace this section:

   1:  /// <summary>
   2:  /// Returns a <%=fkClass%> ActiveRecord object related to this <%=className%>
   3:  /// 
   4:  /// </summary>
   5:  public <%=fkClassQualified%> <%=fkMethod%>
   6:  {
   7:      get { return <%=fkClassQualified%>.FetchByID(this.<%=fkID%>); }
   8:      set { SetColumnValue("<%=fkColumnID%>", value.<%=fkTbl.PrimaryKey.PropertyName%>); }
   9:  }

With this:

   1:  /// <summary>
   2:  /// Returns a <%=fkClass%> ActiveRecord object related to this <%=className%>
   3:  /// 
   4:  /// </summary>
   5:  protected <%=fkClassQualified%> m_<%=fkMethod%> = null;
   6:  public <%=fkClassQualified%> <%=fkMethod%>
   7:  {
   8:  <% if (baseClass != "ActiveRecord") { %>
   9:      get { 
  10:          if (m_<%=fkMethod%> == null)
  11:               m_<%=fkMethod%> = <%=fkClassQualified%>Controller.Get(this.<%=fkID%>); 
  12:          return m_<%=fkMethod%>;
  13:      }
  14:  <% } else { %>
  15:      get { return <%=fkClassQualified%>.FetchByID(this.<%=fkID%>); }
  16:  <% } %>
  17:      set { SetColumnValue("<%=fkColumnID%>", value.<%=fkTbl.PrimaryKey.PropertyName%>); }
  18:  }

Not only are we now using the Controller to retrieve the item, we’re also storing it in a member variable so every time we call that property for this instance, we only hit the database once.

The last change you need to make to CS_ClassTemplate.aspx is to remove the checks for ActiveRecord so that foreign key properties are still generated even if you select RepositoryRecord. Download the files at the end of the post to see this in action.

There’s also a small change that needs to be added to CS_StructsTemplate.aspx to support my ControllerBase.Delete(int id) customization. Add this between the existing Delete and Destroy methods:

   1:  public static void Delete<T>(int keyId) where T : RepositoryRecord<T>, new() 
   2:  {
   3:      Repository.Delete<T>(keyId);
   4:  }
   5:  public static void Destroy<T>(int keyId) where T : RepositoryRecord<T>, new() 
   6:  {
   7:      Repository.Destroy<T>(keyId);
   8:  }

If you use GUIDs or something else for your key, you’ll have to adjust accordingly (HOWEVER, I am working on committing some new methods to the SubSonic core to allow deleting by key for all data types…stay tuned).

To use these customizations in your own projects, download my templates here.

Coming soon is Part 3 which will cover using the ControllerBase to easily add request-length (or longer) caching and query performance auditing. If you have any comments or feedback, I’d love to hear it below!

Posted October 26th, 5:46 PM
Read more posts about .NET, SubSonic, Tips.

View Comments
Link

How I Use SubSonic, Part 1

I started heavily using SubSonic right around the time Rob Conery released his poorly-named ‘MVC Templates’. I was never a fan of the ActiveRecord pattern, but I really liked the Repository pattern used in the MVC Templates. The controllers generated by these templates were partial classes that inherited a base class that offered some common methods like List, Get, Delete, Save, etc. For each project, I would build up a pretty substantial set of controllers and I would have all my data access code in a nice centralized library.

The only place I made any real changes was with the templates themselves. Rob’s shipping version didn’t include lazy-loaded properties or methods for foreign keys like ActiveRecord. I really liked this feature, so I added them back to the templates using the controllers’ Get methods to call the correct item.

This worked great for awhile until I wanted to extend the base class. Despite repeatedly asking Rob for the source to his unofficial branch (and never getting an email back), I was stuck with what was there for awhile. Then along came SubSonic 2.1 and the new RepositoryRecord base class.

I really wanted to use the new SqlQuery capabilities so off I went to migrate my existing Controllers. In Part 1, I’ll cover recreating the ControllerBase class. In Part 2, I’ll outline my changes to the code gen templates. In Part 3, I’ll cover some caching and query metrics this architecture let me easily add.

This is the class definition I needed to replicate:

Old SubSonic ActiveController

The methods I used most frequently were:

  • Delete(object keyID): In my controllers, I would add a Delete(ItemType item) method, handle any pre-delete logic there, and then call Delete(keyID) within my method.
  • Get(object key): This method was called extensively in my generated objects when loading foreign key properties. Also used whenever I need a single item.
  • List(): Rarely used since it’s the equivalent of "SELECT * FROM Table" which isn’t very useful except for in my utility classes. Still used however.
  • List(IDataReader rdr): I used this with my StoredProcedures to get a strongly-typed collection from the results of calling the sprocs.
  • List(Query q): All other non-sproc collection loading was done here by passing a Query object from my Controllers.
  • Save(ItemType item, string, userName): In my controllers, I would add a Save(ItemType item) method overload, handle all the saving logic and validation, then call this method when I needed to finalize the changes. (Yes, this ended up with a lot of business-logic leaking into my DAL, but that was my fault, not SubSonic’s and it is currently being remedied).

My new ControllerBase needed to support all of the above scenarios and work with the new SqlQuery object.

I started with the easiest methods to replicate:

   1:  public class ControllerBase<ItemType, ListType>
   2:      where ItemType : RepositoryRecord<ItemType>, new()
   3:      where ListType : RepositoryList<ItemType, ListType>, new()
   4:  {
   5:      public static ListType List()
   6:      {
   7:          return DB.Select().From<ItemType>().ExecuteAsCollection<ListType>();
   8:      }
   9:   
  10:      public static void Delete(int id)
  11:      {
  12:          DB.Delete<ItemType>(id);
  13:      }
  14:   
  15:      public static void Save(ItemType item, string username)
  16:      {
  17:          DB.Save(item, username);
  18:      }
  19:  }

SubSonic pros will know there’s no DB.Delete(int id) method in SubSonicRepository. You’ll have to download the source and add it yourself if you want to replicate this solution exactly. Here’s the code to add to SubSonicRepository.cs (you’ll also need to update ISubSonicRepository):

   1:  public void Delete<T>(int itemId) where T : RepositoryRecord<T>, new()
   2:  {
   3:      T item = new T();
   4:      TableSchema.Table tbl = item.GetSchema();
   5:      string pkColumn = tbl.PrimaryKey.ColumnName;
   6:      Delete<T>(pkColumn, itemId);
   7:  }

OK, back to ControllerBase. Next up was replicating the List overloads for building a collection from a Query object and a Stored Procedure.

   1:  public static ListType List(Query query)
   2:  {
   3:      ListType coll = GetCollectionFromReader(query.ExecuteReader());
   4:      return coll;
   5:  }
   6:   
   7:  public static ListType List(StoredProcedure s)
   8:  {
   9:      ListType coll = GetCollectionFromReader(s.GetReader());
  10:      return coll;
  11:  }
  12:   
  13:  protected static ListType GetCollectionFromReader(IDataReader rdr)
  14:  {
  15:      ListType coll = new ListType();
  16:      coll.LoadAndCloseReader(rdr);
  17:      return coll;
  18:  }

And the Get method:

   1:  public static ItemType Get(object id)
   2:  {
   3:      if (id == null)
   4:          return default(ItemType);
   5:   
   6:      ItemType item = DB.Get<ItemType>(id);
   7:   
   8:      return item;
   9:  }

Supporting the new SqlQuery object was trivial:

   1:  public static ListType List(SqlQuery q)
   2:  {
   3:      ListType coll = q.ExecuteAsCollection<ListType>();
   4:      return coll;
   5:  }

That was all I needed for backwards compatibility. I then added a new method with overloads for Query, SqlQuery and StoredProcedure to return a single item (thanks to Rob for responding to my feature request in the forums and implementing ExecuteSingle<T>()):

   1:  public static ItemType GetFirst(SqlQuery q)
   2:  {
   3:      return q.ExecuteSingle<ItemType>();
   4:  }
   5:   
   6:  public static ItemType GetFirst(Query q) // also created an overload for StoredProcedure
   7:  {
   8:      ListType coll = List(q);
   9:   
  10:      if (coll.Count == 0)
  11:      {
  12:          return default(ItemType);
  13:      }
  14:      else
  15:      {
  16:          return coll[0];
  17:      }
  18:  }

And there you have it. A backwards-compatible controller base class that lets you use the new features of SubSonic 2.1. You can download the complete class for use in your own projects. Part 2 (hopefully tomorrow) will cover my SubSonic template customizations.

Posted October 22nd, 10:54 PM
Read more posts about .NET, SubSonic.

View Comments
Link

My Projects

ManagedAssembly

RestSharp

jQuery Snippets for Visual Studio 2010

@dotnetlinks on Twitter

SnapLeague