The power of interface polymorphism

The problem

Apparently there are lots of developers out there that don't exactly know (or haven't actually thought of) how to separate their data access layer from the business or even the presentation layer in their projects and always get into trouble when they have to insert something new into their DAL or Business Layer.

For about two weeks now I've been developing a new blog engine based on ASP.NET MVC (BETA) and I was wondering how I could make it database independent because I would like the engine to be able to use XML files as the backend, Sql Server 2005, MySql or even Oracle if needed.

This post will explain the simple solution I rely on to make my project backend-agnostic. The code here will be oversimplified. If you'd like to have a look at what I'm building send me an email via the contact link and perhaps I'll send you something when I reach the next milestone.

Having determined the project's needs I realized that my presentation layer should be agnostic of the database access (which is indeed the best way to develop anyway).

Step 0: Defining the data access layer's functionality

Considering what a really simple - probably a  baby like - blog engine does these days I designed an interface that met my simple needs.

using System;
using System.Collections.Generic;

using Blog.Models;

namespace Blog.DataAcessLayer
{
    public interface IBlog
    {
        Post GetPostById( int Id );
        List<Post> GetIndexPosts( );
    }
}

This interface will be the only way the rest of the project (my controllers that is - remember? it's an ASP.NET MVC Project) can communicate with the underlying data source.

Step 1: Creating the data access layer factory

The way the blog engine understands where the underlying data source exists and what type of data source (XML, MySql, Sql Express, etc) we've chosen to use is though the connectionStrings setting in the Web.config file.

<connectionStrings>
	<add name="Db" connectionString="Data Source=localhost;Initial Catalog=blog;Integrated Security=True" providerName="System.Data.SqlClient"/>
</connectionStrings>  

What this connection string entry (Db) really says to the application is that I'm using a connection whose provider is the System.Data.SqlClient namespace with the specified connection string value. Easy right? Indeed.

Next, we'll create factory that can give us (when invoked) the appropriate data access layer implementation.

using System;
using System.Configuration;

namespace Blog.DataAcessLayer
{
    internal static class BlogFactory
    {
        public static IBlog GetNewBlog( )
        {
            string connectionString = ConfigurationManager.ConnectionStrings["Db"].ConnectionString;
            string providerName = ConfigurationManager.ConnectionStrings["Db"].ProviderName;

            if ( providerName == "System.Data.SqlClient" )
                return new Providers.SqlServerProvider( connectionString );
            /*
            else if ( providerName == "MySql" )
                return new Providers.MySqlProvider( connectionString );
            else if ( providerName == "Xml" )
                return new Providers.XmlProvider( connectionString );
            else if ( providerName == "Oracle" )
                return new Providers.OracleProvider( connectionString );
            */
            else
                return ( IBlog )null;
        }
    }
}

Step 2: Implementing a provider

What we need next is to implement a provider so you can get an idea of what's going on. I'm going to give you head start on the SqlServerProvider.

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;

using Blog.Models;
using Blog.DataAcessLayer;

namespace Blog.DataAcessLayer.Providers
{
    internal class SqlServerProvider : IBlog
    {
        #region [ Properties ]

        private string ConnectionString { get; set; }

        #endregion

        #region [ Constructor ]

        public SqlServerProvider( string ConnectionString )
        {
            this.ConnectionString = ConnectionString;
        }

        #endregion

        #region [ Interface Implementation ]

        public Post GetPostById( int PostId )
        {
            Post ret = null;

            using ( SqlConnection cnn = GetConnection( ) )
            {
                SqlCommand cmd = cnn.CreateCommand( );
                cmd.CommandText = "p_GetPostById";
                cmd.CommandType = CommandType.StoredProcedure;

                cmd.Parameters.AddWithValue( "@PostId", PostId );

                using ( SqlDataReader reader = cmd.ExecuteReader( ) )
                {
                    if ( reader.Read( ) )
                    {
                        ret = new Post( );
                        ret.PostId = ( int )reader["PostId"];
                        ret.Title = ( string )reader["Title"];
                        ret.Body = ( string )reader["Body"];
                        ret.Author = ( string )reader["Author"];
                        ret.DateSubmitted = ( DateTime )reader["DateSubmitted"];
                    }

                    reader.Close( );
                }

                cnn.Close( );
            }

            return ret;
        }

        public List<Post> GetIndexPosts( )
        {
            List<Post> ret = new List<Post>( );

            using ( SqlConnection cnn = GetConnection( ) )
            {
                SqlCommand cmd = cnn.CreateCommand( );
                cmd.CommandText = "p_GetIndexPosts";
                cmd.CommandType = CommandType.StoredProcedure;

                using ( SqlDataReader reader = cmd.ExecuteReader( ) )
                {
                   while ( reader.Read( ) )
                    {
                        Post p = new Post( );

                        p.Author = ( string )reader["Author"];
                        p.DateSubmitted = ( DateTime )reader["DateSubmitted"];
                        p.Body = ( string )reader["Body"];
                        p.PostId = ( int )reader["PostId"];
                        p.Title = ( string )reader["Title"];

                        ret.Add( p );
                    }

                    reader.Close( );
                }

                cnn.Close( );
            }

            return ret;
        }

        #endregion

        #region [ Helpers ]

        private SqlConnection GetConnection( )
        {
            SqlConnection cnn = new SqlConnection( this.ConnectionString );
            cnn.Open( );

            return cnn;
        }

        #endregion
    }
} 

Step 3: What does our database look like?

Normally, the database should have been created first but this is not a real world example so I'll just provide a simple script you can use to create your database (this is a Sql Server 2005 script):

SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [Post](
	[PostId] [int] IDENTITY(1,1) NOT NULL,
	[Title] [nvarchar](400) NOT NULL,
	[Body] [ntext] NOT NULL,
	[Author] [nvarchar](40) NOT NULL,
	[DateSubmitted] [datetime] NOT NULL CONSTRAINT [DF_Post_DateSubmitted]  DEFAULT (getdate()),
 CONSTRAINT [PK_Post] PRIMARY KEY CLUSTERED 
(
	[PostId] ASC
)WITH (IGNORE_DUP_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]

GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE PROCEDURE [p_GetPostById]
	@PostId int
AS
BEGIN

	SET NOCOUNT ON;

	SELECT
		*
	FROM Post
	WHERE
	( PostId = @PostId )

END
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE PROCEDURE [p_GetIndexPosts]
AS
BEGIN

	SET NOCOUNT ON;

	SELECT TOP 10
		*
	FROM Post
	ORDER BY DateSubmitted DESC

END

After creating the database, remember to update the connection string to the one that suits your installation.

Lets define our Post object now:

using System;

namespace Blog.Models
{
    public class Post
    {
        public int PostId { get; set; }
        public string Title { get; set; }
        public string Body { get; set; }
        public string Author { get; set; }
        public DateTime DateSubmitted { get; set; }
    }
}

Step 4: Getting the data access to our controllers.

Since the controllers in ASP.NET MVC can inherit from base classes (we are after all using .Net aren't we?) wouldn't it be cool not to have to write the same stuff over an over again till the end of time? Please! someone argue that!

Lets define a base controller for our application:

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Security.Principal;
using System.Web;
using System.Web.Mvc;
using System.Web.Security;
using System.Web.UI;

using Blog.DataAcessLayer;

namespace Blog.Controllers
{
    [HandleError]
    [OutputCache( Location = OutputCacheLocation.None )]
    public abstract class ControllerBase : Controller
    {
        #region [ Properties ]

        private IBlog __blog;

        #endregion

        #region [ Constructor ]

        protected ControllerBase( ) : base( ) { }

        #endregion

        #region [ Properties ]

        protected IBlog Blog
        {
            get
            {
                if ( __blog == null )
                    __blog = BlogFactory.GetNewBlog( );

                return __blog;
            }
        }

        #endregion
    }
}

Now that this is done, lets implement the HomeController (front page controller for our blog):

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Mvc.Ajax;

namespace Blog.Controllers
{
    public class HomeController : ControllerBase
    {
        public ActionResult Index( )
        {
            return View( Blog.GetIndexPosts( ) );
        }

        public ActionResult Entry( int id )
        {
            return View( Blog.GetPostById( id ) );
        }
    }
}

Last and final step is to define our ASP.Net MVC Views, we'll just need 2 for this example (one for the blogs front page and one for the permalink of each post):

Index View (front page):

<%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master" AutoEventWireup="true" CodeBehind="Index.aspx.cs" Inherits="Blog.Views.Home.Index" %>

<%@ Import Namespace="Blog.Models" %>

<asp:Content ID="indexContent" ContentPlaceHolderID="MainContent" runat="server">
    <% foreach( Post p in ViewData.Model) { %>
    
        <h2><%= Html.Encode( p.Title ) %></h2>
    
        <p><%= Html.Encode( p.Body ) %></p>
        <%
            string permalinkUrl = Url.Action( "Entry", "Home", new { id = p.PostId } );
            string author = Html.Encode( p.Author );
            string timeStamp = p.DateSubmitted.ToUniversalTime( ).ToString("dd/MM/yyyy HH:mm");
        %>
        <small>
            <%= author %> | <%= timeStamp %> UTC | <a href="<%= permalinkUrl %>" title="Permalink">Permalink</a>
        </small>
    
    <% } %>
</asp:Content>

Entry View (permalink):

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" AutoEventWireup="true" CodeBehind="Entry.aspx.cs" Inherits="Blog.Views.Home.Entry" %>
<asp:Content ID="Content1" ContentPlaceHolderID="MainContent" runat="server">
    
    <% if( ViewData.Model == null ) { %>
    
        <h1>The post was not found!</h1>
        
    <% } else { %>
        
            <h2><%= Html.Encode( ViewData.Model.Title ) %></h2>
        
            <p><%= Html.Encode( ViewData.Model.Body )%></p>
            <%
                string permalinkUrl = Url.Action( "Entry", "Home", new { id = ViewData.Model.PostId } );
                string author = Html.Encode( ViewData.Model.Author );
                string timeStamp = ViewData.Model.DateSubmitted.ToUniversalTime( ).ToString( "dd/MM/yyyy HH:mm" );
                string indexUri = Url.Action( "Index", "Home" );
            %>
            
            <small>
                <%= author %> | <%= timeStamp %> UTC | <a href="<%= permalinkUrl %>" title="Permalink">Permalink</a>
                <br />
                <a href="<%= indexUri %>" title="Back to index">Back to the blog's index</a>
            </small>
    
    <% } %>
    
</asp:Content>

Step 5: Putting it all together

For your convenience I've uploaded the full source code (the link is provided below) - written in Visual Studio 2008. You should need ASP.NET MVC (BETA) installed for it to launch.

I hope I gave you something to think about, your comments are always welcome.

DALInterfaces.zip (278.67 kb)


Something useful (hopefully)

micro-blogging

@twitter: azazeal

Search

Disclaimer

The opinions expressed herein are my own personal opinions and do not represent my my associates' or empoyers' view in anyway.