There’s a lot of content for latest CTP, CTP4, for Entity Framework “new features”. It’s mainly focused on Code First stuff, that fills the triad with Database First and Model First. I like Code First but what I also like is maintainable code. Hence I was more trying not to use convention-over-configuration, in opinion good for only small projects, and focus on specifying everything the way I want it to be, especially with my database structure (yes, I’m data consistency and storage freak).
Let’s model some kind of simple library and try to use there couple of Entity Framework’s features. And to make things worse, try to do it with ADO.NET provider for Firebird.
Good news is I succeeded. My database structure was my first and non-touchable object (to be honest the first was the idea what to create and how to represent it in database) together with the idea of objects.
Here’s the script for Firebird database (Did you noticed we’re fully Entity Framework 4 compatible?). See the quoted column names? Yes, not nice, but later in mapping in C# it looks nicer.
And I mixed the mapping with one for MS SQL Server (see below).
RECREATE TABLE Authors (
"id" INT NOT NULL,
"FirstName" VARCHAR(255) NOT NULL,
"LastName" VARCHAR(255) NOT NULL,
CONSTRAINT PK_Authors PRIMARY KEY ("id")
);
RECREATE TABLE Books (
"id" INT NOT NULL,
"Discriminator" char(2) NOT NULL,
"Title" VARCHAR(1000) NOT NULL,
"Published" TIMESTAMP NOT NULL,
"ID_Author" INT NOT NULL,
"IssuesPerYear" SMALLINT,
"Price" DECIMAL(9,0),
CONSTRAINT PK_Books PRIMARY KEY ("id")
);
RECREATE TABLE Languages (
"id" INT NOT NULL,
"LanguageName" VARCHAR(100) NOT NULL,
"LanguageAbbrevation" CHAR(3),
CONSTRAINT PK_Languages PRIMARY KEY ("id")
);
RECREATE TABLE Translators (
"id" INT NOT NULL,
CONSTRAINT PK_Translators PRIMARY KEY ("id")
);
RECREATE TABLE Translators_Languages (
"ID_Language" INT NOT NULL,
"ID_Translator" INT NOT NULL,
CONSTRAINT PK_Translators_Languages PRIMARY KEY ("ID_Language", "ID_Translator")
);
ALTER TABLE Books ADD CONSTRAINT FK_Book_Author FOREIGN KEY ("ID_Author")
REFERENCES Authors("id")
ON DELETE CASCADE
;
ALTER TABLE Translators_Languages ADD CONSTRAINT FK_TL_Languages FOREIGN KEY ("ID_Language")
REFERENCES Languages("id")
ON DELETE NO ACTION
;
ALTER TABLE Translators_Languages ADD CONSTRAINT FK_TL_Translators FOREIGN KEY ("ID_Translator")
REFERENCES Translators("id")
ON DELETE NO ACTION
;
ALTER TABLE Translators ADD CONSTRAINT FK_TypeConstraint FOREIGN KEY ("id")
REFERENCES Authors("id")
ON DELETE NO ACTION
;
So it’s time to create mapping right? Nope. Now I’ll switch my brain from ER thinking into OO thinking mode. Here’s the world of entities (sure I made some adjustments to fit some Entity Framework features, like Complex Types):
public abstract class Book
{
public int ID { get; protected set; }
public string Title { get; set; }
public DateTime Published { get; set; }
public Author Author { get; set; }
public int AuthorID { get; set; }
}
public class RealBook : Book
{
public decimal Price { get; set; }
}
public class Magazine : Book
{
public short IssuesPerYear { get; set; }
}
public class Author
{
public int ID { get; protected set; }
public Name FullName { get; set; }
public ICollection<Book> Books { get; set; }
public Author()
{
this.Books = new List<Book>();
}
}
public class Translator : Author
{
public ICollection<Language> Languages { get; set; }
public Translator()
{
this.Languages = new List<Language>();
}
}
public class Language
{
public int ID { get; protected set; }
public string LanguageName { get; set; }
public string LanguageAbbrevation { get; set; }
}
#region Complex Types
public class Name
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
#endregion
As you see I’m using pure POCOs.
So far we haven’t touched any Entity Framework related stuff. So it’s time to create our context and all DAL related stuff. Mine is very simple and exposes only few properties and methods, just to keep it simple and focus on the aim.
public class LibraryContext : DbContext
{
public LibraryContext(DbConnection connection)
: base(connection)
{
this.ObjectContext.ContextOptions.LazyLoadingEnabled = false;
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Configurations.Add(new BookConfiguration());
modelBuilder.Configurations.Add(new AuthorConfiguration());
modelBuilder.Configurations.Add(new TranslatorConfiguration());
modelBuilder.Configurations.Add(new LanguageConfiguration());
modelBuilder.Configurations.Add(new NameConfiguration());
}
public string CreateDatabaseScript()
{
return this.ObjectContext.CreateDatabaseScript();
}
public IDbSet<Book> Books
{
get { return this.Set<Book>(); }
}
public IDbSet<Author> Authors
{
get { return this.Set<Author>(); }
}
}
public class DoNothingWithMyDatabase<TContext> : IDatabaseInitializer<TContext>
where TContext : DbContext
{
public void InitializeDatabase(TContext context)
{ }
}
Pretty simple, isn’t it. I’m using new stripped down objects DbContext (← ObjectContext) and IDbSet/DbSet (← IObjectSet/ObjectSet).
You may also notice, I’ve created object which implements IDatabaseInitializer. That’s because, by default, Entity Framework will try to create database for you and check whether your database matches model (CreateDatabaseOnlyIfNotExists). I‘m controlling my database. You have to add somewhere into your application before doing anything with the context call to SetInitializer:
Database.SetInitializer(new DoNothingWithMyDatabase<LibraryContext>());
And finally we’re ready to dive into the mapping. Again, you can type the mapping directly into overridden OnModelCreating, useful if you have only couple of diversions from default conventions. I created configuration classes where I specified as much as I want. And I need (almost) everything to be nailed down (remember, data consistency and storage freak
) as I don’t want to be surprised when something in database or in entities changes and application will start behave weird.
class BookConfiguration : EntityConfiguration<Book>
{
public BookConfiguration()
{
this.HasKey(x => x.ID);
this.Property(x => x.ID).IsIdentity();
this.Property(x => x.Title).IsRequired().IsVariableLength().HasMaxLength(1000).IsUnicode();
this.Property(x => x.Published);
this.HasRequired(x => x.Author).WithMany(a => a.Books).WillCascadeOnDelete().HasConstraint((b, a) => b.AuthorID == a.ID);
this.MapHierarchy()
.Case<Book>(x => new
{
id = x.ID,
Title = x.Title,
Published = x.Published,
ID_Author = x.AuthorID,
})
.Case<RealBook>(x => new
{
Discriminator = "B",
Price = x.Price,
})
.Case<Magazine>(x => new
{
Discriminator = "M",
IssuesPerYear = x.IssuesPerYear,
})
#if FB
.ToTable("BOOKS");
#else
.ToTable("Books");
#endif
}
}
class AuthorConfiguration : EntityConfiguration<Author>
{
public AuthorConfiguration()
{
this.HasKey(x => x.ID);
this.Property(x => x.ID).IsIdentity();
this.HasMany(x => x.Books);
this.MapHierarchy(x => new
{
id = x.ID,
FirstName = x.FullName.FirstName,
LastName = x.FullName.LastName,
})
#if FB
.ToTable("AUTHORS");
#else
.ToTable("Authors");
#endif
this.MapHierarchy().Case<Translator>(x => new
{
id = x.ID,
})
#if FB
.ToTable("TRANSLATORS");
#else
.ToTable("Translators");
#endif
}
}
class TranslatorConfiguration : EntityConfiguration<Translator>
{
public TranslatorConfiguration()
{
this.HasMany(x => x.Languages).WithMany()
#if FB
.Map("TRANSLATORS_LANGUAGES",
#else
.Map("Translators_Languages",
#endif
(t, l) => new
{
ID_Translator = t.ID,
ID_Language = l.ID,
});
}
}
class LanguageConfiguration : EntityConfiguration<Language>
{
public LanguageConfiguration()
{
this.HasKey(x => x.ID);
this.Property(x => x.ID).IsIdentity();
this.Property(x => x.LanguageName).IsRequired().IsVariableLength().HasMaxLength(100).IsUnicode();
this.Property(x => x.LanguageAbbrevation).IsFixedLength().HasMaxLength(3).IsUnicode();
this.MapSingleType(x => new
{
id = x.ID,
LanguageName = x.LanguageName,
LanguageAbbrevation = x.LanguageAbbrevation
})
#if FB
.ToTable("LANGUAGES");
#else
.ToTable("Languages");
#endif
}
}
class NameConfiguration : ComplexTypeConfiguration<Name>
{
public NameConfiguration()
{
this.Property(x => x.FirstName).IsRequired().IsVariableLength().HasMaxLength(255).IsUnicode();
this.Property(x => x.LastName).IsRequired().IsVariableLength().HasMaxLength(255).IsUnicode();
}
}
I don’t know whether it’s worth to describe the lines. Should be understandable if you know how the entities and database look like. Just maybe small notice. Besides Complex Types we used two most common inheritance mapping scenarions – TPH aka Table Per Hierarchy for Books and TPT aka Table Per Type for Authors (there’s also TPC (Table Per Concrete Type)). However if you have questions feel free to use comments, if I’ll know answer I’ll be happy to reply.
And finally some really simple application to test the result:
If you define #define FB it’ll use Firebird database else MS SQL Server.
Database.SetInitializer(new DoNothingWithMyDatabase<LibraryContext>());
Action<LibraryContext> doSomething = (context) =>
{
Console.WriteLine(context.CreateDatabaseScript());
var query = context.Books
.Select(b => new
{
BookName = b.Title,
AuthorName = b.Author.FullName.LastName + ", " + b.Author.FullName.FirstName
})
.OrderBy(x => x.BookName);
Console.WriteLine((query as ObjectQuery).ToTraceString());
var data = query.ToArray();
foreach (var item in data)
{
Console.WriteLine("Book {0} written by {1}.", item.BookName, item.AuthorName);
}
};
#if FB
using (LibraryContext context = new LibraryContext(new FbConnection(@"database=localhost:ctp4;username=sysdba;password=masterkey;pooling=true;")))
{
doSomething(context);
}
#else
using (LibraryContext context = new LibraryContext(new SqlConnection(@"Data Source=.\sqlexpress;Initial Catalog=ctp4;Integrated Security=True;Pooling=False;MultipleActiveResultSets=True")))
{
doSomething(context);
}
#endif
I’m glad I was able to figure out how to map everything without touching database structure (although it’s pretty straightforward) or letting the default rules to kick in. Hope it will help you if you struggle with something (and hope the naming will not change much in future). It’s also nice to see the whole infrastructure fits together and changing provider, in my case for Firebird’s, doesn’t make the code to blow out.
Pingback: Tweets that mention Ultimate EFv4 CTP4 Code First (full mapping) example (using Firebird) « Jiří {x2} Činčura -- Topsy.com
Pingback: Firebird News » Ultimate EFv4 CTP4 Code First full mapping example using Firebird
Great work! This is amazing
When I look at things that ommit DataAnnotations I always think about how much you lose in terms of Validation on the client side. What do you think about that?
Yes, you’ll loose something. It’s always about balancing. On the other hand you have clean objects not polluted with some specific attributes. I like to specify mapping and constraints in configuration, where, when DB changes, I can easily find all the stuff. But it’s my personal preference.
And often the application has more layers so the annotations may not be available to UI or the UI needs more validation than that.
You don’t need the DoNothingWithMyDatabase initializer. Instead you can just do:
Database.SetInitializer(null);
@michael Interesting. That’s definitely easier than creating dummy implementation. But at least I and readers see how the IDatabaseInitializer works.
I’ve tried this example with my VS2010 and Firebird 2.5, but I get exception System.Data.ProviderIncompatibleException at this.ObjectContextOptions.LazyLoadingEnabled = false; with Message “This provider doesn’t support DatabaseExists”. Directly before comes System.PlatformNotSupportedException with Message “given cryptography algorithm not supported on this platform”. What have I forgotten?
I’ve also found that .NET provider 2.5.2 doesn’t work with Interbase (Index exceeds the maximum). And there also bug (or in both Firebird and Interbase?) – the query “select * from where @p0 = @p1″ gives error -804 Invalid data type. The same query with “@p0 = ‘X’” is OK.
@Gabriel
Sure. Firebird has no idea whether database exists, until you actually use it (but if this fails, it may be as well wrong username etc.). There’s no master database.
FirebirdClient, as name suggests, is about Firebird. With InterBase you should use provider for InterBase.
Hi, thanks for the overview! I have a question concerning complex mapping; like you showed in your example with ‘Name’. In my complex property I have a foreign key to another entity; but it seems I cannot succeed in mapping this. Example: Customer has property Address (which is complex type), Address has property City (which is another entity). Any idea on how to map this?
I don’t know it from top of my head. But I’ll try it soon and blog about it. Stay tuned.
@Ludwig> I did some investigation and it’s not supported right now (neither Code Only nor in EDMX file). The complex type cannot contain navigation property.
Great job.
Best example yet on peeling back the covers underneath DbContext.
Curious how you learned all of the various methods that can be applied to objects for the EF metadata creation behind the scenes? Is there a documentation resource you went to in order to learn these? (They don’t seem intuitive).
I also noticed you have a SqlConnection object you pass into DbContext upon instance creation, instead of allowing DbContext to create it by itself. I think this approach lends itself to replacing the SqlConnection with a new class that can handle SQL Azure connection retry logic very easily as well, since EF doesn’t handle this out of the box, and deploying any Code-First type approach on SQL Azure today will cause problems without that logic in place.
Have you experimented yet with using Code-First on SQL Azure and investigated what changes you’d have to introduce to make your “connection” object more robust, while keeping your code elegant?
There are a few examples on the web (not very elegant), including an implementation of a SqlAzureConnection, but I haven’t seen an implementation yet that uses it with Code-First.
There’s no documentation, I’m aware of, for the Code First right now. I think it’ll be finished when RTM (or betas) will be ready. Now it’s still under changes based on feedback. My approach was simple trial-error.
I’m now playing with Entity Framework and Azure. Stay tuned, some posts are almost finished.