Skip to content

Limitations

InfoCarrier.Core runs Microsoft's own Entity Framework Core specification suite, the same suite the SQL Server, SQLite and InMemory providers run. This page lists every scenario in that suite which does not behave the way a normal EF Core provider behaves, so you can judge whether any of them affects your application.

It is complete for what a caller can observe as a limitation. The suite's other differences ask the client for something only a database has, assert a refusal this provider does not need to make, or are EF Core defects that every provider hits and this one reports with a different exception type.

Total tests: 29958, Failed: 0

Measured against 10.2.0. No test fails. Where this provider answers differently from EF Core, the test that covers it says so, and this page names the differences you can observe. The skipped tests are EF Core's own: EF skips them itself for the store behind them, and none is a suppression added here.

Not supported

Inserting an entity whose property bag holds a list

Affects you if you map a complex property, or a complex collection, whose CLR type is Dictionary<string, object>, and one of its members is a list. EF Core calls this type a property bag: the shape is declared in the model rather than in the CLR type.

public class Product
{
    public int Id { get; set; }
    public string Sku { get; set; } = "";

    // A property bag: no CLR properties, the members come from the model below.
    public Dictionary<string, object> Spec { get; set; } = new();
}

modelBuilder.Entity<Product>()
    .ComplexProperty(e => e.Spec, "Spec", b =>
    {
        b.Property<string>("Material");
        b.Property<List<string>>("Finishes");
    });

The insert throws ArgumentException:

context.Products.Add(new Product
{
    Sku = "BOLT-M6",
    Spec = { ["Material"] = "steel", ["Finishes"] = new List<string> { "zinc" } },
});

await context.SaveChangesAsync();   // throws

The same applies to the collection form, List<Dictionary<string, object>> mapped with ComplexCollection. A property bag with no list member works: you can insert, query and update it.

Reading an entity whose property bag holds a list throws the same exception with plain EF Core too, because the cause is a defect in EF Core's own materializer.

The workaround is to declare the complex type as an ordinary class:

public class ProductSpec
{
    public string Material { get; set; } = "";
    public List<string> Finishes { get; set; } = [];
}

modelBuilder.Entity<Product>().ComplexProperty(e => e.Spec);

Suppressing a concurrency exception in an interceptor on the client

Affects you if an ISaveChangesInterceptor registered on the client returns InterceptionResult.Suppress() from ThrowingConcurrencyException or ThrowingConcurrencyExceptionAsync.

With EF Core, a suppression skips the conflicting row and the rest of the save goes ahead. Here the server finds the conflict and rolls the save back before the client's interceptor runs. SaveChanges returns 0 and does not throw. Nothing in that save is written, and the change tracker marks every change in it as saved.

Register the interceptor on the server instead. The server runs EF Core's own save, so a suppression there behaves as it does with EF Core: the other changes are written, and the client's SaveChanges returns the count EF Core would.

// On the server
builder.Services.AddDbContext<ShopContext>(o => o
    .UseSqlServer(connectionString)
    .AddInterceptors(new IgnoreConcurrencyConflicts()));

An interceptor on the client still sees the exception, so it can log it and let it throw.

Differences that are not limitations

These behave correctly, and differ from another EF Core provider only in ways you would notice when porting code or tests.

Exception message text for an untranslatable query

When a query cannot be translated, this provider throws InvalidOperationException, exactly as EF Core does. The message text may differ. Two examples:

// (a) a method call where ExecuteUpdate expects a property
context.Orders
    .Where(o => o.Total > 100m)
    .ExecuteUpdate(s => s.SetProperty(o => Math.Round(o.Total), 0m));

// (b) a cast to a type no mapped entity implements
IQueryable orders = context.Orders;
orders.Cast<IArchivable>().FirstOrDefault();

Both throw. Catch the exception type and do not match on message text, which is unsupported on any EF Core provider.

Queries this provider answers that other providers do not

One scenario in EF's suite expects the provider to reject the query, and this provider answers it. Composing LINQ over a collection stored through a value converter:

modelBuilder.Entity<Dashboard>()
    .Property(e => e.Layouts)
    .HasConversion(                       // List<Layout> stored as a single string column
        v => Serialize(v),
        v => Deserialize(v),
        layoutComparer);

context.Dashboards
    .Select(d => new { d.Name, Heights = d.Layouts.Select(l => l.Height).ToList() })
    .ToList();
// EF Core providers: throws.   This provider: reads the column and answers.

The inner lambda names Layout, which the model does not imply, so this client keeps that part of the query and the server sends the whole column. Name Layout on both halves, as Keys of your own types describes for a GroupBy key, and the query reaches the server, which rejects it as every other provider does.

A test suite you port will expect the exception, and LINQ that relies on the answer will not run unchanged elsewhere.

Consequences of the client having no database

These are not defects. They follow from where the client sits.

Relational-only APIs, such as ExecuteSqlRaw, GetDbTransaction and migrations, are not part of this provider's surface. Calling one throws. FromSql runs only where the server has granted it, and that grant is arbitrary SQL Querying
Automatic lazy loading does not work in Blazor WebAssembly Blazor WebAssembly
The client never sees the server's provider, so it assumes a relational store and refuses three queries relational providers refuse. UseInfoCarrier(client, o => o.UseNonRelationalServerStore()) says otherwise Querying
The server's provider writes the SQL, so how a query is translated is settled there, not on the client. EF.Constant and EF.Parameter are part of your query, so they cross the wire and the server honours them
EF.CompileQuery changes nothing about the wire. The server answers a compiled query once per execution, as it answers any other query Querying
A query result arrives in one response rather than as a stream, so a very large result set is a very large response. Page it.
Authentication and authorization are yours Security
Native AOT is not supported: remoting a query means compiling an expression tree at runtime. Trimming is a separate question, and it works. Blazor WebAssembly

What this page cannot tell you

Every entry above corresponds to tests in EF Core's specification suite that run on every build. A failing test breaks the build, so a new entry cannot appear unnoticed. When an entry is fixed, or a new one appears, this page changes with it.

What the suite measures bounds what this page can promise. A conformance suite says nothing about performance, payload size or concurrency under load. A scenario it never exercises is outside what this page claims at the top.