Environment: .Net Core 3.1
REST API / EntityFrameworkCore.InMemory 3.1.6
/ XUnit 2.4.1
In a Database First Setup I have a model mapped to a Sql View.
During Code Generation (with EF Core PowerTools 2.4.51
) this entity is marked in DbContext
with .HasNoKey()
When I try to test the endpoint accessing the DbSet
mapped to the Sql View it throws exception:
Unable to track an instance of type '*' because it does not have a primary key. Only entity types with primary keys may be tracked.
Follows some code snippets with highlights of what I have tries so far.
Auto generated DbContext: ViewDemoAccountInfo
is the entity mapped to a Sql View. Other entities are mapped to regular Sql Tables
// <auto-generated> This file has been auto generated by EF Core Power Tools. </auto-generated>
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;
namespace Demo.Data.Entities
{
public partial class DemoDbContext : DbContext
{
public DemoDbContext(){}
public DemoDbContext(DbContextOptions<DemoDbContext> options): base(options){}
public virtual DbSet<ViewDemoAccountInfo> ViewDemoAccountInfo { get; set; }
// multiple other entities
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<ViewDemoAccountInfo>(entity =>
{
entity.HasNoKey();
entity.ToView("ViewDemoAccountInfo");
entity.Property(e => e.AccountType).IsUnicode(false);
});
OnModelCreatingPartial(modelBuilder);
}
partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
}
}
Attempt #1:
The test
public class MyIntegrationTests : BaseIntegrationTest {
// throws "Unable to track an instance of type 'ViewDemoAccountInfo'
// because it does not have a primary key. Only entity types with primary keys may be tracked."
[Fact]
public async Task SetupData_WhenKeylessEntity_ThenShouldNotThrow() {
using (var scope = _testServer.Host.Services.CreateScope()) {
var dbContext = scope.ServiceProvider.GetService<DemoDbContext>();
await dbContext.ViewDemoAccountInfo.AddAsync(MockedAccountInfo); // setup some data
await dbContext.SaveChangesAsync();
}
var endpointUrl = $"{ControllerRootUrl}/account-info";
var response = await _testClient.GetAsync(endpointUrl);
// Assertions
}
}
Helpers
public class BaseIntegrationTest {
protected readonly HttpClient _testClient;
protected readonly TestServer _testServer;
public BaseIntegrationTest() {
var builder = new WebHostBuilder()
.UseEnvironment("Test")
.ConfigureAppConfiguration((builderContext, config) => {
config.ConfigureSettings(builderContext.HostingEnvironment);
});
builder.ConfigureServices(services => {
services.ConfigureInMemoryDatabases(new InMemoryDatabaseRoot());
});
builder.UseStartup<Startup>();
_testServer = new TestServer(builder);
_testClient = _testServer.CreateClient();
}
}
// Regular DbContext
internal static class IntegrationExtensions {
public static void ConfigureInMemoryDatabases(this IServiceCollection services, InMemoryDatabaseRoot memoryDatabaseRoot) {
services.AddDbContext<DemoDbContext>(options =>
options.UseInMemoryDatabase("DemoApp", memoryDatabaseRoot)
.EnableServiceProviderCaching(false));
}
}
The simplest solution is to change the Auto generated DbContext
and remove the .HasNoKey()
config, but it would be removed each time the schema structure will be generated with EF Core PowerTools
.
Search for other solutions which would not require changes in Auto generated files
Found: how to test keyless entity - github discussion planned for EF Core 5 and stackoverflow source
Attemp #2 - Try to create another DbContext
and override the entity setup by adding explicitly a key when Database.IsInMemory
public class TestingDemoDbContext : DemoDbContext {
public TestingDemoDbContext(){}
public TestingDemoDbContext(DbContextOptions<DemoDbContext> options): base(options){}
protected override void OnModelCreating(ModelBuilder modelBuilder) {
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<ViewDemoAccountInfo>(entity => {
if (Database.IsInMemory()) {
entity.HasKey(e => new { e.AccountType, e.AccountStartDate });
}
});
}
}
In BaseIntegrationTest
use the "extended" TestingDemoDbContext
in ConfigureInMemoryDatabases
method.
internal static class IntegrationExtensions {
public static void ConfigureInMemoryDatabases(this IServiceCollection services, InMemoryDatabaseRoot memoryDatabaseRoot) {
services.AddDbContext<TestingDemoDbContext>(options =>
options.UseInMemoryDatabase("DemoApp", memoryDatabaseRoot)
.EnableServiceProviderCaching(false));
}
}
The test is similar, with a small difference: var dbContext = scope.ServiceProvider.GetService<TestingDemoDbContext>();
Result - strange, but it throws The string argument 'connectionString' cannot be empty.
- even I do use the InMemoryDatabase
Attemp #3 - Try to use the OnModelCreatingPartial
method to add a Key for that keyless entity.
So, in the same namespace with the Regular DbContext
, create the partial DbContext
meant to enrich the existing config
namespace Demo.Data.Entities {
public partial class DemoDbContext : DbContext {
partial void OnModelCreatingPartial(ModelBuilder builder) {
builder.Entity<ViewDemoAccountInfo>(entity => {
// try to set a key when Database.IsInMemory()
entity.HasKey(e => new { e.AccountType, e.AccountStartDate }));
});
}
}
}
Result - The key { e.AccountType, e.AccountStartDate } cannot be added to keyless type 'ViewDemoAccountInfo'.
Any hints on how to add some mock data for Keyless entities (mapped to Sql View), with InMemoryDatabase, for testing purpose (with XUnit
) would be grateful appreciated.
As well, if something is wrong or is considered bad practice in the setup I have listed here - would appreciate to receive improvement suggestions.