This recipe allows us to map a set of objects to another set of objects without constraining either side.
We will be using NuGet Package Manager to install the Entity Framework 4.1 assemblies.
The package installer can be found at http://nuget.org/.
We will also be using a database for connecting to the data and updating it.
Open the Improving Many-To-Many References solution in the included source code examples.
Let us get connected to the database using the following steps:
MappingTest
to the Test
project. We make a test that connects to the database and retrieves an object. This will test the configuration and ensure that the model matches the database schema, using the following code:using System; using System.Collections.Generic; using System.Linq; using System.Text; using BusinessLogic; using DataAccess; using DataAccess.Database; using Microsoft.VisualStudio.TestTools.UnitTesting; using Test.Properties; using System.Data.Entity; namespace Test { [TestClass] public class MappingTest { [TestMethod] public void ShouldReturnABlogWithAuthors() { //Arrange var init = new Initializer(); var context = new BlogContext(Settings.Default.BlogConnection); init.InitializeDatabase(context); //Act var post = context.Posts.Include(x => x.Authors).FirstOrDefault(); //Assert Assert.IsNotNull(post); Assert.IsTrue(post.Authors.Count == 1); } } }
Initializer
to the DataAccess
project in the Database
folder with the following code:using System; using System.Collections.Generic; using System.Data.Entity; using BusinessLogic; namespace DataAccess.Database { public class Initializer : DropCreateDatabaseAlways<BlogContext> { public Initializer() { } protected override void Seed(BlogContext context) { AuthorDetail authorDetail = new AuthorDetail() { Bio = "Test", Email = "Email", Name = "Testing" }; Post item = new Post() { Content = "Test", PostedDate = DateTime.Now, Title = "Test", Authors = new List<AuthorDetail>{authorDetail} }; context.Set<Blog>().Add(new Blog() { Posts = new List<Post>() { item }, AuthorDetail = authorDetail, Creationdate = DateTime.Now, ShortDescription = "Testing", Title = "Test Blog" }); context.SaveChanges(); } } }
Post
to the BusinessLogic
project with the following code:using System; using System.Collections.Generic; namespace BusinessLogic { public class Post { public int Id { get; set; } public string Title { get; set; } public string Content { get; set; } public DateTime PostedDate { get; set; } public ICollection<AuthorDetail> Authors { get; set; } } }
AuthorDetail
to the BusinessLogic
project with the following code:using System.Collections.Generic; namespace BusinessLogic { public class AuthorDetail { public int Id { get; set; } public string Name { get; set; } public string Email { get; set; } public string Bio { get; set; } public ICollection<Post> Posts { get; set; } } }
Blog
to the BusinessLogic
project with the following code:using System; using System.Collections.Generic; using DataAccess; namespace BusinessLogic { public class Blog { public int Id { get; set; } public DateTime Creationdate { get; set; } public string ShortDescription { get; set; } public string Title { get; set; } public AuthorDetail AuthorDetail { get; set; } public ICollection<Post> Posts { get; set; } } }
Mapping
folder to the DataAccess
project and then add a BlogMapping
class to the folder with the following code:using System.ComponentModel.DataAnnotations; using System.Data.Entity.ModelConfiguration; using BusinessLogic; namespace DataAccess.Mappings { public class BlogMapping : EntityTypeConfiguration<Blog> { public BlogMapping() { this.ToTable("Blogs"); this.HasKey(x => x.Id); this.Property(x => x.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity); this.Property(x => x.Title).IsRequired().HasMaxLength(250); this.Property(x => x.Creationdate).HasColumnName("CreationDate").IsRequired(); this.Property(x => x.ShortDescription).HasColumnType("Text").IsMaxLength().IsOptional().HasColumnName("Description"); this.HasRequired(x => x.AuthorDetail); this.HasMany(x => x.Posts).WithRequired(x => x.Blog).WillCascadeOnDelete(false); } } }
Mapping
folder named PostMapping
with the following code:using System.ComponentModel.DataAnnotations; using System.Data.Entity.ModelConfiguration; using BusinessLogic; namespace DataAccess.Mappings { public class PostMapping : EntityTypeConfiguration<Post> { public PostMapping() { this.ToTable("Posts"); this.HasKey(x => x.Id); this.Property(x => x.Id).HasColumnName("PostId").HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity); this.Property(x => x.Content).HasColumnName("Body").IsMaxLength(); this.Property(x => x.PostedDate).HasColumnName("PostedDate"); this.Property(x => x.Title).HasColumnName("Title").IsMaxLength(); this.HasMany(x => x.Authors).WithMany(x => x.Posts); } } }
AuthorDetailMapping
to the Mapping
folder with the following code:using System.ComponentModel.DataAnnotations; using System.Data.Entity.ModelConfiguration; using BusinessLogic; namespace DataAccess.Mappings { public class AuthorDetailMapping : EntityTypeConfiguration<AuthorDetail> { public AuthorDetailMapping() { this.ToTable("AuthorDetails"); this.HasKey(x => x.Id); this.Property(x => x.Id).HasColumnName("AuthorDetailId").HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity); this.Property(x => x.Bio).HasColumnType("Text").IsMaxLength(); this.Property(x => x.Email).HasMaxLength(100).IsRequired(); this.Property(x => x.Name).HasMaxLength(100).IsRequired(); } } }
BlogContext
class to include the new mappings and DbSet<T>
for each type, with the following code:using System; using System.Data.Entity; using System.Linq; using BusinessLogic; using DataAccess.Mappings; namespace DataAccess { public class BlogContext : DbContext, IUnitOfWork { public BlogContext(string connectionString) : base(connectionString) { if (Database.Exists() && !Database.CompatibleWithModel(false)) Database.Delete(); if (!Database.Exists()) Database.Create(); } protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Configurations.Add(new BlogMapping()); modelBuilder.Configurations.Add(new AuthorDetailMapping()); modelBuilder.Configurations.Add(new PostMapping()); base.OnModelCreating(modelBuilder); } public DbSet<Blog> Blogs { get; set; } public DbSet<Post> Posts { get; set; } public DbSet<AuthorDetail> AuthorDetails { get; set; } public IQueryable<T> Find<T>() where T : class { return this.Set<T>(); } public void Refresh() { this.ChangeTracker.Entries().ToList().ForEach(x=>x.Reload()); } public void Commit() { this.SaveChanges(); } } }
Our solution starts off, as always, with a test that defines the behaviour that we wish to code. We focus on this test at the beginning of each recipe to reaffirm the importance of test-driven code that only accomplishes the goal and nothing more.
Secondly, we set up our domain objects. The simple version is that Blog
has many posts and Post
has many Tags
, but Tag
also has many posts. This can create some interesting options for the database for reference, but for the objects, it is a very simple task of putting collections into each object.
The HasMany()
method starts on one side of this many-to-many relationship. It tells the code that it is looking for a related set of objects.
This WithMany()
method loops back to Post
, which allows us to find a tag, and then find all the posts that it is used in. This not only creates some power for searching the application, but also creates multiple cascade paths for the cascading deletes. That is why WillCascadeOnDelete(false)
had to be used in the Blog
configuration. Any cascaded deletes that hit this will cause errors and, therefore, have to be turned off. Unrelated
cascades are fine though.
Many-to-many relationships in the database are fairly simple in the configuration. There are very few extras here.