Code First approach allows you to create the Domain Model classes on your own and let Entity Framework do the hard work creating the actual database for you. We have seen in previous post an introduction in Code First development with Entity Framework where we discussed how Code First works by default and how you can interfere to prevent it’s default behavior. We also mentioned that there are three different ways to add your configurations highlighting the one that uses different EntityTypeConfiguration class for each entity, keeping that way the model classes clearer. In this post we will see how to manage relationships between database tables by configuring the domain classes. Moreover, will see:
- How Code First manages relationships by default
- Which are the options to alter the default behavior
- How Cascade works by default
- What does Coplex Type mean and how it is used
Let’s start by creating a new solution named CodeFirstRelationships in Visual Studio 2012. Add a class library project named Model. This will be our domain classes project. Add a new c# class file named Product and paste the following code.
namespace Model { public class Product { public int ProductID { get; set; } public string Name { get; set; } public decimal Price { get; set; } public string Description { get; set; } public List<Order> Orders { get; set; } } }
Obviously, you need to add a new class named Order like the following.
public class Order { public int OrderID { get; set; } public string CustomerName { get; set; } public string CustomerCountry { get; set; } public string CustomerCity { get; set; } public string CustomerZipCode { get; set; } public DateTime OrderDate { get; set; } public Product Product { get; set; } }
In order to keep the example simple, each Product can have many orders and each order can be associated with only one product. We build our domain model, let’s create the Data Access layer now and let Entity Framework know about our classes. Add a new class library project named DataAccess in your solution and make sure you install Entity Framework from the Nuget Package Manager in the way we saw in the previous post and add a reference to the Model project. Add a new class named ProductsContext pasting the following code.
using System.Data.Entity; using Model; namespace DataAccess { public class ProductsContext : DbContext { public DbSet<Product> Products {get; set;} public DbSet<Order> Orders { get; set; } } }
Next add a new C# Console Application project named Client where we are going to create Product and Order objects. Make sure you install Entity Framework again and reference both the Model and the DataAccess projects. Paste the following code in your main method in order to add a Product object in your database. As soon as you build your project, run the solution and take a look the database the Entity Framework has created for you, either in the localhost\SQLEXPRESS or the localdb\v11.0 instance.
using Model; using DataAccess; using System.Data.Entity; namespace Client { class Program { static void Main(string[] args) { Database.SetInitializer(new DropCreateDatabaseIfModelChanges<ProductsContext>()); InsertProduct("Product 1", "Product 1 Description", 200m); } private static void InsertProduct(string name,string description,decimal price) { var product = new Product { Name =name, Description =description, Price = price, }; using (var context = new ProductsContext()) { context.Products.Add(product); context.SaveChanges(); } } } }
Entity Framework was able to figure out the One-To-Many relationship between the Products and the Orders table from our domain classes but still by convention it named the FK key Product_ProductID keeping it also nullable. Let’s see how to overpass this convention. First of all let’s make the FK constraint required. Create a new folder named Configurations inside the DataAccess project, add a new class file named DomainModelConfigurations pasting the following code:
using System.Data.Entity.ModelConfiguration; using System.ComponentModel.DataAnnotations.Schema; using Model; namespace DataAccess.Configurations { public class ProductConfiguration : EntityTypeConfiguration<Product> { public ProductConfiguration() { HasMany(p => p.Orders).WithRequired(o => o.Product); } } }
What we have told here is that a Product can have many Orders, but an Order must always be associated with a Product. You need to add this configuration in the ProductsContext class like this:
using System.Data.Entity; using Model; using DataAccess.Configurations; namespace DataAccess { public class ProductsContext : DbContext { public DbSet<Product> Products {get; set;} public DbSet<Order> Orders { get; set; } protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Configurations.Add(new ProductConfiguration()); } } }
At this time, if your build and run your application again (assuming you have closed any connections to the database) you should see that the FK key is not nullable anymore.
Now, let’s see how to change the default FK named given by the Entity Framework. The only thing you need to do, is to add a new field in the Order Domain class named ProductID.
public class Order { public int OrderID { get; set; } public string CustomerName { get; set; } public string CustomerCountry { get; set; } public string CustomerCity { get; set; } public string CustomerZipCode { get; set; } public DateTime OrderDate { get; set; } public int ProductID { get; set; } public Product Product { get; set; } }
In fact you can remove if you want the configuration we added before for the Product domain class cause EF will assume it anyway.
We have seen so far both how to rename and make an FK key required. Now we can add a new Order by defining either the ProductID or the Product object in case it has been loaded in memory. Add a new function to insert an order and call it in your main. Bind the ProductID order’s property into the first product we have added before.
class Program { static void Main(string[] args) { Database.SetInitializer(new DropCreateDatabaseIfModelChanges<ProductsContext>()); //InsertProduct("Product 1", "Product 1 Description", 200m); InsertOrder(1, "Christos", "Greece", "Athens", "12345"); } private static void InsertProduct(string name,string description,decimal price) { var product = new Product { Name = name, Description = description, Price = price, }; using (var context = new ProductsContext()) { context.Products.Add(product); context.SaveChanges(); } } private static void InsertOrder(int productID,string name,string country,string city, string zipcode) { var order = new Order { ProductID = productID, CustomerName = name, CustomerCountry = country, CustomerCity = city, CustomerZipCode = zipcode, OrderDate = DateTime.Now, }; using (var context = new ProductsContext()) { context.Orders.Add(order); context.SaveChanges(); } } }
We added an Order for the “Product 1” but what will happen if we try to delete that product from the database? By default Code First will delete any associated orders to that product. Let’s give it a try. Add and call the following function in your main method.
private static void DeleteProduct(int ID) { using (var context = new ProductsContext()) { //Find a product with Primary Key = ID var product = context.Products.Find(ID); if (product != null) { context.Products.Remove(product); context.SaveChanges(); } } }
You may don’t wish that behavior and have more control over how should deleting primary objects impacts all your foreign key objects. You can do that simply adding a configuration to your domain classes. First you need to define the relationship between the two tables and then define that you don’t want the Cascade option on Delete Rule.
public ProductConfiguration() { HasMany(p => p.Orders).WithRequired(o => o.Product).WillCascadeOnDelete(false); }
Cause the Model has changed the all database will be created from scratch so make sure first to add a product, then an order and finally delete the first product in your main method.
static void Main(string[] args) { Database.SetInitializer(new DropCreateDatabaseIfModelChanges<ProductsContext>()); InsertProduct("Product 1", "Product 1 Description", 200m); InsertOrder(1, "Christos", "Greece", "Athens", "12345"); DeleteProduct(1); }
Build and run your application. Make sure you get a nice big fat exception 🙂 That’s normal though, if you recall the Delete Rule has changed from Cascade to No Action which means that you must delete the associated orders by yourself before actual call the context.SaveChanges() function.
private static void DeleteProduct(int ID) { using (var context = new ProductsContext()) { //Find a product with Primary Key = ID var product = context.Products.Find(ID); if (product != null) { context.Products.Remove(product); // You also need to remove associated Order's by yourself now // cause Delete Rule has changed from Cascade to No Actioin List<Order> productOrders = context.Orders.Where(o => o.ProductID == ID).ToList(); foreach (Order order in productOrders) context.Orders.Remove(order); context.SaveChanges(); } } }
One last thing I wanna show you on this post is the Complex Type in Code First. If you remember the Order Domain class has some properties for the customer that makes the order.
public class Order { public int OrderID { get; set; } public string CustomerName { get; set; } public string CustomerCountry { get; set; } public string CustomerCity { get; set; } public string CustomerZipCode { get; set; } public DateTime OrderDate { get; set; } public int ProductID { get; set; } public Product Product { get; set; } }
Wouldn’t be better if our Order class look like that?
public class Order { public int OrderID { get; set; } public Customer Customer { get; set; } public DateTime OrderDate { get; set; } public int ProductID { get; set; } public Product Product { get; set; } }
And of course you need a Customer class..
public class Customer { public string CustomerName { get; set; } public string CustomerCountry { get; set; } public string CustomerCity { get; set; } public string CustomerZipCode { get; set; } }
Notice that if you want to use a Complex Type inside a Domain Model class, that complex type is required to have pass some validations:
- Cannot have a Key property (e.g CustomerID)
- It cannot be used as a collection type (e.g List Customers)
- It can contain only primitive properties
After you have declared the Customer class, you need to make Entity Framework aware that this class is a Complex Type not an Entity.
protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Configurations.Add(new ProductConfiguration()); modelBuilder.ComplexType<Customer>(); }
And of course, if you want you can treat that class as if it was an Entity and make relative configurations. This time though, the configuration class must inherit the ComplexTypeConfiguration class.
public class CustomerConfiguration : ComplexTypeConfiguration<Customer> { public CustomerConfiguration() { Property(c => c.CustomerName).IsRequired(); } }
Make the relative additions on the OnModelCreating event..
protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Configurations.Add(new ProductConfiguration()); modelBuilder.Configurations.Add(new CustomerConfiguration()); modelBuilder.ComplexType<Customer>(); }
You need to add a new empty constructor for the Order class now.
public class Order { public int OrderID { get; set; } public Customer Customer { get; set; } public DateTime OrderDate { get; set; } public int ProductID { get; set; } public Product Product { get; set; } public Order() { Customer = new Customer(); } }
Before running your application change slightly the InsertOrder function..
private static void InsertOrder(int productID,string name,string country,string city, string zipcode) { var order = new Order { ProductID = productID, Customer = new Customer { CustomerName = name, CustomerCountry = country, CustomerCity = city, CustomerZipCode = zipcode }, OrderDate = DateTime.Now, }; using (var context = new ProductsContext()) { context.Orders.Add(order); context.SaveChanges(); } }
We declared the Customer class as a Complex Type so Entity Framework wont create another table named Customers in the database. Instead, it created relative properties inside the Order table. That’s all for now. You can download the project we created from here. I hope you enjoyed the post.
Categories: ADO.NET
Leave a Reply