Testing concurrency tokens with Microsoft.EntityFrameworkCore.InMemory
Asked Answered
G

4

5

As part of my EF 6.1 to EF Core 2.0 migration I added a simple test to check if the concurrency tokens do work the same way. I noted, however, that is dependent on the underlying database provider: it works for SqlServer, but it does not for MS InMemory database.

The entity class is pretty simple:

public class AcademicTermDate
{
    public int AcademicTermDateID { get; set; }

    public DateTime StartDate { get; set; } //but no end date, because it's derived in controcc and rederived here.

    public bool Deleted { get; set; }

    [Timestamp]
    public byte[] RowVersion { get; set; }
}

The code that creates it is also trivial:

        using (var context = _factory.CreateDbContext(null))
        {
            var term = new AcademicTermDate();
            term.StartDate = new DateTime(2001, month, 1);
            context.AcademicTermDate.Add(term);

            context.SaveChanges();
        }

What is interesting, if I use old plain Sql Server as per following code:

    public MyContext CreateDbContext(string[] args)
    {
        var builder = new DbContextOptionsBuilder<MyContext>();

        var connectionString = "server=.\\sql2012;Database=CA15;Trusted_Connection=True;";
        builder.UseSqlServer(connectionString);

        return new MyContext(builder.Options);
    }

it works as expected; on context.SaveChanges() I can see RowVersion to be populated.

If, however, I use the InMemory database provider, which seemed so tempting to be used for my tests, I can see a different behaviour: RowVersion remains populated with null value (i.e. not initialised at all).

For the latter, the factory is defined as:

    public MyContext CreateDbContext(string[] args)
    {
        var builder = new DbContextOptionsBuilder<MyContext>();

        builder.UseInMemoryDatabase(databaseName: "InMemory");

        return new MyContext(builder.Options);
    }

Am I missing any vital setting for InMemory db I should provide? The difference seem odd and, honestly, quite disturbing.

All code targets .NET Core 2.0:

<PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp2.0</TargetFramework>
</PropertyGroup>

<ItemGroup>
    <PackageReference Include="System.ComponentModel.Annotations" Version="4.4.0" />
    <PackageReference Include="Microsoft.EntityFrameworkCore" Version="2.0.0" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="2.0.0" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="2.0.0" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="2.0.0" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="2.0.0" />
</ItemGroup>

Any help greatly appreciated.

Gabbi answered 29/10, 2017 at 6:30 Comment(9)
Have you tried fluent api to set concurrency token ?Baucom
Yes, I did: modelBuilder.Entity<AcademicTermDate>() .Property(p => p.RowVersion) .ValueGeneratedOnAddOrUpdate() .IsConcurrencyToken() ;Propagation
OK, the previous code was probably not right, but this one does not work neither: modelBuilder.Entity<AcademicTermDate>() .Property(p => p.RowVersion) .ValueGeneratedOnAddOrUpdate() .IsRowVersion() ;Propagation
What is annoying me is the fact the original code works well for Sql Server, but not for InMemory db. So my guess this issue is about db provider in first place.Propagation
Remember in memory database it's a different database from database on sql server instance, have you checked rows from in memory database?Baucom
Not sure what do you mean by that, can you provide some details please?Propagation
Useful info from a now deleted answer from the EF team: There is a PR out that implements this: #10158. It will most likely make it into the 2.1.0 release of EF Core. Once it's merged, you can test it out using the nightly builds.Unctuous
That is really a great news, thanks!Propagation
i added rowversion+timestamp support to in memory extensions github.com/SimonCropp/EfCore.InMemoryHelpersDithyrambic
U
3

The docs on testing with InMemory do a serious attempt at expectation management. For example:

(InMemory) is not designed to mimic a relational database.

Which, among other things, means

  • InMemory will allow you to save data that would violate referential integrity constraints in a relational database.

  • If you use DefaultValueSql(string) for a property in your model, this is a relational database API and will have no effect when running against InMemory.

No doubt, initializing and updating RowVersion column values can be added to this list.

Then they give the tip:

For many test purposes these difference will not matter. However, if you want to test against something that behaves more like a true relational database, then consider using SQLite in-memory mode.

For what it's worth, I agree with the first part which amounts to: test things where you know the differences don't matter. It may be convenient to use InMemory where you want to use the database layer merely as a quick supplier of mock data that you subsequently use in business logic unit tests.

But I wholeheartedly disagree with the second advice to use SQLite to test functions that depend more on correct data layer behavior. Well, go ahead if SQLite is the production database. Otherwise: always do integration tests against the same database brand as the production database. There are too many differences between database brands and query providers (the part that translates expressions into SQL) to make integration test reliable enough. And what if SQLite doesn't support LINQ constructs or statements or features that your own database brand/query provider does? Avoid them to please SQLite? I don't think so.

So my advice is to set up a Sql Server integration test database to test RowVersion-related code.

Unctuous answered 30/10, 2017 at 13:43 Comment(2)
Great answer, thanks. It can be my assumptions were wrong as I expected InMemory to behave as any other RDBMS; it looks like the differences are more fundamental. The lack of list of implemented/missing features would really be helpful, but I was unlucky to find any. SQLite is much better on that: learn.microsoft.com/en-us/ef/core/providers/sqlite/limitations. I can see your point suggesting this might be considered as an integration test; from my perspective, however (I'm more db-oriented), timestamp not being maintained properly is a bug, not a feature.Propagation
I don't think the EF team shares your view. A bug is something that doesn't run as claimed (not as expected). I'm pretty sure they'll never claim that RowVersions will be supported in InMemory. (Don't tell anyone, but I don't see much practical use for InMemory, because wherever my DAL is involved in automated tests I want to be 100% sure it behaves exactly as the real thing. I take for granted that integration tests take much time, I prefer correctness over speed).Unctuous
Z
2

I know this question was asked some time ago. But I just faced with the same issue.

The best solution I found without affecting the context that will be used during runtime of the application is to add a SaveChangesInterceptor within Unit Tests and update any concurrency token if a tracked entity is added or modified within that interceptor.

My requirement was purely to set the RowVersion in InMemory Dbcontext so that the tests weren't affected by it.

You can check how to create SaveChangesInterceptor here

public override InterceptionResult<int> SavingChanges(DbContextEventData eventData, InterceptionResult<int> result)
{
    var dbContext = eventData.Context;
    foreach (var entry in dbContext.ChangeTracker.Entries().Where(e => e.State == EntityState.Added || e.State == EntityState.Modified))
    {
        var concurrencyTokens = entry.Properties.Where(p => p.Metadata.IsConcurrencyToken);

        foreach (var token in concurrencyTokens)
        {
            token.CurrentValue = new byte[] { 1 };
        }
    }
    return base.SavingChanges(eventData, result);
}

Within AddDbContext you can add the interceptor. Just make sure it's added to AddDbContext within unit tests and not the one in your application.

options.AddInterceptors(new TestDbContextSaveChangesInterceptor());
Zincography answered 21/8 at 10:6 Comment(1)
THIS IS THE BEST ANSWER!!!Cardiomegaly
K
1

In memory DB does not generate timestamps. However, entity framework still does validate that the timestamps match for any update!

They do just match all the time because the stay null.

So, you can still test that the DbUpdateConcurrencyException is thrown by setting a row version for an update.

entity.Timestamp = new byte[] {1};
repository.Update(entity);
await context.SaveAsync(); // <-- this will throw a DbUpdateConcurrencyException
Kattie answered 1/3, 2019 at 15:23 Comment(0)
A
0

EF Core 5.0 and lower doesn't support concurrency checks with the in-memory provider, but it is planned. See https://github.com/dotnet/efcore/issues/10625

EfCore.InMemoryHelpers Provides a wrapper around the EF Core In-Memory Database Provider. Specifically works around the following EF bugs.

InMemory: Improve in-memory key generation

Audry answered 1/4, 2021 at 23:38 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.