Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Configure ValueGenerator for autoincrement decimal field in InMemoryProvider

Problem

In my app, I use SQL Server:

services.AddDbContext<MyDbContext>(options => options.UseSqlServer(connectionString));

but when I use InMemoryProvider in my unit tests:

[TestInitialize]
public void Initialize()
{
    Services = new ServiceCollection();
    Services.AddDbContext<MyDbContext>(options => options.UseInMemoryDatabase("MyDbContext"), ServiceLifetime.Transient);
    ServiceProvider = Services.BuildServiceProvider();
}

I'm getting:

System.NotSupportedException: The 'PersonId' on entity type 'Person' does not have a value set and no value generator is available for properties of type 'decimal'. Either set a value for the property before adding the entity or configure a value generator for properties of type 'decimal'.

Question

Where do I configure custom ValueGenerator for PersonId property, so that it will be used only in my test project where I use InMemoryProvider?

What I have

I have MS SQL Server Database and a table with autoincrement identity column of type numeric(10, 0).

CREATE TABLE [dbo].[People](
    [Person ID] [numeric](10, 0) IDENTITY(100000000,1) NOT NULL,
 CONSTRAINT [PK_People] PRIMARY KEY CLUSTERED 

and the EF Code

[Column("Person ID", TypeName = "numeric(10, 0)")]
public decimal PersonId { get; set; }

public class MyDbContext
{
    public OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Person>(entity =>
        {
           entity.Property(e => e.PersonId).ValueGeneratedOnAdd();
        });
    }
}

My application uses SQL Server, but I want to setup EF to use InMemoryProvider in tests. The Custom ValueGenerator should be used only in tests

like image 923
Liero Avatar asked Oct 16 '17 11:10

Liero


1 Answers

As of EFC 2.0, the In-Memory Database Provider supports value generation only for integer types - byte, sbyte, short, ushort, int, uint, long and ulong.

So one way to resolve the issue is to map the PK property to some of these types which fits in the desired range, like int, long etc.

But if you insist using decimal, then you can use the following fluent setup:

using Microsoft.EntityFrameworkCore.ValueGeneration.Internal;

// ...

modelBuilder.Entity<Person>(entity =>
{
    var pb = entity.Property(e => e.PersonId).ValueGeneratedOnAdd();
    if (Database.IsInMemory())
        pb.HasValueGenerator<InMemoryIntegerValueGenerator<decimal>>();
});

or if you have more properties like this, you could keep the existing configuration and add the following at the end of your OnModelCreating override:

using Microsoft.EntityFrameworkCore.Metadata
using Microsoft.EntityFrameworkCore.ValueGeneration.Internal;

// ...

if (Database.IsInMemory())
{
    var autoGenDecimalProperies = modelBuilder.Model.GetEntityTypes()
        .Select(t => t.FindPrimaryKey())
        .Where(pk => pk != null)
        .SelectMany(pk => pk.Properties)
        .Where(p => p.ClrType == typeof(decimal) && p.ValueGenerated != ValueGenerated.Never);

    foreach (var property in autoGenDecimalProperies)
        property.SetValueGeneratorFactory((p, t) => new InMemoryIntegerValueGenerator<decimal>());
}
like image 119
Ivan Stoev Avatar answered Oct 27 '22 08:10

Ivan Stoev