Personal Web Starter Kit LINQed up

In case anyone is interested, I have put together a sample port of the original Personal Web Starter Kit using LINQ rather than the standard ADO data tier in the PhotoManager.vb class. With this version, we can eliminate all of the stored procedures and rely on LINQ for our entire data access. In this implementation, I intentionally attempted to retain the original method signatures where possible to make migration more seamless. The project site is at http://code.msdn.microsoft.com/LinqPersonalWeb.

This is one of the sample projects I use in my LINQ Migration strategies talks, so if you attended that talk, check out the sample project for some more concrete examples. Because the original example is fairly basic and the tiers are separated out neatly, doing a migration really only requires replacing code in one file: PhotoManager.vb. Let's take a look at a couple of the refactorings we did for this example.

The meat of the original project is a solution to group images into albums and store them in a database. To start, we create a new mapping file by creating a new LINQ to SQL Classes file. Onto this surface, drag the Album and Photo tables to generate the entity classes and associated mappings. With that in place, we can move our attention to the PhotoManager class which abstracts all of the data access.

The PhotoManager class has separate methods to GetPhoto, GetPhotos, AddPhoto, EditPhoto and RemovePhoto. The same is true for albums. Each of these maps to corresponding stored procedures. Let's compare the original implementation of GetPhoto with the LINQ enabled version. In the original, we see familiar code to create a datareader and Fetch a scalar result from the function.

Public Overloads Shared Function GetPhoto(ByVal photoid As Integer, ByVal size As PhotoSize) As Stream
  Using connection As New SqlConnection(ConfigurationManager.ConnectionStrings("Personal").ConnectionString)
    Using command As New SqlCommand("GetPhoto", connection)
      command.CommandType = CommandType.StoredProcedure
      command.Parameters.Add(New SqlParameter("@PhotoID", photoid))
      command.Parameters.Add(New SqlParameter("@Size", CType(size, Integer)))
      Dim Filter As Boolean = Not (HttpContext.Current.User.IsInRole("Friends") Or HttpContext.Current.User.IsInRole("Administrators"))
      command.Parameters.Add(New SqlParameter("@IsPublic", Filter))
      connection.Open()
      Dim result As Object = command.ExecuteScalar
      Try
       
Return New MemoryStream(CType(result, Byte()))
      Catch
       
Return Nothing
     
End Try
   
End Using
 
End Using
End Function

The corresponding Stored Procedure is as follows:

CREATE PROCEDURE GetPhoto
 
@PhotoID int,
  @Size int,
  @IsPublic bit

AS
 
IF @Size = 1
    SELECT TOP 1 [BytesThumb] FROM [Photos] LEFT JOIN [Albums] ON [Albums].[AlbumID] = [Photos].[AlbumID] WHERE [PhotoID] = @PhotoID AND ([Albums].[IsPublic] = @IsPublic OR [Albums].[IsPublic] = 1)
  ELSE IF @Size = 2
    SELECT TOP 1 [BytesPoster] FROM [Photos] LEFT JOIN [Albums] ON [Albums].[AlbumID] = [Photos].[AlbumID] WHERE [PhotoID] = @PhotoID AND ([Albums].[IsPublic] = @IsPublic OR [Albums].[IsPublic] = 1)
  ELSE IF @Size = 3
    SELECT TOP 1 [BytesFull] FROM [Photos] LEFT JOIN [Albums] ON [Albums].[AlbumID] = [Photos].[AlbumID] WHERE [PhotoID] = @PhotoID AND ([Albums].[IsPublic] = @IsPublic OR [Albums].[IsPublic] = 1)
  ELSE IF @Size = 4
    SELECT TOP 1 [BytesOriginal] FROM [Photos] LEFT JOIN [Albums] ON [Albums].[AlbumID] = [Photos].[AlbumID] WHERE [PhotoID] = @PhotoID AND ([Albums].[IsPublic] = @IsPublic OR [Albums].[IsPublic] = 1)
  ELSE
   
SELECT TOP 1 [BytesPoster] FROM [Photos] LEFT JOIN [Albums] ON [Albums].[AlbumID] = [Photos].[AlbumID] WHERE [PhotoID] = @PhotoID AND ([Albums].[IsPublic] = @IsPublic OR [Albums].[IsPublic] = 1)

RETURN

For those unfamiliar with the sample solution, realize that the photos table contains multiple copies of each image rendered in different resolutions. The procedure is responsible for determining which field to return based on the size parameter that is passed in. By limiting the fields returned, we can optimize the IO requirements, but this does mean that any time we want to access the photos, we need to repeat the same logic. Indeed, the same If block used in the GetPhoto procedure is copied and reused in the GetFirstPhoto procedure. This does not lead for the kind of code maintainability we would like to see.

Naturally, since your reading about it here, I'm sure you would like to see how LINQ may offer a better alternative. Starting with the GetPhoto method, we can eliminate the late bound ADO code and provide a more strongly typed version of the same method. We will also be able to refactor and reuse more pieces of the query through-out the application. Here is the code for our GetPhoto method.

Public Overloads Shared Function GetPhoto(ByVal photoid As Integer, ByVal size As PhotoSize) As Stream
  Dim dc As New PersonalDataContext
  Dim query = From p In dc.Photos _
                      Where (p.PhotoID = photoid) And _
                                 (p.Album.IsPublic Or IsFriend)
  Return GetPhotoBytes(query, size)
End Function

Get photo greatly reduces the amount of plumbing code and allows us to focus on the desired results. GetPhoto returns the actual image that corresponds to the requested ID and size. It also checks to see if the user is allowed to see that photo by checking to see if the album is marked as a public album, or if the user is considered a friend based on their login credentials. Here's the implementation of the IsFriend method.

Public Shared Function IsFriend() As Boolean
  
Return (HttpContext.Current.User.IsInRole("Friends") Or _
              HttpContext.Current.User.IsInRole("Administrators"))
End Function

Since the user's credentials are already cached for the current user, there is no need to requery that part of the database on every fetch.  The real key to this implementation lies in the GetPhotoBytes method. In this method, we evaluate the size parameter and dynamically extend our query to project just the field we want to consume.

Private Shared Function GetPhotoBytes(ByVal source As IQueryable(Of Photo), ByVal size As PhotoSize) As Stream
  Dim imageBytes As Byte()
  Select Case size
    Case PhotoSize.Large
      imageBytes = source.Select(Function(p) p.BytesFull).SingleOrDefault
    Case PhotoSize.Original
      imageBytes = source.Select(Function(p) p.BytesOriginal).SingleOrDefault
    Case PhotoSize.Small
      imageBytes = source.Select(Function(p) p.BytesThumb).SingleOrDefault
    Case Else
     
imageBytes = source.Select(Function(p) p.BytesPoster).SingleOrDefault
  End Select
 
If imageBytes IsNot Nothing Then
   
Return New MemoryStream(imageBytes)
  Else
   
Return New MemoryStream()
  End If
End Function

Here we extend the initial query and add custom projection to it. When we issue the query to the database, the resulting SQL statement wraps the functionality we declared in the GetPhoto with the GetPhotoBytes to create a single statement which only returns the image stream that we requested. What's better is that we can now reuse this same GetPhotoBytes method in the GetFirstPhoto implementation, passing a different baseline query.

Public Shared Function GetFirstPhoto(ByVal albumid As Integer, ByVal size As PhotoSize) As Stream
  Dim dc As New PersonalDataContext
  Dim query = From p In dc.Photos _
                      Where p.AlbumID = albumid And (p.Album.IsPublic Or IsFriend()) _
                      Take 1
  Return GetPhotoBytes(query, size)
End Function

There. Nicely refactored and no more copy-paste inheritance in the database.

If you're interested in looking at this implementation further, check out the project site at http://code.msdn.microsoft.com/LinqPersonalWeb. Also, let me know if you would like to see other starter kits migrated to LINQ and I'll see what I can do.

Posted on 2/10/2008 2:47:00 PM - Comments (5)
Categories: VB Dev Center , LINQ , VB

  • #Posted on 2/12/2008 11:30:00 AM by Roger Jennings
    Pingback from http://oakleafblog.blogspot.com/2008/02/linq-and-entity-framework-posts-for_11.html
  • #Posted on 3/5/2008 11:35:00 PM by Steven Foster Murray
    Hey... I am a big fan of the original personal web site releae. It really helped me understand asp.net etc. LinqPersonal Web site in c#???? Once again... Thanks!
  • #Posted on 3/6/2008 1:24:00 PM by Steven Foster Murray
    Great way to learn about Linq-SQL... question about the vb code. Many vb function have ... Dim dc As New PersonalDataContext. Is this too much overhead... should the DataContext object be cached and shared??? Thanks SFM!
  • #Posted on 3/6/2008 9:57:00 PM by Jim
    Steven, In a windows application, you can keep the context for the life of a form or specific need, it should not be cached for the life of the application. Consider that the context retains a copy of each object it fetches. Keeping the context alive outside of a postback would cause unnecessary bloat and could end up causing the context to replicate the database in memory. Additionally, persisting the context accross session could cause problems in a web farm environment. If you were to persist it for the app domain, you would also loose the ability to maintain concurrency checks cross users. The general recommendation is to keep the scope of the context short lived, at the most for the scope of a single post/request cycle. In addition, I purposedly choose to keep the same method signatures in the PersonalWeb re-write to show how you can fit within a specific architecture. In the original, the connection was repeatedly created, so I didn't have as much problems doing the same with the context. As for your other post, I'm not planning on doing a C# port, but would recommend you taking a stab at it yourself. One of the best ways to learn is to do.
  • #Posted on 3/8/2008 7:13:00 PM by Steven Foster Murray
    Great, Great advice... Thanks. And yes, I have successfully completed the port to c#. It was a terrific learning experience. The only way to really learn this stuff. I really like using Linq with both SQL and regular .net objects. I will probably buy the pdf version of your book. Once again... thanks SFM!



 
Save Comment