EF Core in progress

The Entity Framework Core provider lives in its own repository, dimzon/h2sharp-efcore — it is a fork, and its own history and release cycle are separate from the engine's.

What it is: a fork of EFCore.PG (Npgsql's provider for PostgreSQL), with PostgreSQL's surface replaced by H2's — the type mappings, the migrations SQL generator, the connection and the DDL are H2's. PostgreSQL-only features went with it: ranges, multiranges, network address types, full-text search, PostGIS. The point of forking rather than writing a provider is to keep EF Core's own specification suite as the measure: the tests are inherited from EF Core, not written for H2.

Measurement

Run against the pinned reference engine (h2-2.5.259-SNAPSHOT.jar), not against the port — a test against the port would only prove the port agrees with itself. The suite is split by trait, and both runs are needed:

RunPassedFailedTotal
dotnet test --filter "Category!=BlockedByPort" — the provider is correct as far as the port allows103401034
dotnet test --filter "Category=BlockedByPort" — the port defects the provider has signed up for19185206
Total10531851240

A test that fails because a function was not ported stays in the suite, tagged BlockedByPort: removing it would take away the only signal that the port has not yet caught up with H2.

The rule of the registry: the provider does not bend to the engine's current gaps. It generates what H2 2.5 requires and records the divergence — an accommodation would be a temporary fix that pins a temporary limitation into observable behaviour.

Using it

await using var ctx = new BlogContext();
await ctx.Database.EnsureDeletedAsync();
await ctx.Database.EnsureCreatedAsync();

ctx.Blogs.Add(new() { Name = "FooBlog" });
await ctx.SaveChangesAsync();

var fBlogs = await ctx.Blogs.Where(b => b.Name.StartsWith("F")).ToListAsync();

public class BlogContext : DbContext
{
    public DbSet<Blog> Blogs { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder.UseH2("Data Source=mem:mydatabase");
}

The connection string is the ADO.NET spelling: Data Source=mem:mydatabase is translated to jdbc:h2:mem:mydatabase, and a JDBC URL is accepted as it is.

H2's own functions

EF.Functions reaches the database's built-ins — the date/time, string, binary, number and bit ones — each checked against the engine:

EF.Functions.DateAdd("DAY", 1, blog.Created)   // DATEADD('DAY', 1, ...)
EF.Functions.Locate(blog.Name, "foo")           // LOCATE(...) — H2 takes the substring first
EF.Functions.Extract("YEAR", blog.Created)      // YEAR(...)
EF.Functions.LeftPad(blog.Name, 10, "0")        // LPAD(...)

Engine gaps the provider is waiting on

Numbered in the provider's own docs/DIVERGENCES.md, where each one says who has to fix it:

#GapSymptom
1Quoted column referenceSELECT "e"."Id" FROM T → SessionLocal.getResolver: the query layer is not ported
2Table alias through ASthe implicit alias (FROM T e) works; FROM T AS "e" is what the provider needs
3GENERATED … AS IDENTITYthe parser rejects GENERATED; the object model already knows identity columns
4FOREIGN KEY in CREATE TABLEconstraints — ROADMAP phase 7
5INTERVAL typesthe parser does not know them; without the qualifier INTERVAL is not a type
6DB_CLOSE_DELAY for an in-memory databasethe database disappears with the last connection, so EnsureCreated cannot work
7INFORMATION_SCHEMAthe catalogue — without it scaffolding and the schema registry are impossible
8CREATE SEQUENCE, NEXT VALUE FORthe parser does not know them; needed for key generation if identity does not arrive

Also in that repository