EF Core 1.1

Implementing provider extension methods

A previous post gave an outline of EF Core metadata. That post showed the extension methods used by providers to add provider-specific functionality to EF. This post describes how to implement those methods. This post is aimed at provider writers or those who may want to contribute to the EF Core source code.

Annotations

All provider-specific functionality is implemented through annotations applied to the various elements of the model. These annotations are expected to follow a standard naming convention of <provider_name>:. For example, the SQL Server memory-optimized annotation name is SqlServer:MemoryOptimized.

These are often defined as constants in the code. For example:

public static class SqlServerAnnotationNames
{
    public const string Prefix = "SqlServer:";
    public const string Clustered = "Clustered";
    public const string MemoryOptimized = "MemoryOptimized";
    // More...
}

Relational annotations

There are a set of annotations that are common to all relational providers. These are defined in the RelationalAnnotationNames class:

public static class RelationalAnnotationNames
{
    public const string Prefix = "Relational:";
    public const string TableName = "TableName";
    public const string Schema = "Schema";
    // More...
}

This is an internal class and provider code should not need to access it directly, as we will see below.

Relational annotations can be used in two ways:

This allows, for example, the table name to be set for all providers but then overridden to something else just for one provider, as was described in the metadata overview post.

The FullAnnotationNames class

It was found that the repeated concatenation of prefix and annotation name could cause perf issues. Therefore, each provider should create a FullAnnotationNames class where the concatenation can be done only once. For relational providers, this should inherit from RelationalFullAnnotationNames. For example:

public class SqlServerFullAnnotationNames : RelationalFullAnnotationNames
{
    protected SqlServerFullAnnotationNames(string prefix)
        : base(prefix)
    {
        Clustered = prefix + SqlServerAnnotationNames.Clustered;
        MemoryOptimized = prefix + SqlServerAnnotationNames.MemoryOptimized;
    }

    public new static SqlServerFullAnnotationNames Instance { get; } 
        = new SqlServerFullAnnotationNames(SqlServerAnnotationNames.Prefix);

    public readonly string Clustered;
    public readonly string MemoryOptimized;
}

Notice that this class is a singleton, with the single instance available via the Instance property. This will be used in the code below.

Core metadata extensions

As was discussed in the metadata overview post, use of the annotations described above is hidden behind provider extension methods, such as the SqlServer() method. These methods can be implemented for the entity types, properties, etc. as required. Here is the SqlServer() method for extending entity types:

public static SqlServerEntityTypeAnnotations SqlServer(this IMutableEntityType entityType)
    => (SqlServerEntityTypeAnnotations)SqlServer((IEntityType)entityType);

public static ISqlServerEntityTypeAnnotations SqlServer(this IEntityType entityType)
    => new SqlServerEntityTypeAnnotations(entityType);

It's actually two methods: one for the read-only IEntityType, and one for read-write IMutableEntityType. They both return the same object, but the read-only version is only exposed as an immutable interface.

The IXxxTypeAnnotations interface

The ISqlServerEntityTypeAnnotations interface returned looks something like this:

public interface ISqlServerEntityTypeAnnotations : IRelationalEntityTypeAnnotations
{
    bool IsMemoryOptimized { get; }
}

Pretty simple. Notice that since SQL Server is a relational provider this interface inherits from IRelationalEntityTypeAnnotations, which has all the common relational extensions on it:

public interface IRelationalEntityTypeAnnotations
{
    string TableName { get; }
    string Schema { get; }
    // More...
}

The XxxTypeAnnotations class

The implementation of this interface is also pretty simple:

public class SqlServerEntityTypeAnnotations
    : RelationalEntityTypeAnnotations, ISqlServerEntityTypeAnnotations
{
    public SqlServerEntityTypeAnnotations(IEntityType entityType)
        : base(entityType, SqlServerFullAnnotationNames.Instance)
    {
    }

    public virtual bool IsMemoryOptimized
    {
        get { return EntityType[SqlServerFullAnnotationNames.Instance.MemoryOptimized] as bool? ?? false; }
        set { ((IMutableAnnotatable)EntityType)[SqlServerFullAnnotationNames.Instance.MemoryOptimized] = value; }
    }
}

Since this is a relational provider the class inherits from RelationalEntityTypeAnnotations. This base class takes care of all the common relational extensions such as TableName and Schema so the provider doesn't have to do anything. The base class uses the SqlServerFullAnnotationNames.Instance passed to the base constructor to determine which annotations to use.

Provider-specific extensions, such as IsMemoryOptimized, simply access annotations on the IEntityType passed to the constructor. Two things to notice about this are:

Fluent API extension methods

Most application code will configure the model using the fluent API rather than the lower-level core metadata described above. Fortunately, once the core metadata extensions have been implemented it becomes trivial to add fluent API. For example:

public static class SqlServerEntityTypeBuilderExtensions
{
    public static EntityTypeBuilder ForSqlServerToTable(
        this EntityTypeBuilder entityTypeBuilder, string name, string schema)
    {
        entityTypeBuilder.Metadata.SqlServer().TableName = name;
        entityTypeBuilder.Metadata.SqlServer().Schema = schema;

        return entityTypeBuilder;
    }

    public static EntityTypeBuilder<TEntity> ForSqlServerToTable<TEntity>(
        this EntityTypeBuilder<TEntity> entityTypeBuilder, string name, string schema)
        where TEntity : class
        => (EntityTypeBuilder<TEntity>)ForSqlServerToTable((EntityTypeBuilder)entityTypeBuilder, name, schema);

    public static EntityTypeBuilder ForSqlServerIsMemoryOptimized(
        this EntityTypeBuilder entityTypeBuilder, bool memoryOptimized = true)
    {
        entityTypeBuilder.Metadata.SqlServer().IsMemoryOptimized = memoryOptimized;

        return entityTypeBuilder;
    }

    public static EntityTypeBuilder<TEntity> ForSqlServerIsMemoryOptimized<TEntity>(
        this EntityTypeBuilder<TEntity> entityTypeBuilder, bool memoryOptimized = true)
        where TEntity : class
        => (EntityTypeBuilder<TEntity>)ForSqlServerIsMemoryOptimized((EntityTypeBuilder)entityTypeBuilder, memoryOptimized);
}

The things to notice about this code are:

Summary

Providers extend metadata through the use of annotations, which should follow a standard naming convention. Providers use extension methods over core metadata so that application code never needs to see the annotations directly. Providers should also ship with fluent API extensions for use in common model building scenarios.


This page is up-to-date as of November 10th, 2016. Some things change. Some things stay the same. Use your noggin.