Code Only Enhancements

We've been working hard on Code Only since the first preview.

In the next release you will be able to specify:

  1. Navigation Property Inverses.
  2. Property Facets, like Nullability, MaxLength, Precision etc.
  3. Property to Column mappings
  4. Type to Table(s) mappings
  5. Inheritance strategy
  6. Encapsulate configuration

The rest of this post will drill into each of these features in turn.

Registering NavigationProperty inverses:

You can now Register Inverses, i.e. link one navigation property to another, like this:

builder.RegisterInverse(
(Customer c) => c.Orders,
(Order o) => o.Customer)
);

This code indicates that Customer.Orders is the other end of the Order.Customer relationship. Adding order1 to the customer1.Orders collection, has the same effect as setting the order1.Customer to customer1.

Specifying Property Facets:

You can also specify property facets, i.e. things like Nullability, MaxLength, Precision etc, like this:

var customerConfig = new EntityConfiguration<Customer>();
// We can infer that the ID is the Primary Key,
// but not that it is generated in the database on insert.
customerConfig.ForProperty(c => c.ID)
.Identity();
customerConfig.ForProperty(c => c.Name)
.MaxLength(100)
.NonUnicode();
customerConfig.ForProperty(c => c.Website)
.MaxLength(200)
.Nullable()

builder.Configure(customerConfig);

This configures the Customer type so that:

  • The ID property is an Identity column, i.e. the value is computed by the database when we do an insert.
  • The Name property has a MaxLength of 100 characters and is NonUnicode i.e. in SqlServer VARCHAR rather than NVARCHAR.
  • The Website property has a MaxLength of 200 characters and is Nullable.

These facets target the Conceptual Model (i.e. CSDL), and from there flow to the database too (i.e. SSDL).

Encapsulating Facet Configuration

You can create a class to encapsulate all this configuration by deriving from EntityConfiguration<T>.

For example:

public class CustomerConfig: EntityConfiguration<Customer>
{
public CustomerConfig(){
ForProperty(c => c.ID)
.Identity();
ForProperty(c => c.Name)
.MaxLength(100)
.NonUnicode();
ForProperty(c => c.Website)
.MaxLenght(200)
.Nullable();
}
}

We recommend creating classes like this instead of configuring an EntityConfiguration<> because of the encapsulation benefits.

Specifying the Tablename

When you use Configure<T>(..) the EF infers a default mapping, inheritance strategy (TPH) and table name(s) for you.

But if you want to specify the table name you can do this:

var customerConfig = new EntityConfiguration<Customer>();
// configure the facets, as per the above example
...

// register configuration with a particular tablename
builder.Tables[“dbo.Custs”] = customerConfig;

Specifying Mappings:

If you need more control over the mappings (for example to map to an existing database or use corporate naming policies) then you can specify mappings like this:

EntityMap<Customer> customerMap =
Map.OfType<Customer>(
c => new {
cid = c.ID,
c.Name,
csite = c.Website
}
);

Interpreting a Mapping

This mapping states that the ID property is mapped to the ‘cid’ column, the Name property is stored in the ‘Name’ column and the Website property is mapped to the ‘csite’ column.

Properties not referenced are not persisted (just like properties in a partial class when using the default code generation of EF)

LINQ Comprehension Syntax

You can even specify exactly the same thing using a LINQ comprehension syntax too:

EntityMap<Customer> customerMap =
from c in Map.OfType<Customer>()
select new {
cid = c.ID,
c.Name,
csite = c.Website
};

Specifying Facets with Mapping

Once you’ve configured the mapping you can also specify facets on the map like this:

customerMap.ForProperty(c => c.ID)
.Identity();
customerMap.ForProperty(c => c.Name)
.MaxLength(100)
.NonUnicode();
customerMap.ForProperty(c => c.Website)
.MaxLenght(200)
.Nullable();

Specifying the table

The final step is to assign the map to a table.

builder.Tables[“dbo.Custs”] = customerMap;

Now we’ve specified a custom table, mapping and custom facets for our Customer class.

Specifying Inheritance:

The default inheritance strategy used by CodeOnly is Table Per Hierarchy (or TPH).

If however you need a different strategy you need to dive in and configure the mappings.

Imagine if you have three classes you need to map: Vehicle , Car and Boat where both Car and Boat are derived from Vehicle which is abstract.

clip_image001

Table Per Hierarchy (TPH)

If you want to map this using TPH, you could do it like this:

var vehicleMap =
Map.OfTypeOnly<Vehicle>(
v => new {
vid = v.ID,
v.Name,
vdesc = v.Description
v.MaxPassengers,
}
).Union(Map.OfTypeOnly<Car>(
c => new {
            vid = c.ID,
c.Name,
vdesc = c.Description
c.MaxPassengers,
trans = c.Transmission,
tspd = c.Topspeed,
ccty = c.EngineCapacity,
ncyld = c.NoCylinder,
discriminator = “CAR”
})
).Union(Map.OfTypeOnly<Boat>(
b => new {
            vid = b.ID,
b.Name,
vdesc = b.Description
b.MaxPassengers,
lng = b.Length,
b.HasSail,
b.HasEngine
discriminator = “BOAT”
})
);

builder.Tables[“dbo.vehicles”] = vehicleMap;

In a TPH mapping:

  1. OfTypeOnly() is used to create mapping fragments.
  2. Mapping fragments are then unioned so they can be assigned to one table (TPH uses a single table for the whole hierarchy).
  3. A discriminator column is required for each non-abstract type. The discriminator column can have any name and must have a different constant value (i.e. “CAR”) for each non abstract type in the hierarchy.
  4. When mapping a derived type you must re-map all properties mapped in the base-type(s).

Table Per Type (TPT)

If you want to map the same hierarchy using Table Per Type or TPT, this is how you do it:

builder.Table[“dbo.Vehicles”]=
Map.OfType<Vehicle>(
v => new {
vid = v.ID,
v.Name,
vdesc = v.Description
v.MaxPassengers,
}
);

builder.Tables[“dbo.Cars”] =
Map.OfType<Car>(
c => new {
cid = c.ID,
trans = c.Transmission,
tspd = c.Topspeed,
ccty = c.EngineCapacity,
ncyld = c.NoCylinders,
}
);

builder.Tables[“dbo.Boats”] =
Map.OfType<Boat>(
b => new {
bid = b.ID,
lng = b.Length,
b.HasSail,
b.HasEngine
}
);

In a TPT mapping:

  1. OfType<>() is used in each mapping 'fragment'
  2. Each mapping 'fragment' is assigned to a different table.
  3. Each mapping 'fragment' only maps properties declared on the current type, except…
  4. Key properties must be mapped in every fragment (this allows the tables participating in the type to be joined).
  5. There are no discriminators

Table Per Class (TPC)

You can also map this hierarchy using TPC like this:

builder.Tables[“dbo.Cars”] =
Map.OfTypeOnly<Car>(
c => new {
cid = c.ID,
c.Name,
vdesc = c.Description
c.MaxPassengers,
trans = c.Transmission,
tspd = c.Topspeed,
ccty = c.EngineCapacity,
ncyld = c.NoCylinders,
}
);

builder.Tables[“dbo.Boats”] =
Map.OfTypeOnly<Boat>(
b => new {
bid = b.ID,
b.Name,
description = b.Description
b.MaxPassengers,
lng = b.Length,
b.HasSail,
b.HasEngine
}
);

In a TPC Mapping:

  1. Like TPH we use OfTypeOnly(..)
  2. But unlike TPH each non-abstract type has a its own table. (So there is no table for Vehicles because it is abstract).
  3. Each mapping fragment re-maps every, non-transient, property.
  4. There is no mapping for any abstract classes in the hierarchy.
  5. There are no discriminators

Default Foreign Key Locations:

If we see a reference (i.e. Order.Customer) we assume the multiplicity is 0..1. Meaning the the Foreign Key or FK is nullable.

If we see a collection (i.e. Customer.Orders) we assume the multiplicity is many.

Then when you Register Inverses, we know the multiplicity of both ends of the relationships i.e. in the above example we know that there are 0..1 Customers for an Order and many Orders for a Customer.

So by convention there are 3 main types of relationships, we need to infer the FK location for:

0..1 to many –> by convention we put the FK on the many end. So in the Customer.Orders example the FK is put in the Orders table. 

many to many –> there is no option but to introduce a join table

0..1 to 0..1 –> You can configure where the FK lives, but in the absence of configuration we will introduce a join table.

Sometimes however a reference is not 0..1 it is 1, i.e. the FK, wherever it might be, may not be nullable.

This is how you specify that the FK is non-nullable:

var orderConfig = builder.Configure<Order>();
orderConfig.RegisterInverse(o => o.Customer, c => c.Orders);
orderConfig.ForProperty(o => o.Customer).NonNullable();

This tells us that there is exactly 1 Customer per Order and many Orders for a Customer.

The ability to tell us that a reference is NonNullable introduces several new multiplicity combinations, for which we need conventions too:

1 to many –> by convention we put the FK on the many end, and make it non-nullable in the database.

0..1 to 1 –> by convention we put the FK on the 1 end, and make it non-nullable.

1 to 1 –> like 0..1 to 0..1 we can’t decide where to put the FK so by convention we introduce a join table.

Specifying FK Mappings:

So far we’ve created mappings for the properties of entities.

What about Navigation Properties and FKs?

All types of relationships (except many to many) can be modeled without a join table in the database. So we allow you to map FKs as part of an EntityMap like this:

EntityMap<Customer> customerMap =
from c in Map.OfType<Customer>()
select new {
cid = c.ID,
c.Name,
csite = c.Website,
salesPersonFK = c.SalesPerson.ID
};

customerMap.RegisterInverse(c => c.SalesPerson, s => c.Clients);
builder.Tables[“dbo.Custs”] = customerMap;

This specifies that the Customer.SalesPerson navigation property, and it’s inverse SalesPerson.Customers, are stored in the salesPersonFK column in the dbo.Custs table. Because the fragment maps the salesPersonFK column to the c.SalesPerson.ID, where SalesPerson.ID is the Primary Key (or part of the PrimaryKey) of the related SalesPerson entity, and of course FK’s point to PK’s.

Specifying Join Table Mappings:

In the case of many to many relationships you must have a join table, so absent mapping information we produce a join table by convention.

If however you need more control you can do this:

var blogPostsMap = new AssociationMap<Blog, Post>(
b => b.Posts
).Map(
(b, p) => new {BlogId = b.ID, PostId = p.ID}
);

builder.Tables[“dbo.BlogPosts”] = blogPostsMap;

This says the many to many relationships between Blog and Post is stored in the ‘dbo.BlogPosts’ table which has two columns:

  • ‘BlogId’ which is also an FK pointing to the column to which the ID property of Blog is mapped.
  • ‘PostId’ which is also an FK pointing to the column to which the ID property of Post is mapped.

Entity Splitting:

Code Only can even support more advanced mapping strategies like entity splitting:

builder.Tables[“dbo.Customer”] = Map.OfType<Customer>(
c => new {
cid = c.ID,
c.Name,
active = c.IsActive
}
);

builder.Tables[“dbo.CustomerDetails”] = Map.OfType<Customer>(
c => new {
cid = c.ID,
c.Size,
c.Industry
}
);

This says that the Customer entity is split across the dbo.Customer and dbo.CustomerDetails tables.

Encapsulating all the Configuration:

You can also write a class that derives from EntityMap<T>, to hold all the mapping, facets etc. For example here a class that holds the configuration for Product

public class ProductMap: EntityMap<Product>
{
public ProductMap{
this.Map( p => new {
pid = p.ID,
pcode = p.Name,
cid = p.Category.ID
});
this.ForProperty(p => p.ID).Identity();
this.ForProperty(p => p.Name).MaxLength(100)
.NonUnicode();
this.ForProperty(p => p.Category).NonNullable();
this.RegisterInverse(p => p.Category,
c => c.Products);
}
}

This approach is highly recommended, because configuring the Product type is now trivial:

builder.Tables[“dbo.Products”] = new ProductMap();

Summary:

As you can see we are planning a lot of enhancements which will allow most core scenarios.

What do you think? Do you like the API? Are there things you’d like to see look a little different?

As always we are very keen to hear your feedback.

Alex James
Program Manager, Entity Framework Team, Microsoft.

This post is part of the transparent design exercise in the Entity Framework Team. To understand how it works and how your feedback will be used please look at this post .