First of all thanks again to Microsoft for both taking the EF further towards something really powerful and usable and then special thanks to Alex James for helping the community out over on stack overflow. I started of small scale. Let’s say I want to create a blog. I have users, blogs, entries and comments. Think that sums up the basic functionality of a blog. That leaves me with the following conceptual model:
From this I want to remove the auto generated classes. I recommend reading this blog post for more information on how to remove this and for a really good tutorial in general. Now all that is left is to right click the model (not a class) and click on generate database script. This does a good job I end up with a database script that I can just run from within visual studio and the database is created for me. Just bear in mind that generating this script and running it will totally delete everything within your database. Only do this on your development machine, not on a production db. To synchronize the changes from your development machine go ahead and use Red Gate’s SQL Toolbelt.
I then move ahead to create my POCO classes and end up with the following:
public partial class User
{
public User()
{
Blog = new Blog();
Active = true;
}
public virtual int Id { get; set; }
public virtual bool Active { get; set; }
public virtual string Alias { get; set; }
public virtual string Email { get; set; }
public virtual string Password { get; set; }
public virtual string Hash { get; set; }
public virtual Blog Blog { get; set; }
}
public class Blog
{
public int Id { get; set; }
public string Title { get; set; }
public bool AllowComments { get; set; }
public User User { get; set; }
public IList<Entry> Entries { get; set; }
}
public abstract class Post
{
public virtual int Id { get; set; }
public virtual string Header { get; set; }
public virtual string Text { get; set; }
public virtual DateTime CreatedAt { get; set; }
public virtual int UserId { get; set; }
}
public class Entry : Post
{
public Blog Blog { get; set; }
public IList<Comment> Comments { get; set; }
}
public class Comment : Post
{
public Entry Entry { get; set; }
}
I instantly run in to an extremely silly problem where I get the following exception:
System.Data.MetadataException: Schema specified is not valid. Errors: The mapping of CLR type to EDM type is ambiguous because multiple CLR types match the EDM type 'Entry'. Previously found CLR type 'Entry', newly found CLR type 'System.Collections.Generic.Dictionary2+Entry'. The mapping of CLR type to EDM type is ambiguous because multiple CLR types match the EDM type 'Entry'. Previously found CLR type 'Entry', newly found CLR type 'System.Runtime.CompilerServices.ConditionalWeakTable2+Entry'.
This is according to Alex (Program Manager on the Entity Framework team) over on Stack Overflow due to me using Entry as a class name. There was a few other “Entry” within the framework found and I get an exception. It should be fixed in Beta2 of EF4 but until then I go ahead and change my class name to BlogEntry instead and voila no more exception. Cheers Alex.
I realize this isn’t an ultimate model of a blog. First of all only a certain user (in this simple blog) should be allowed to post to his/her own blog so there is no need for every blog entry to have the user id and for the comments there is no need for a header. Simple text would do. I refactor the conceptual model and the classes to the following:
public abstract class Post
{
public virtual int Id { get; set; }
public virtual string Text { get; set; }
public virtual DateTime CreatedAt { get; set; }
}
public class BlogEntry : Post
{
public virtual string Header { get; set; }
public virtual Blog Blog { get; set; }
public virtual IList<Comment> Comments { get; set; }
}
public class Comment : Post
{
public virtual string Header { get; set; }
public virtual int UserId { get; set; }
public virtual BlogEntry BlogEntry { get; set; }
}
I add virtual because I want to support automatic lazy (deferred) loading. This should improve performance and usability of the tiny blog domain quite a bit. Now to make this work I need to create my own ObjectContext. It’s not such a big thing to do and I simplified it a lot with just a couple of automatic properties.
public class EntityContext : System.Data.Objects.ObjectContext
{
public ObjectSet<Blog> Blogs { get; set; }
public ObjectSet<Post> Posts { get; set; }
public ObjectSet<BlogEntry> Entries { get; set; }
public ObjectSet<Comment> Comments { get; set; }
public EntityContext() : base("name=Entities", "Entities")
{
this.Blogs = CreateObjectSet<Blog>();
this.Posts = CreateObjectSet<Post>();
this.Entries = CreateObjectSet<BlogEntry>();
this.Comments = CreateObjectSet<Comment>();
}
}
The constraint : base(“name=Entities”, “Entities”) is just telling what connection string we should use. And the rest is pretty straight forward. However I’d like to note that the ObjectSet is an enhanced/empowered version of the ObjectQuery if I am not mistaken. Now it’s time to start playing around with this. I need to create a unit test and in my first try I got a couple of things wrong. I try to use it in mixed mode and it doesn’t work out as I expected.
/// <summary>
/// A test for adding users
/// </summary>
[TestMethod()]
public void CanAddUser()
{
EntityContext context = new EntityContext();
var user = new User();
user.Alias = "CatZ";
user.Active = true;
user.Email = "first.last@domain.com";
user.Hash = "1234";
user.Password = "1234";
user.Blog.Title = "Mad Cat";
user.Blog.EnableComments = true;
context.Users.AddObject(user);
context.SaveChanges();
Assert.IsTrue(user.Id > 0);
}
It gives me the following exception:
Test method threw exception: System.ArgumentException: There are no EntitySets defined for the specified entity type 'BlogEntry'. If 'BlogEntry' is a derived type, use the base type instead. For example, you would see this error if you called CreateObjectSet() and DiscontinuedProduct is a known entity type but is not directly mapped to an EntitySet. The DiscontinuedProduct type may be a derived type where the parent type is mapped to the EntitySet or the DiscontinuedProduct type might not be mapped to an EntitySet at all. Parameter name: TEntity
Huh? LOL! WTF? Ok back to basics or back to base class actually (at least that’s what the message says). I tweak the EntityContext a bit and end up with something like:
public class EntityContext : System.Data.Objects.ObjectContext
{
public ObjectSet<User> Users { get; set; }
public ObjectSet<Blog> Blogs { get; set; }
public ObjectSet<Post> Posts { get; set; }
public EntityContext() : base("name=Entities", "Entities")
{
this.Users = CreateObjectSet<User>();
this.Blogs = CreateObjectSet<Blog>();
this.Posts = CreateObjectSet<Post>();
}
}
Woohoo end of problems or so I thought at least. The test runs and fails with the following message:
Failed CanAddUser UnitTests Test method threw exception: System.InvalidOperationException: Invalid relationship fixup detected in the navigation property 'User' of the entity of the type 'Blog'.
This is insane, I was ready to give up. I mean what could be wrong now? The only thing I could think of was that the navigation property of the blog was not set to the current user. Quick modification of the test:
/// <summary>
/// A test for adding users
/// </summary>
[TestMethod()]
public void CanAddUser()
{
EntityContext context = new EntityContext();
var user = new User();
user.Alias = "CatZ";
user.Active = true;
user.Email = "first.last@domain.com";
user.Hash = "1234";
user.Password = "1234";
user.Blog.Title = "Mad Cat";
user.Blog.EnableComments = true;
user.Blog.User = user;
context.Users.AddObject(user);
context.SaveChanges();
Assert.IsTrue(user.Id > 0);
}
The test runs and IT IS A SUCCESS!!! WOOHOO!! Now I am soooo happy I run it again and bummer I end up with two identical users and blogs in the database with two different id’s. Ok so I need some validation but I don’t care right now. I am just so happy with how easy it finally is to use my own classes with the entity framework. Ultimately I would not add a user to the database like this of course though I would hardly let the user add itself but it’s a start of course. I’ll write more in the future about this since I’ve waited 1,5 years for this I am quite eager to learn how it’s working!