32

See the following post for some background:

Entity framework one to zero or one relationship without navigation property

I had always thought that ForeignKey was used to show which property in a class held the ForeignKey that determined the navigation property e.g.

public class MemberDataSet
{
    [Key]
    [DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)]
    public int Id { get; set; }

    public int? DeferredDataId { get; set; }
    [ForeignKey("DeferredDataId")]
    public virtual DeferredData DeferredData { get; set; }
}

However, I discovered on the linked post that this is not right and that as DeferredData's primary key was called Id I actually needed:

public class MemberDataSet
{
    [Key]
    [DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)]
    public int Id { get; set; }

    public int? DeferredDataId { get; set; }
    [ForeignKey("Id")]
    public virtual DeferredData DeferredData { get; set; }
}

i.e. ForeignKey is used to point to the other class.

I then proceeded to change some of the other references:

public class MemberDataSet
{
    [Key]
    [DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)]
    public int Id { get; set; }

    public int? DeferredDataId { get; set; }
    [ForeignKey("Id")]
    public virtual DeferredData DeferredData { get; set; }

    public int? SignedOffById { get; set; }
    [ForeignKey("UserId")]
    public virtual UserProfile SignedOffBy { get; set; }
}

However, this failed. Turned out on this one the ForeignKey needed to point to the Id on MemberDataSet class.

public class MemberDataSet
{
    [Key]
    [DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)]
    public int Id { get; set; }

    public int? DeferredDataId { get; set; }
    [ForeignKey("Id")]
    public virtual DeferredData DeferredData { get; set; }

    public int? SignedOffById { get; set; }
    [ForeignKey("SignedOffById")]
    public virtual UserProfile SignedOffBy { get; set; }
}

I presume this is because this second relationship is one to many whereas the first was one to zero or one, and that effectively the principal end of the relationship differs, but I would appreciate some clarity on this/references to good articles, so I can understand what is happening and exactly what ForeignKey is doing.

I was also looking for clarity in the example above of how public int? DeferredDataId { get; set; } fits into the equation given it is not explicitly linked to DeferredData. I am happy this will match up by convention but how would I explicitly tell it this e.g. if it had a different name? Al the examples I have seen on this talk about using the ForeignKey attribute but this can't be the answer in all cases per above!

All help greatly appreciated - looking to understand the issue rather than fix a specific problem as I have lots of references in my model so need to establish what approach to take with each.

Thanks.

Edit

Added other classes to help:

public class DeferredData
{

    [Key]
    [DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)]
    public int Id { get; set; }

    //other properties
}

public class UserProfile
{

    [Key]
    [DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)]
    public int UserId { get; set; }

    //other properties
}
Moho
  • 15,457
  • 1
  • 30
  • 31
Mark007
  • 345
  • 1
  • 4
  • 8

3 Answers3

33

The required side of the 1..0 relationship MemberDataSet should not have a FK to DeferredData. Instead, DeferredData's PK should also be a FK to MemberDataSet (known as shared primary key)

public class MemberDataSet
{
    [Key]
    [DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)]
    public int Id { get; set; }

    public virtual DeferredData DeferredData { get; set; }
}

public class DeferredData
{
    // DeferredData.Id is both the PK and a FK to MemberDataSet
    [Key]
    [DatabaseGenerated( DatabaseGeneratedOption.None )]
    [ForeignKey( "MemberDataSet" )]
    public int Id { get; set; }

    [Required]
    public virtual MemberDataSet MemberDataSet { get; set; }
}

Fluent API:

modelBuilder.Entity<MemberDataSet>()
    .HasOptional( mds => mds.DeferredData )
    .WithRequired()
    .WillCascadeOnDelete();
Moho
  • 15,457
  • 1
  • 30
  • 31
  • So with all 1 to 1 or 1 to 0..1 relationships you would reference the other class? How does the public int? DeferredDataId { get; set; } property then fit in with this? – Mark007 Feb 18 '14 at 14:30
  • 1
    No, you're referencing the PK field of the current class, which apparently has the same name of `Id`. `DeferredDataId` would not be used in this situation (as the FK, anyway) – Moho Feb 18 '14 at 14:32
  • Ah ok - so I'm not referencing the Id in the DeferredData class I'm referencing Id in the current MemberDataSet class which will have the same value as Id in the DeferredData class as it is a 1:0..1 relationship. That's making much more sense. Should DeferredData have anything set to ensure it uses the MemberDataId as it's key? Seems odd to specify it as an identity if this is how they interact. – Mark007 Feb 18 '14 at 14:43
  • yes, my updated answer shows how the only FK should be the PK of `DeferredData` and that `MemberDataSet` does not need a FK to `DeferredData` – Moho Feb 18 '14 at 14:46
  • 1
    Great! Is this possible to do without having the navigation property in DeferredData? – Mark007 Feb 18 '14 at 14:49
  • 1
    Not with attributes, you would have to use the fluent API – Moho Feb 18 '14 at 14:52
  • 1
    `modelBuilder.Entity().HasOptional(d => d.DeferredData).WithRequired().WillCascadeOnDelete(true);` then leave off the ForeignKey attribute? Can't see a IsForeignKey type option. – Mark007 Feb 18 '14 at 14:59
  • yup, i had just added that exact block of code to the answer – Moho Feb 18 '14 at 14:59
  • Do you have any documentation on using the `[ForeignKey]` attribute this way? Couldn't find anything. The only thing I found was, that virtual on the navigation property makes the foreign key obsolete. – csteinmueller Feb 18 '14 at 15:03
  • 1
    `virtual` nav properties simply means that proxies can lazy load the property - it has nothing to do with the FK. This is standard usage of the `ForeignKey` attribute, just applied to the PK field of an entity to enforce a 1:0 relationship – Moho Feb 18 '14 at 15:09
0

I think you were right (provided that I understand you right). [ForeignKeyAttribute] is used on the coresponding foreign key. Not on the primary key of your linked object.

This is my object, and the foreign key is DeferredDataId

Neither the Id within your object is the foreign key (it's primary key) nor the ID of the linked object is the foreign key (it's the primary key of the other side of the relation)

Hope I understood you correctly :) Because I'm not sure.

Trevor Nestman
  • 2,456
  • 19
  • 26
csteinmueller
  • 2,427
  • 1
  • 21
  • 32
  • I like this description "This is my object, and the foreign key is DeferredDataId". That's how I'd always thought of it. However, this failed to build the database and the solution was to change to Id i.e. referencing the other class per the linked post. – Mark007 Feb 18 '14 at 14:28
  • I've checked an older project of mine. I did it exactly this way (and it worked). Environment was wcf dataservice with sql ce 4 and entity framework code first. But I don't think that's relevant. Do you get any errors? – csteinmueller Feb 18 '14 at 14:36
  • Error was "The ForeignKeyAttribute on property 'DeferredData' on type 'MemberDataSet' is not valid. The foreign key name 'DeferredDataId' was not found on the dependent type 'DeferredData'." I think @Moho has explained the issue and it's starting to make some sense! – Mark007 Feb 18 '14 at 14:45
  • I will link to a MSDN article. In chapter `Relationship Attributes: InverseProperty and ForeignKey` the explanation is exactly this. Another way is to use virtual for the relationship property. EF code first will create the foreign key by itself. http://msdn.microsoft.com/en-us/data/gg193958.aspx – csteinmueller Feb 18 '14 at 15:01
  • Thanks had read that earlier but couldn't work out how to apply it to what I was doing. All seems much clearer now! – Mark007 Feb 18 '14 at 15:06
0

I think that your original idea was correct, with one slight exception. By putting the foreign key on the MemberDataSet you are implying that you would like a zero-or-one-to-many relationship.

In your example, MemberDataSet.DeferredData is optional, and DeferredData can be referred to by many MemberDataSet instances.

In fluent syntax this would be expressed by:

modelBuilder.Entity<MemberDataSet>()
    .HasOptional(dataSet => dataSet.DeferredData)
    .WithMany()
    .HasForeignKey(deferredData => deferredData.DeferredDataId);

In order to make this a one-to-zero-or-one property you can put a unique (where not null) key constraint on MemberDataSet's DeferredDataId column. This would mean that a DeferredData entity could only be referred to by a single MemberDataSet entity.

CREATE UNIQUE INDEX unique_MemberDataSet_DeferredDataId ON MemberDataSet(DeferredDataId) WHERE DeferredDataId IS NOT NULL

Note: This type of filtered key is only available in SQL Server 2008 and up.

Community
  • 1
  • 1
David Kirkland
  • 2,431
  • 28
  • 28