This post is over 6 months old. Some details, especially technical, may have changed.

Entity Framework: Code First - Head First

I find the data access layer on most projects to be either overly complex or fiddly with lots of XML mapping files that are difficult to debug so anything that could make this layer more developer friendly I'm all for it.  Though I also want to point out that I understand why the DAL is often complex - there is a lot to consider and so I want to also understand if these "friendlier" technologies can handle that sort of complexity or if they simply make the happy path easier but making the more complex scenarios more difficult or even impossible (which is obviously a blocker).

So I've been tinkering with this new Entity Framework CTP5 release and the "Code First" features recently.  This comes after some time-out from EF due to some really bad experiences with EF1. I was promised that there has been significant changes/improvements since I last dabbled and it really seems there have been.  So I wanted to put it to the test and as one of my co-workers wanted an "Ideas" app I thought it would be a fun [may not be anyone's definition of fun but my own] to throw an MVC app together using EF "Code First" to model my domain entities.  To make it all even more simple I went ahead and used SQL Server CE 4 for persistence.  So what did the solution need to do?  The basic requirements were,

  1. Use Windows Authentication for users
  2. Allow users to submit an idea (Title, Description)
  3. Allow users to tag an idea with a variable number of tags
  4. Allow users to vote up or vote down ideas (but not their own)
  5. Allow users to comment on ideas
  6. Allow users to filter ideas by tags
  7. Allow users to sort ideas by newest ideas or by most popular.

Nothing too extreme involved - not unless you turn the whole thing into a computer game style EXTREME speed run competition - man vs. machine - the ULTIMATE [typing] battle.... with bathroom and snack breaks!  Just to make it even more INSANE I documented my steps and created a graphical timeline of the session in a PRETTY timeline. 

Ammmm don't mean to be rude but your jaw.... we'll it's on the floor.  Can you pick it up please?  17:21 to 20:38 minus about an hour and a bit for bathroom, snack and chat breaks - zero to datafied in less than 3 hours!  Few points to note,

  • This experiment focused on the data model, EF CTP5 and the database.
  • There is a working UI (MVC3) it's just not exactly pretty
  • I had no EF "Code First" experience before hand
  • I could be doing a few things incorrectly
  • It'll probably take me longer to write this post than it did the app.

So lets look at what I produced.  The source is available on Github* for your fiddling pleasure.

I am not going to dive into the whole MVC part of it as the source is available but I may touch on some of the interface points such as controllers and binders.

The Domain Models

Lets take a high level look at our domain models.

DomainEntity

The abstract domain entity is used to prevent me having to repeat common auditing and database related stuff across all my entities.  It is not mandatory or derived from anything related to Entity Framework - all these classes are simple POCO's.  DomainEntity sets up the entities primary key using the Key attribute and also gold 2 audit related properties CreatedBy and CreatedDate.

/// <summary>
/// Base class for domain entities responsible for holding auditing and 
/// persistence related properties
/// </summary>
public abstract class DomainEntity
{
    [Key]
    public int Id { get; set; }

    [Required]
    public string CreatedBy { get; set; }

    [Required]
    public DateTime CreatedDate { get; set; }
}

Idea

Idea is our principle class in our domain.  As you can see there are various associations set up between the other classes.  2 1-* mappings between Comment and Vote and a *-* mapping between itself and tag (a tag can exist for any number of ideas and an idea can have many tags).  It also holds a number of methods related to business logic - specifically calculating Votes, number of Comments etc.

/// <summary>
/// Main domain object in the idea solution.  Represents an idea
/// </summary>
public class Idea : DomainEntity
{
    [Required]
    [MaxLength(255)]
    public string Title { get; set; }

    [Required]
    public string Description { get; set; }

    [DefaultValue(false)]
    public bool IsRejected { get; set; }

    #region Associations
    public virtual ICollection<Comment> Comments { get; set; }
    public virtual ICollection<Tag> Tags { get; set; }
    public virtual ICollection<Vote> Votes { get; set; }
    #endregion

    #region Business Logic
    public virtual int Score
    {
        get { return Votes == null ? 0 : Votes.Sum(v => v.Value); }
    }

    public virtual int VoteCount
    {
        get { return Votes == null ? 0 : Votes.Count; }
    }

    public virtual int CommentCount
    {
        get { return Comments == null ? 0 : Comments.Count; }
    }
    #endregion
}

Tag

Tag is pretty simple.  The only interesting thing about it is the use of NormalisedName - essentially the name field lowercased and whitespace removed.  This is used when attempting to fetch potentially existing tags from the database.

/// <summary>
/// Represents a tag in the idea solutuion
/// </summary>
public class Tag : DomainEntity
{
    private string _name;
        
    [Required]
    public string Name 
    {
        get
        {
            return _name;
        }
        set
        {
            _name = value;
            NormalisedName = value.ToLower().Replace(" ", string.Empty);
        }
    }

    [Required]
    public string NormalisedName { get; set; }

    #region Associations
    public virtual ICollection<Idea> Ideas { get; set; }
    #endregion
}

Vote

Rather than just store a calculated value against an idea the Vote object represents a rich representation of a Vote (either up or down, whom by and when).  This allows us to provide extra validation when we need it.  For example people not allowed to vote on their own idea or vote on an idea in any particular direction more than once.  Having this rich association makes these things much easier and we aren't forced to create custom objects to track this sort of thing.

/// <summary>
/// Represents a single vote for an idea
/// </summary>
public class Vote : DomainEntity
{
    [Required]
    [Range(-1,1)]
    public int Value { get; set; }

    #region Associations
    public virtual Idea Idea { get; set; }
    #endregion
}

Comment

Nothing special here. 

/// <summary>
/// Represents an ideas comment
/// </summary>
public class Comment : DomainEntity
{
    /// <summary>
    /// Gets or sets the comments content
    /// </summary>
    [Required]
    public string Text { get; set; }

    #region Associations
    /// <summary>
    /// Gets or sets the link to the parent idea
    /// </summary>
    public virtual Idea Idea { get; set; }
    #endregion
}

The Database Context

This is where all the EF magic happens.  We use this class to provide an entry point into our database.  It's possible to configure entities here in terms of mapping and associations as well as providing a means to seed the database with initial data but I didn't need any of that.  No I just defined my sets and added a method for filtering/sorting ideas based on criteria.  Simple stuff yet again.  It just extends the DbContext class from Entity Framework.

/// <summary>
/// Data Repository for the ideas solution
/// </summary>
public class IdeaRepository : DbContext
{
    #region Db Sets
    public DbSet<Comment> Comments { get; set; }
    public DbSet<Idea> Ideas { get; set; }
    public DbSet<Tag> Tags { get; set;}
    public DbSet<Vote> Votes { get; set; }
    #endregion

    /// <summary>
    /// Main entry point for querying the ideas dat
    /// </summary>
    /// <param name="filters"></param>
    /// <returns></returns>
    public IList<Idea> QueryIdeas(IdeaFilter filters = null)
    {
        IQueryable<Idea> ideas = Ideas;
        IdeaFilter.OrderBy orderBy = IdeaFilter.OrderBy.MostVotes;
        if (filters != null)
        {
            if (!string.IsNullOrWhiteSpace(filters.Tag))
            {
                ideas = ideas.Where(i => i.Tags.Any(t => t.NormalisedName == filters.Tag));
            }

            orderBy = filters.Order.GetValueOrDefault(IdeaFilter.OrderBy.MostVotes);
        }

        switch (orderBy)
        {
            case IdeaFilter.OrderBy.MostVotes:
                ideas = ideas.OrderByDescending(i => i.Votes.Sum(v => v.Value));
                break;
            case IdeaFilter.OrderBy.Newest:
                ideas = ideas.OrderByDescending(i => i.CreatedDate);
                break;
        }

        return ideas.ToList();            
    }
}

Tag Model Binder

This was an interesting thing I discovered.  If you are using a *-* relationship and are associating one side with an object that already exists you are required to fetch this object before using it.  For example when adding a tag to an idea I need to attempt fetch that tag first of it exists.  What I can't do is create a new tag object and assign an existing Id to it - this will be thrown away and saved as a new instance.  To fix this problem I feel back onto a Tag Model binder that attempts to fetch or create tags depending on their normalised name.  It won't save new tag - simply create them (this is why I use a shared DbContext between the controller and the binder).  The binder takes a CSV styled string, breaks it apart, "normalises" the string and tries to fetch tags based on their normalised name.  If it finds one it pushes it into the collection otherwise it creates a new tag object and pushes that in instead.  Probably a better way to do that and I am open to suggestions.  But what I don't want is saving tags that are then going to become orphaned if the other save didn't go through for some reason.

/// <summary>
/// Converts a string of tags (comma seperated) into a list of tags - 
/// creating new ones where necessary and fecthing exisitng ones
/// </summary>
public class TagCollectionModelBinder : DefaultModelBinder
{
    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        List<Model.Tag> tags = new List<Model.Tag>();            
        HttpContextBase ctx = controllerContext.HttpContext;
        string user = ctx.User.Identity.Name;
        string value = ctx.Request.Form[bindingContext.ModelName];
        Model.IdeaRepository _db = Utils.BaseIdeaController.DataContext;

        if (!string.IsNullOrWhiteSpace(value))
        {
            string[] clientTags = value.Split(',');
            foreach (string clientTag in clientTags)
            {
                string normalised = clientTag.ToLower().Replace(" ", string.Empty);
                Model.Tag tag = _db.Tags.FirstOrDefault(t => t.NormalisedName == normalised);

                if (tag == default(Model.Tag))
                {
                    tag = new Model.Tag()
                    {
                        CreatedBy = user,
                        CreatedDate = DateTime.Now,
                        Name = clientTag.Trim()
                    };
                }

                tags.Add(tag);
            }

            return tags;
        }

        return null;
    }

}

Other Things

Not very much else worth mentioning right now as I more or less used the MVC scaffolding to build the views (with some minor tweaking).  Validation on the UI is pushed from the domain object making things a lot more streamlined.  The controllers are still very light and could be made lighter by pushing stuff into the IdeaRepository as well but that's for another day.

So there you go.  A very quick and dirty intro into the world of Entity Framework.  There isn't anything complex going on here and I was worried that EF would mask a lot of stuff that we would need access to but it seems there is plenty of configuration points to hook into.  It has come on leaps and bounds since I last dipped my toes into EF and hopefully they keep up the same momentum.  There is still a lot of due diligence required before I'd recommend EF over any other data access layer that we are currently using but I am certainly keen to dig deeper and push it to it's limits.

Once again the source for the solution is available on Github*.  Phew..... 

* Expect bugs.

Published in .NET on February 11, 2011