简体   繁体   中英

How to configure one-to-one optional relationship with EntityFramework?

That's a minimal test case of what I have.

public class Project
{
  public int ProjectId { get; set; }
  public string Name { get; set; }
}
public class Claim
{
  [Key]
  public int ClaimId { get; set; }
  public int ProjectId { get; set; }
  public virtual Project Project { get; set; }
  public virtual CommentDiscussion Discussion { get; set; }
}

public class ForumThread
{
  [Key]
  public int ForumThreadId { get; set; }
  public int ProjectId { get; set; }
  public virtual Project Project { get; set; }
  public virtual CommentDiscussion Discussion { get; set; }
}

public class CommentDiscussion
{
  public int CommentDiscussionId { get; set; }
  public int? ClaimId { get; set; }
  public virtual Claim Claim { get; set; }
  public int? ForumThreadId { get; set; }
  public virtual ForumThread ForumThread { get; set; }
  public int ProjectId { get; set; }
  public virtual Project Project { get; set; }
}
  modelBuilder.Entity<Claim>().HasRequired(c => c.CommentDiscussion).WithRequiredDependent(cd => cd.Claim);
  modelBuilder.Entity<ForumThread>().HasRequired(c => c.CommentDiscussion).WithRequiredDependent(cd => cd.ForumThread);

I want configure following relationship: Claim has exactly one CommentDiscussion & ForumThread has exactly one CommentDiscussion . CommentDiscussion can have either Claim or ForumThread .

If create this as ONE migration, everything fine. But if I split into two migrations, and first create everything except CommentDiscussion , migration for adding CommentDiscussion will generate:

  AddForeignKey("dbo.ForumThreads", "ForumThreadId", "dbo.CommentDiscussions", "CommentDiscussionId");
  AddForeignKey("dbo.Claims", "ClaimId", "dbo.CommentDiscussions", "CommentDiscussionId");

That's really wrong. Even I'll fix migration by hand, EF will incorrectly map everything on load.

There are two things you have to take care of. What do you want to store and what is valid. Not all possible values can be valid.

In this case CommentDiscussion must contain a Claim or a ForumThread reference, but must not have both set.

The entities:

public class Claim
{
    public int Id { get; set; }
    public string Name { get; set; }
    public CommentDiscussion CommentDiscussion { get; set; }
}

public class ForumThread
{
    public int Id { get; set; }
    public string Name { get; set; }
    public CommentDiscussion CommentDiscussion { get; set; }
}

public class CommentDiscussion
{
    public int Id { get; set; }
    public int? ClaimId { get; set; }
    public Claim Claim { get; set; }
    public int? ForumThreadId { get; set; }
    public ForumThread ForumThread { get; set; }
}

The configuration

public class ClaimConfiguration : EntityTypeConfiguration<Claim>
{
    public ClaimConfiguration()
    {
        HasKey( e => e.Id );
        Property( e => e.Name )
            .IsRequired( )
            .HasMaxLength( 100 );
    }
}

public class ForumThreadConfiguration : EntityTypeConfiguration<ForumThread>
{
    public ForumThreadConfiguration()
    {
        HasKey( e => e.Id );
        Property( e => e.Name )
            .IsRequired( )
            .HasMaxLength( 100 );
    }
}

public class CommentDiscussionConfiguration : EntityTypeConfiguration<CommentDiscussion>
{
    public CommentDiscussionConfiguration()
    {
        HasKey( e => e.Id );
        HasOptional( e => e.Claim )
            .WithRequired( m => m.CommentDiscussion )
            .Map( cfg => cfg.MapKey( nameof( CommentDiscussion.ClaimId ) ) );
        HasOptional( e => e.ForumThread )
            .WithRequired( m => m.CommentDiscussion )
            .Map( cfg => cfg.MapKey( nameof( CommentDiscussion.ForumThreadId ) ) );
    }
}

And the validation to ensure CommentDiscussion has one reference to either Claim or ForumThread :

public class ModelContext : DbContext
{
    // Der Kontext wurde für die Verwendung einer ModelContext-Verbindungszeichenfolge aus der
    // Konfigurationsdatei ('App.config' oder 'Web.config') der Anwendung konfiguriert. Diese Verbindungszeichenfolge hat standardmäßig die
    // Datenbank 'ConsoleApp7.Model.ModelContext' auf der LocalDb-Instanz als Ziel.
    //
    // Wenn Sie eine andere Datenbank und/oder einen anderen Anbieter als Ziel verwenden möchten, ändern Sie die ModelContext-Zeichenfolge
    // in der Anwendungskonfigurationsdatei.
    public ModelContext()
        : base( "name=ModelContext" )
    {
    }

    protected override void OnModelCreating( DbModelBuilder modelBuilder )
    {
        modelBuilder.Configurations.Add( new ClaimConfiguration( ) );
        modelBuilder.Configurations.Add( new ForumThreadConfiguration( ) );
        modelBuilder.Configurations.Add( new CommentDiscussionConfiguration( ) );

        base.OnModelCreating( modelBuilder );
    }

    protected override bool ShouldValidateEntity( DbEntityEntry entityEntry )
    {
        return base.ShouldValidateEntity( entityEntry );
    }

    protected override DbEntityValidationResult ValidateEntity( DbEntityEntry entityEntry, IDictionary<object, object> items )
    {
        if ( entityEntry.Entity is CommentDiscussion )
        {
            if ( !entityEntry.CurrentValues.GetValue<int?>( nameof( CommentDiscussion.ClaimId ) ).HasValue && !entityEntry.CurrentValues.GetValue<int?>( nameof( CommentDiscussion.ForumThreadId ) ).HasValue )
            {
                var list = new List<System.Data.Entity.Validation.DbValidationError>( );
                list.Add( new System.Data.Entity.Validation.DbValidationError( nameof( CommentDiscussion.Claim ), "Claim or ForumThread is required" ) );
                list.Add( new System.Data.Entity.Validation.DbValidationError( nameof( CommentDiscussion.ForumThread ), "Claim or ForumThread is required" ) );

                return new System.Data.Entity.Validation.DbEntityValidationResult( entityEntry, list );
            }

            if ( entityEntry.CurrentValues.GetValue<int?>( nameof( CommentDiscussion.ClaimId ) ).HasValue && entityEntry.CurrentValues.GetValue<int?>( nameof( CommentDiscussion.ForumThreadId ) ).HasValue )
            {
                var list = new List<System.Data.Entity.Validation.DbValidationError>( );
                list.Add( new System.Data.Entity.Validation.DbValidationError( nameof( CommentDiscussion.Claim ), "Only Claim or ForumThread is possible, not both" ) );
                list.Add( new System.Data.Entity.Validation.DbValidationError( nameof( CommentDiscussion.ForumThread ), "Only Claim or ForumThread is possible, not both" ) );

                return new System.Data.Entity.Validation.DbEntityValidationResult( entityEntry, list );
            }
        }
        return base.ValidateEntity( entityEntry, items );
    }
}

Check

using ( var context = new ModelContext() )
{

    CommentDiscussion discussion = new CommentDiscussion( );
    context.Set<CommentDiscussion>( ).Add( discussion );
    try
    {
        context.SaveChanges( );
    }
    catch ( Exception ex )
    {
        Console.WriteLine( ex.ToString() );
    }

    Claim claim = new Claim( );
    ForumThread forumThread = new ForumThread( );

    claim.CommentDiscussion = discussion;
    forumThread.CommentDiscussion = discussion;

    try
    {
        context.SaveChanges( );
    }
    catch ( Exception ex )
    {
        Console.WriteLine( ex.ToString( ) );
    }
}

Update

Here the generated migration for the model

public partial class InitialCreate : DbMigration
{
    public override void Up()
    {
        CreateTable(
            "dbo.Claims",
            c => new
                {
                    Id = c.Int(nullable: false, identity: true),
                    Name = c.String(nullable: false, maxLength: 100),
                    ClaimId = c.Int(nullable: false),
                })
            .PrimaryKey(t => t.Id)
            .ForeignKey("dbo.CommentDiscussions", t => t.ClaimId)
            .Index(t => t.ClaimId);

        CreateTable(
            "dbo.CommentDiscussions",
            c => new
                {
                    Id = c.Int(nullable: false, identity: true),
                    ClaimId = c.Int(),
                    ForumThreadId = c.Int(),
                })
            .PrimaryKey(t => t.Id);

        CreateTable(
            "dbo.ForumThreads",
            c => new
                {
                    Id = c.Int(nullable: false, identity: true),
                    Name = c.String(nullable: false, maxLength: 100),
                    ForumThreadId = c.Int(nullable: false),
                })
            .PrimaryKey(t => t.Id)
            .ForeignKey("dbo.CommentDiscussions", t => t.ForumThreadId)
            .Index(t => t.ForumThreadId);

    }

    public override void Down()
    {
        DropForeignKey("dbo.ForumThreads", "ForumThreadId", "dbo.CommentDiscussions");
        DropForeignKey("dbo.Claims", "ClaimId", "dbo.CommentDiscussions");
        DropIndex("dbo.ForumThreads", new[] { "ForumThreadId" });
        DropIndex("dbo.Claims", new[] { "ClaimId" });
        DropTable("dbo.ForumThreads");
        DropTable("dbo.CommentDiscussions");
        DropTable("dbo.Claims");
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM