One Unicorn

Thoughts from one member of the Entity Framework team…

  • Home
  • About
Bookmark the permalink.

Implementing provider extension methods in EF Core 1.1

Nov10 by Arthur Vickers

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>:<annotation 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:

  • With the Relational: prefix, meaning that the annotation applies to all relational providers. For example, Relational:TableName.
  • With the provider prefix, meaning that the annotation applies to only that provider. For example, SqlServer:TableName.

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:

  • It is normal for the extensions to provide a default if no annotation has been set. So if the annotation for MemoryOptimized is null, then false is returned. Extension methods may provide more complex defaults, such as looking for a value set further up the model if one has not been set on this element. The goal is for application developers to get back a useful value without thinking about where it comes from.
  • The cast to IMutableAnnotatable will fail if the IEntityType is not mutable. However, notice that IsMemoryOptimized on the interface above does not have a setter, so the only way this can fail is if the interface is cast inappropriately by the application, after which an exception can be expected. (We did at one time have more types here, but given how the API is used we were able to remove a bunch of code just by doing this way with no real loss of usability.)

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:

  • Provider-specific (e.g. IsMemoryOptimized) and common relational (e.g. ToTable) are implemented in the same way. There is no base class here.
  • The fluent API gets the core metadata type from the builder and calls the SqlServer() method to set extension properties
  • The methods return the passed-in builder to allow chaining
  • There are generic and non-generic overloads so that genericness is preserved when chaining. The generic overload just calls the non-generic overload and then casts the builder as generic again before returning it.

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.

Share this:

  • Twitter
  • Facebook

Like this:

Like Loading...

Related

This entry was posted in EF Core, Entity Framework, Metadata and tagged EF Core, EF Core Metadata, EF Core Provider, Entity Framework.

Post navigation

← Internal code in EF Core 1.1
So you want to write an EF Core provider… →

Follow me on Twitter

My Tweets

Tags

AddDbContext Agile Backing fields C# Cascade delete Change Tracking Code First Code First Migrations ConcurrentDictionary CreateDelegate CTP Data Annotations Database First Database Initializers DbCollectionEntry DbContext DbContextOptions DbContextOptionsBuilder DbModelBuilder DbSet.Add DbSet.Attach DbSet.Update Dependency Injection DetectChanges EdmMetadata EDMX EF EF4 EF4.1 EF4.2 EF4.3 EF5 EF6 EF6.1 EF7 EF Core EF Core Metadata EF Core Provider Entity Framework Enums Extra-lazy Extra-lazy.NET Extra-lazy loading Foreign Keys Foreign Keys; Complex Types immersive Independent Associations Indexs INotifyCollectionChanged INotifyPropertyChanged INotifyPropertyChanging Interception IQueryable IsLoaded IsModified Lazy loading Logging Many-to-Many Metadata Notification Entities NuGet ObjectMaterialized OData OnConfiguring OSS POCO Proxies Reflection ReplaceService SaveChanges Snapshot Change Tracking Spatial T4 Templates UseInternalServiceProvider Virtual Reality

Archives

  • September 2018
  • September 2017
  • November 2016
  • October 2016
  • May 2015
  • December 2014
  • November 2014
  • September 2014
  • February 2014
  • May 2013
  • March 2013
  • October 2012
  • July 2012
  • June 2012
  • May 2012
  • April 2012
  • March 2012
  • February 2012
  • January 2012
  • December 2011
  • November 2011
  • April 2011
  • March 2011
Blog at WordPress.com.
%d bloggers like this: