Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to separate entity's primary key from base class? #27421

Closed
ibocon opened this issue Feb 10, 2022 · 3 comments
Closed

How to separate entity's primary key from base class? #27421

ibocon opened this issue Feb 10, 2022 · 3 comments
Labels
closed-no-further-action The issue is closed and no further action is planned. customer-reported

Comments

@ibocon
Copy link

ibocon commented Feb 10, 2022

Ask a question

I want to accomplish clean architecture with EF Core. I trid to separate primary key from base class, but it failed with below exception.

Include your code

Core

public class Manager
{
	public Manager(Guid identifier, string email)
	{
		Identifier = identifier;
		Email = email;
	}

	public Guid Identifier { get; }
	public string Email { get; }

	public void FixPrinter(Printer printer)
	{
	    printer.IsOutOfControl = true;
	}
}
public class Printer
{
	public Printer(Guid token)
	{
		Token = token;
		Manager = null;
		IsOutOfControl = false;
	}

	public Guid Token { get; }

	public Manager? Manager { get; set; }

	public bool IsOutOfControl { get; set; }
}

Infrastructure

public class ApplicationContext
	: DbContext
{
    // ...

	public DbSet<ManagerEntity> ManagerSet { get; set; }
	public DbSet<PrinterEntity> PrinterSet { get; set; }

	protected override void OnModelCreating(ModelBuilder modelBuilder)
	{
		base.OnModelCreating(modelBuilder);
		modelBuilder.ApplyConfiguration(new ManagerEntityConfiguration(Database));
		modelBuilder.ApplyConfiguration(new PrinterEntityConfiguration(Database));
	}
}
Configure Manager
public sealed class ManagerEntity
	: Manager
{
	public ManagerEntity(string email)
		: base(Guid.Empty, email)
	{
	}

    // Primary key for database.
	public long Id { get; }
}
internal sealed class ManagerEntityConfiguration
	: IEntityTypeConfiguration<ManagerEntity>
{
	private readonly DatabaseFacade _database;

	public ManagerEntityConfiguration(DatabaseFacade database)
	{
		_database = database;
	}

	public void Configure(EntityTypeBuilder<ManagerEntity> builder)
	{
		builder
			.Property(e => e.Id)
			.ValueGeneratedOnAdd();

        // The exception occurs here.
		builder
			.HasKey(e => e.Id);

        // ...
	}
}
Configure Printer
public sealed class PrinterEntity
	: Printer
{
	public PrinterEntity()
		: base(Guid.Empty)
	{
	}

    // Primary key for database.
	public long Id { get; }
}
internal sealed class PrinterEntityConfiguration
	: IEntityTypeConfiguration<PrinterEntity>
{
	private readonly DatabaseFacade _database;

	public PrinterEntityConfiguration(DatabaseFacade database)
	{
		_database = database;
	}

	public void Configure(EntityTypeBuilder<PrinterEntity> builder)
	{
		builder
			.Property(e => e.Id)
			.ValueGeneratedOnAdd();

		builder
			.HasKey(e => e.Id);

        // ...
	}
}

Web API

app.MapPost("/printer", async (ApplicationContext context) =>
{
	PrinterEntity printer = new()
	{
		Manager = new ManagerEntity("master@google.com"),
	};

	await context.PrinterSet.AddAsync(printer);
	await context.SaveChangesAsync();

	return printer;
});

Include stack traces

System.InvalidOperationException: 'A key cannot be configured on 'ManagerEntity' because it is a derived type. The key must be configured on the root type 'Manager'. If you did not intend for 'Manager' to be included in the model, ensure that it is not referenced by a DbSet property on your context, referenced in a configuration call to ModelBuilder, or referenced from a navigation on a type that is included in the model.'

Include verbose output

Include provider and version information

EF Core version: 6.0.1
Database provider: Microsoft.EntityFrameworkCore.InMemory
Target framework: .NET 6
Operating system: Windows 10 (19044.1466)
IDE: Visual Studio 2022 17.0.4

Extra

Should I architect it by using interface, not inheritance?

At Core,

public interface IPrinter
{
    public Manager? Manager { get; set; }
}

At Infrastructure,

public sealed class PrinterEntity : IPrinter
{
    // ...
}

Github source code

@roji
Copy link
Member

roji commented Feb 10, 2022

This is likely happening because you're mapping both Manager and ManagerEntity in your model, which means you're configuring inheritance mapping (i.e. EF thinks you intend to store both Manager and ManagerEntity instances in the database). With inheritance mapping, the key must be specified at the root.

However, it seems like you only want the class separation on the .NET side, without needing any actual hierarchy, so make sure you are not mapping the base class (Manager in the above). See the minimal code sample below.

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

public class BlogContext : DbContext
{
    // Uncomment the below to make the exception appear
    // public DbSet<Manager> Managers { get; set; }
    public DbSet<ManagerEntity> ManagerEntities { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder
            .UseSqlServer(@"Server=localhost;Database=test;User=SA;Password=Abcd5678;Connect Timeout=60;ConnectRetryCount=0")
            .LogTo(Console.WriteLine, LogLevel.Information)
            .EnableSensitiveDataLogging();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<ManagerEntity>().HasKey(b => b.Id);
    }
}

public class Manager
{
    public string Name { get; set; }
}

public class ManagerEntity : Manager
{
    public int Id { get; set; }
}

However... if the goal is simply not to expose an Id property on Manager, there are simpler ways to do that rather than introducing a .NET hierarchy. You can have a private _id field instead, which would be used by EF Core but not otherwise exposed in your application, keeping your data model clean (see docs). Alternatively, you can have an Id shadow property, removing the field/property from your CLR type altogether.

@ibocon
Copy link
Author

ibocon commented Feb 11, 2022

Now I understand EFCore mapping convention, which seems to me a magic, and I see what should I do next and other options that I can think about.

Thanks, @roji.

@ibocon ibocon closed this as completed Feb 11, 2022
@roji roji added the closed-no-further-action The issue is closed and no further action is planned. label Feb 11, 2022
@AndriySvyryd
Copy link
Member

To make sure that the base type is not picked up by EF Core call modelBuilder.Ignore<Manager>()

@ajcvickers ajcvickers reopened this Oct 16, 2022
@ajcvickers ajcvickers closed this as not planned Won't fix, can't repro, duplicate, stale Oct 16, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
closed-no-further-action The issue is closed and no further action is planned. customer-reported
Projects
None yet
Development

No branches or pull requests

4 participants