This tutorial makes the following assumptions:
-
Create an EF Core Provider that is built on top of an existing ADO.NET Provider.
-
The backed database is relational.
-
You are using EF Core 6.x
-
Your provider needs to do only CRUD operations and have support for EnsureCreated() and EnsureDeleted(). (i.e. no Migrations nor Scaffolding support yet)
In this tutorial, we will look at what it takes to create a basic EF Core Provider that merely can handle all the operations demostrated in the Getting Started with EF Core. By the end of this tutorial, your provider will have unit tests to exerise each of these operations.
In this example provider, we will use the SQLite ADO.NET Provider as the basis to build upon. This ADO.NET Provider has an in-memory database ability that will make it easy to setup our unit tests.
To implement a basic custom Entity Framework Core Data Provider, you'll need to create and configure the following main classes and components:
-
MyCustomTypeMappingSource
: A custom implementation of RelationalTypeMappingSource that maps CLR types to the corresponding SQLite data types and vice versa. -
MyCustomConnection
: A custom implementation ofRelationalConnection
that creates and returns instances ofSqliteConnection
, which are used to connect to the SQLite database. -
DbContext extension method: An extension method for DbContextOptionsBuilder that allows users to configure the custom SQLite provider when setting up their
DbContext
. -
MyCustomOptionsExtension
: A custom implementation of RelationalOptionsExtension that configures the provider with the necessary services and settings specific to SQLite. This class is responsible for registering custom services likeMyCustomConnectionFactory
andMyCustomTypeMappingSource
.
There will also be 2 extension classes that are used to wire-up the dependency injection required. They are:
-
MyCustomDbContextOptionsExtensions
: A static class that contains extension methods for configuring a custom EF Core data provider within aDbContext
. These extension methods provide an easy-to-use API for configuring the custom provider and its options. The primary purpose of the class is to define aUseMyCustom
(which you will rename) extension method. This method accepts a connection string and optional configuration options as parameters. It is used to configure the custom provider for aDbContext
by adding an instance of theMyCustomOptionsExtension
to theDbContextOptionsBuilder
. -
MyCustomServiceCollectionExtensions
: A static class that contains extension methods for registering the services required by the custom EF Core data provider with the dependency injection container. These extension methods extend theIServiceCollection
interface, making it easy for developers to configure the custom provider's services within the application's startup code or other initialization logic.The primary purpose of the class is to define a
AddEntityFrameworkMyCustom
(which you will rename) extension method. This method accepts anIServiceCollection
instance and registers the necessary services for the custom SQLite provider, such asIDatabaseProvider
,IRelationalTypeMappingSource
,IRelationalConnectionFactory
, andIRelationalConnection
.
This section provides sub-sections that walk you through what is required for a basic EF Core Provider. By the end of each sub-section, you should be able to compile your provider. Successful complitation (and passing unit tests, in the last sub-section), helps serve as checkpoints to indicate that you are properly implementing your provider. The result of completing each sub-section is represented in branches of the this repo.
As a starting point, we’ll create our project, add references to the libraries that we need, get a few classes (that are needed in later steps) in place:
-
Create the C# class library for your custom provider.
-
Add the necessary NuGet packages for EF Core and your ADO.NET Provider:
-
Microsoft.EntityFrameworkCore
-
Microsoft.EntityFrameworkCore.Relational
-
<Your ADO.NET Provider>
in this example,MySql.Data.EntityFrameworkCore
-
-
Implement MyCustomTypeMappingSource and define all CLR to database types.
-
The Framework also needs to be able to create and manage instances of the underlying ADO.NET database connection, such as in our case,
SqliteConnection
for SQLite. Therefore, we must implement IMyCustomRelationalConnection and MyCustomRelationalConnection that merely constructs an instance of your ADO.NET Connection class by providing it with the current connection string that was dependency injected into it by the RelationalConnectionDependencies instance.
At this point, you should be able to build your provider. Nothing is wired up and many classes are still missing, so you will not yet be able to do anything with it.
As mentioned before, these classes are necessary for registering the services required by the custom EF Core data provider. After completing this sub-section, you should be able to create your own DbContext
to make use of your provider.
-
The EF Core Framework, expects that your provider has registered a LoggingDefinitions service. Unfortunately,
LoggingDefinitions
and even RelationalLoggingDefinitions are abstract. Therefore, we need to implement MyCustomLoggingDefinitions. -
For updates, EF Core needs to be able to create batch instances. There is no default implementation, so we need to create our own. Implement MyCustomModificationCommandBatchFactory. The factory creates instances of
SingularModificationCommandBatch
. That class creates a single command batch for each operation, effectively disabling command batching. If you need batching, then implement your own custom class that inheritsModificationCommandBatch
and create an instance of it in the factory’s Create() method. -
Although, it isn’t absolutely necessary, the default convention for your EF Core provider to automatically determine on a model if a column a primary key and auto-incrementing if its name is ‘Id’ or ends with ‘Id’ wasn’t working until our provider derived a class off of
RelationalConventionSetBuilder
and registeredIProviderConventionSetBuilder
. This derived class is called MyCustomConventionSetBuilder.
So far, all the classes implemented, have been fairly boiler plate, only using a few of the major ADO.NET provider classes. This section will address classes have very specific logic of your database technology that isn’t available through its ADO.NET provider.
-
The EF Core Framework needs to be able to generate SQL for queries that are compatible with the SQLite database system, based on the Entity Framework Core's internal representation of LINQ queries. An example of an issue that was encounter for the SQLite provider, was when a user uses the First() linq method, the EF Core Framework creates SQL with the FETCH command in it. The SQLite database does not have the FETCH command (as well as other commands) and hence we must create our own derived class MyCustomQuerySqlGenerator which inherits QuerySqlGenerator.
-
The
MyCustomQuerySqlGenerator
must be created by a factory and therefore, we must create MyCustomQuerySqlGeneratorFactory to simply inherit IQuerySqlGeneratorFactory and have its Create() method instantiate an instance of ourMyCustomQuerySqlGenerator
class. -
The EF Core Framework needs to be able to generate SQL for update operations, such as INSERT, UPDATE, and DELETE. The
IUpdateSqlGenerator
interface must be implemented to provide this provider-specific SQL commands. Typically, a provider will derive their implementation from theUpdateSqlGenerator
abstract class. For our case, in order to avoid adding a dependency on SQLite EF Core provider, we have just copied the code for this class that is called MyCustomUpdateSqlGenerator. -
The Framework also needs to be able to handle the creation, deletion, and existence check of relational databases. This is done by it offering the interface
IRelationalDatabaseCreator
and abstract classRelationalDatabaseCreator
. Again, a derived class must be created for your provider. In this tutorial, the example implements MyCustomDatabaseCreator.
Now that all the essential classes for CRUD operations in your provider have been implemented, we need to expose them for both the EF Core Framework and consumers to take advantage of. These classes are typical C# static extension classes and follow EF Core’s standard pattern for wiring everything up.
-
Implement MyCustomDbContextOptionsExtensions.
-
Implement MyCustomServiceCollectionExtensions.
-
Finally, we can implement MyCustomOptionsExtension. Add the necessary services and settings that are specific to your ADO.NET Provider.
-
Add our the Getting Started unit tests to exercise our provider. Of course, these unit tests are very minimal and you’ll need to build unit tests that are specialized for your specific provider.