Many times the data structure we need in our applications isn’t as simple as the ideal examples found at most manuals. Sometimes we need more advanced mechanics for the persistence of our data entities.
In today’s article, we’re going to create an entity structure containing a collection of polymorphic elements, in other words, a collection that can contain several object types, all of them inheriting from a common ancestor class.
The problem with polymorphic collections is that, even though their components share a common class, they don’t share all the same properties implemented, so they can’t be saved in a single table at the database. They need a different table for every different final entity.
This is a very common problem but it’s a little difficult to explain. For this, I’ve created a little example project in a “real world” context to try to explain it the best I can, using Entity Framework 6.0 Code First with POCO entities and using the TPT (Table Per Type) feature of EF 6.0+ that simplifies a lot the scenario.
This article includes a link to my Git repository, where you can find a full example solution that will allow you to see the result by yourself, and get your hands dirty making new tests.
Download the solution in .ZIP format
Clone the GIT repository
Data model
For this example, in our test scenario we need an application able to keep a list of cars with all their car parts. To keep it simple, in our Car
class, we will only have a Model name and a collection with the parts composing it, with three types of parts: Wheel
, Door
and Seat
.
Our start point is a Car that can have n parts, but instead of making a collection for every part type, we will make a single collection from a unique class called CarPart
that will contain all the parts even if they’re different. We will accomplish this by making all the parts Door
, Wheel
and Seat
inherit from CarPart
, where we will only have a SerialNo that is common to all the part types.
public virtual ICollection<CarPart> Parts { get; set; }
The problem we are facing when we save or read our data from the database, is that Parts
elements are not homegeneous, in other words, a Wheel has width, radius, brand and so on, while a door has its position in the car and a seat has material and style, for example. This can’t be saved in the same table (without sparing a lot of data columns that won’t be used). To make it right, we must save each row in it’s own table depending on the part type.
For architectural reasons, it’s better and easier to keep all the parts in a single Parts
collection instead of having a collection for every part type. That last scenario would force us to write much more code when, for example, we need a part by its serial number.
Normally, having a single collection would require some complex code at our infrastructure layer, having to check for every object’s type in the collection and calling it’s context to save it. Using Entity Framework‘s TPT makes this process very trivial.
Creating our Domain
We’ll start creating our POCO entities. BaseEntity
is our base entity (obvious?) and it only contains a long property Id
.
public class BaseEntity { public long Id { get; set; } }
The central class of our Domain is Car
. It contains a model name and the polymorphic collection of parts.
public class Car : BaseEntity { public string Model { get; set; } public virtual ICollection<CarPart> Parts { get; set; } }
The CarPart
entity is our union between the parts and the car. It will only contain the CarId
of the car it belongs to, and a SerialNo that will be common to all parts, of any type.
public class CarPart : BaseEntity { public long CarId { get; set; } public virtual Car Car { get; set; } public string SerialNo { get; set; } }
From here, we will define the classes for every part type, inheriting from CarPart
.
public class Door : CarPart { public enum DoorPositionEnum { FrontLeft = 0, FrontRight = 1, RearLeft = 2, RearRight = 3 } public DoorPositionEnum Position { get; set; } }
public class Seat : CarPart { public enum SeatMaterialEnum { Fabric = 0, Leather = 1 } public enum SeatTypeEnum { Comfort = 0, Sportive = 1 } public string Brand { get; set; } public SeatMaterialEnum Material { get; set; } public SeatTypeEnum Type { get; set; } }
public class Wheel : CarPart { public double Radius { get; set; } public double Width { get; set; } public string Brand { get; set; } }
Once we have all our POCO entities, we must prepare our infrastructure project for Entity Framework 6.0, and there, create the mappers for every entity class against the database.
Mapping entities for EF 6.0 Code First
We will create a BaseEntityConfiguration
class inheriting EF’s EntityTypeConfiguration
to map the Id
in all our entities.
public class BaseEntityConfiguration<TEntity> : EntityTypeConfiguration<TEntity> where TEntity : BaseEntity { public BaseEntityConfiguration() { this.HasKey(t => t.Id); this.Property(t => t.Id) .HasDatabaseGeneratedOption(System.ComponentModel.DataAnnotations .Schema.DatabaseGeneratedOption.Identity); } }
As you can see in the example, we are telling EF that the key of our table will be always Id
with .HasKey
and then we add a Identity
constraint to the database’s column.
Let’s go on with the remaining mapping entities:
public class CarMap : BaseEntityConfiguration<Car> { public CarMap() : base() { this.ToTable("Cars"); this.Property(t => t.Model) .IsRequired(); } }
… indicating for each one what table will receive their data with .ToTable
and flagging required properties with .IsRequired
…
public class DoorMap : BaseEntityConfiguration<Door> { public DoorMap() { this.ToTable("Doors"); this.Property(t => t.Position).IsRequired(); } }
public class SeatMap : BaseEntityConfiguration<Seat> { public SeatMap() { this.ToTable("Seats"); this.Property(t => t.Brand).IsRequired(); this.Property(t => t.Material).IsRequired(); this.Property(t => t.Type).IsRequired(); } }
public class WheelMap : BaseEntityConfiguration<Wheel> { public WheelMap() { this.ToTable("Wheels"); this.Property(t => t.Brand).IsRequired(); this.Property(t => t.Radius).IsRequired(); this.Property(t => t.Width).IsRequired(); } }
The real EF 6.0 “magic” comes when we define the CarPart
class mapping. As we saw before, our car will have a collection of car parts called Parts
that will contain elements from any class inheriting Carpart
class. What we have to tell to Entity Framework is where it should save each different final entity based on its type, like this:
public class CarPartMap : BaseEntityConfiguration<CarPart> { public CarPartMap() : base() { this.ToTable("Parts"); this.Property(t => t.SerialNo) .IsRequired(); this.Map<Wheel>(t => t.ToTable("Wheels")) .Map<Door>(t => t.ToTable("Doors")) .Map<Seat>(t => t.ToTable("Seats")); this.HasRequired(t => t.Car) .WithMany(car => car.Parts) .HasForeignKey(t => t.CarId); } }
Let’s split this. First we are indicating the we are going to use a Parts table. This table will be an intermediate table between the car and its parts.
Following on, we set what table should receive each entity type using Map<T>
, and finally we create the one to many relation between the car and its parts.
With this information, Entity Framework will know what it has to do with each entity in our domain. Now we’re gonna tell it how to stick it all together in a database.
Creating the context
Once the entity mapping is finished, we are going to create a database context to be able to access the data:
public class CarBoundContext : DbContext { public CarBoundContext(): base("CarsDB") { Database.SetInitializer(new CreateDatabaseIfNotExists<CarBoundContext>()); } public DbSet<Car> Cars { get; set; } public DbSet<Door> Doors { get; set; } public DbSet<Wheel> Wheels { get; set; } public DbSet<Seat> Seats { get; set; } protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Configurations.Add(new CarMap()); modelBuilder.Configurations.Add(new DoorMap()); modelBuilder.Configurations.Add(new WheelMap()); modelBuilder.Configurations.Add(new SeatMap()); base.OnModelCreating(modelBuilder); } }
Our class CarBoundContext
inherits from Entity Framework’s context DbContext
, overriding its method OnModelCreating
so it gets our database definition mapping classes with modelBuilder.Configurations.Add
for each of our mappers.
We also publish DbSet
for every table we are going to use. Notice we did’nt publish a DbSet
for CarPart
since it’s a intermediate table and we won’t be using it normally, since it’s used internally by EF to get the different part types. We could add it if we should need it, anyway.
At the constructor, we are telling EF that the database should be created if it doesn’t exist with a call to SetInitializer
.
Now our context is done
The test
Once our domain and context have been created, it’s time to test. We create a console application that will make a new Car
object with some parts on it and save it, then read it again on a new variable.
class Program { private static Random _seed = new Random(); static string NewSerialNumber() { return string.Format("{0:00000000}", _seed.NextDouble() * (double)100000000); } static Car NewCar() { return new Car { Model = "Chevrolet Cruze LTZ", Parts = new List<CarPart> { new Wheel(NewSerialNumber(), "Bridgestone",17.0,225.0), new Wheel(NewSerialNumber(), "Bridgestone",17.0,225.0), new Wheel(NewSerialNumber(), "Bridgestone",17.0,225.0), new Wheel(NewSerialNumber(), "Bridgestone",17.0,225.0), new Door(NewSerialNumber(), Door.DoorPositionEnum.FrontLeft), new Door(NewSerialNumber(), Door.DoorPositionEnum.FrontRight), new Seat(NewSerialNumber(),"Sparco",Seat.SeatMaterialEnum.Leather,Seat.SeatTypeEnum.Sportive), new Seat(NewSerialNumber(), "Sparco",Seat.SeatMaterialEnum.Fabric,Seat.SeatTypeEnum.Comfort) } }; } static void PrintParts(string partName, IEnumerable<CarPart> parts) { Console.WriteLine("*** {0} ({1}):", partName, parts.Count()); parts.ToList().ForEach(Console.WriteLine); } static void Main(string[] args) { // The database will be created at bin/Debug/ by default AppDomain.CurrentDomain.SetData("DataDirectory", Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "")); // Get a new context for de DB (always "using" to dispose the connection) using (var context = new CarBoundContext()) { // If no records, let's create one... if (!context.Cars.Any()) { context.Cars.Add(NewCar()); context.SaveChanges(); } // Fetch the record and display its parts! var car = context.Cars.First(); Console.WriteLine("Model: {0}", car.Model); Console.WriteLine("Total parts: {0}", car.Parts.Count); PrintParts("Doors", car.Parts.Where(part => part is Door)); PrintParts("Seats", car.Parts.Where(part => part is Seat)); PrintParts("Wheels", car.Parts.Where(part => part is Wheel)); Console.ReadLine(); } } }
As you can see, by only making .Add(NewCar())
all the rest happens “automagically”, the sub-entities (parts) are chain-saved under the hood. One row is generated at Cars for the car, 8 rows at CarParts for all the parts, and then as much rows as parts of that type in each specific parts table.
When we read again the Car
entity, again, TPT magic looks in every needed table to gather the parts of our car, and all of them are added to our Parts
collection, where we can use Linq to retrieve and filter them with a simple .Where(part => part is XXXXXX)
to get a given type only, or for example .Where(part => part.SerialNo == "XXXXXX")
to retrieve a part using its serial number whatever type it is.
Running the solution we should get this result:
And what about the DB structure?
With EF 6.0, it’s easy to forget what’s happening underground, so it’s good to give a look to the DB that has just been created:
Conclusions and References
This is just a small tip on what Entity Framework 6.0 Code First can do for you. If you want more information on the subject, check the Microsoft Entity Framework documentation.
For more information about TPT (Table Per Type), check this link.
If you wanna see more example projects, don’t forget to visit my Git repository
Joan Vilariño
Latest posts by Joan Vilariño (see all)
- Object Factory: Creating objects with Expression Trees and Attributes - 17/02/2016
- New year, new address, new hosting! - 02/12/2015
- Using
dynamic
overloads - 21/10/2015
28/01/2019 at 22:17
Para emagrecer você não é possível que passar inópia. http://www.forumdipace.org/profile.php?mode=viewprofile&u=22995