Design Meeting Notes - February 28, 2013

Code First mapping to nested types

Recent changes to use Code First CLR type annotations for runtime o/c mapping open up the possibility of allowing nested types to be used in Code First models. However, it is not clear how these types should appear in the conceptual model and what the default mapping to database tables should be.

Scenarios

There are two general scenarios for using nested types in Code First. The first is to embed an entire model inside an outer type—for example:

public class SomeClass
{
    public class Blog
    {
        public int BlogId { get; set; }
        public ICollection<Post> Posts { get; set; }
    }

    public class Post
    {
        public int PostId { get; set; }
        public int BlogId { get; set; }
        public virtual Blog Blog { get; set; }
    }
}

Examples of this are models nested in test classes, such as in the EF tests, and models created in interactive sessions in languages like F# where there is an implicit outer “module” type.

The second scenario is nesting a type inside an outer type because it is semantically related to the use of the outer type. This is commonly done for enums—for example:

public class Cheese
{
    public int Id { get; set; }
    public Type CheeseType { get; set; }
    
    public enum Type
    {
        Wensleydale,
        Cheshire
    }
}

public class Pickle
{
    public int Id { get; set; }
    public Type PickleType { get; set; }

    public enum Type
    {
        Branston,
        Piccalilli
    }
}

Type naming

Options for type mappings are:

  • Use outer-class qualified CLR names directly
    • For example, Cheese+Type, SomeClass+Blog
    • However, the ‘+’ is makes this an invalid EDM name
    • The advantage of this choice is that the names should be unique, so the case with Cheese.Type and Pickle.Type would work
    • We could relax EDM naming rules to allow ‘+’, but technically this would need a schema version rev and would have implications for other consumers of EDM
  • Use modified CLR names
    • Building on the case above, we could replace ‘+’ with some valid EDM character—for example, Cheese_Type, SomeClass_Blog
    • We could also drop the character entirely—for example, CheeseType, SomeClassBlog
    • This is not unique, but there is limited scope for collisions—the cheese and pickle example still works
    • This change would require Code First conventions for matching type names to be updated
    • One issue with using ‘_’ or other non-alphabetical characters is that names will not be pluralized
  • Use simple names
    • For example, Type, Blog
    • This looks nice in the model and database, especially when the entire model is nested inside an outer type
    • However, the nested type names are now not unique, so the cheese and pickle example will now not work
  • Make the outer class name become part of the namespace name
    • This would result in different types in the same conceptual model having different namespaces, which is something that is theoretically supported but not really used
    • The CSDL XML format doesn’t make this easy to do since the namespace is part of the top-level schema element
    • The names may not be 100% unique, but collisions would be very unlikely
    • The main advantage of this approach is that types with the same simple name but different namespaces/outer types could be used in the same model
      • Note that this is different from types with the same simple name but different namespaces being used in different models (or one type in the model, one type not in any model) which is now supported in Code First anyway
    • For the store side, we would by default use the simple name and uniquify if necessary.

Decision: We will look into making the outer name part of the namespace name. However, this is likely to be too complicated to do for EF6. if this is the case, then we will use simple names for now, with the idea that we can potentially switch to the namespace approach in the future without it changing the store mapping that is generated. This means that for EF6 is may not be possible to have two nested types with the same simple name in the same model.

Back compatibility

Nested types should be discovered by convention when using the latest DbModelBuilderVersion. When using an older builder version nested types will not be discovered. However, we will not prevent them from being explicitly mapped. (Compare this to enums and spatials where such explicit mappings are not allowed because they require a v3 schema.)

Currently the type mapper gets is model builder information from EdmModel as the schema version. This will need to be changed to the model builder version because in this case the schema version has not changed while the model builder version has.

Connection resiliency execution strategy configuration

Currently configuring EF to use the SQL Azure an execution strategy is done by deriving from a special DbConfiguration type:

public class BlogConfiguration : SqlAzureDbConfiguration
{
}

This is problematic because it is a pattern that can only be used once (since there is no multiple inheritance) and limits how applications can define or make use of DbConfiguration hierarchies.

An alternative is to add sugar methods to DbConfiguration like we do for other resolvers. For example:

public class BlogConfiguration : DbConfiguration
{
    public BlogConfiguration()
    {
        AddExecutionStrategy(
            "System.Data.SqlClient",
            () => new SqlAzureExecutionStrategy());
    }
}

This is nice, but would be better if the provider invariant name did not need to be explicitly set. This could be handled by introducing an attribute that can be added to various service classes such that if the attribute is found then it will be used as the provider invariant name to register. This would allow the following:

    AddExecutionStrategy(
        () => new SqlAzureExecutionStrategy());

    AddExecutionStrategy(
        "System.Data.SqlClient",
        () => new SqlAzureExecutionStrategy());

    AddExecutionStrategy(
        () => new SqlAzureExecutionStrategy(),
        "MyServer");

    AddExecutionStrategy(
        "System.Data.SqlClient",
        () => new SqlAzureExecutionStrategy(),
        "MyServer");

Note that server name is an optional part of the resolver key.

Use of disposed DbContextTransaction

Currently a call to Rollback or Commit will be silently ignored when called on a disposed DbContextTransaction. This is clearly wrong for Commit, which should throw. Arguments can be made both ways for Rollback, but ultimately we decided to just delegate to the underlying disposed DbTransaction in both cases. For SqlTransaction this means that Commit will throw and Rollback will be a no-op.

Last edited Mar 1, 2013 at 2:18 AM by ajcvickers, version 2