1

I've been pulling my hair out trying to figure out what I thought would be a relatively simple relationship mapping (one to many).

Let's go over what I have in the models:

ContentExternalLink

public class ContentExternalLink
{
    public ContentExternalLink()
    {
        ContentTagAssignments = new List<ContentTagAssignment>();
    }

    [Key]
    public string LinkId { get; set; }
    public string LinkTypeId { get; set; }
    public string LinkTitle { get; set; }
    public string LinkUrl { get; set; }
    public string LinkSource { get; set; }
    public string LinkPhoneNumber { get; set; }
    public DateTime LinkDate { get; set; }
    public DateTime LinkCreatedDate { get; set; }
    public DateTime LinkModifiedDate { get; set; }
    [ScriptIgnore]
    public virtual ICollection<ContentTagAssignment> ContentTagAssignments { get; set; }
}

ContentTagAssignmnent

public class ContentTagAssignment
{
    public ContentTagAssignment()
    {
        this.ContentExternalLink = new ContentExternalLink();
    }

    [Key]
    public string TagId { get; set; }
    [Key]
    public string ArticleId { get; set; }
    public bool IsPrimary { get; set; }
    public DateTime CreatedDate { get; set; }
    [ScriptIgnore]
    public virtual ContentExternalLink ContentExternalLink { get; set; }
}

Now into the mapping:

ContentExternalLinkMap

public class ContentExternalLinkMap : EntityTypeConfiguration<ContentExternalLink>
{
    public ContentExternalLinkMap()
    {
        this.ToTable("content_external_link", "dbo");

        this.HasKey(c => c.LinkId);

        this.Property(c => c.LinkId).HasColumnName("link_id");
        this.Property(c => c.LinkTypeId).HasColumnName("link_type_id");
        this.Property(c => c.LinkTitle).HasColumnName("link_title");
        this.Property(c => c.LinkUrl).HasColumnName("link_url");
        this.Property(c => c.LinkSource).HasColumnName("link_source");
        this.Property(c => c.LinkPhoneNumber).HasColumnName("link_phone_number");
        this.Property(c => c.LinkDate).HasColumnName("link_date");
        this.Property(c => c.LinkCreatedDate).HasColumnName("link_created_date");
        this.Property(c => c.LinkModifiedDate).HasColumnName("link_modified_date");
    }
}

ContentTagAssignmnetMap

public class ContentTagAssignmentMap : EntityTypeConfiguration<ContentTagAssignment>
{
    public ContentTagAssignmentMap()
    {
        this.ToTable("content_tag_assignment", "dbo");

        this.HasKey(t => new {t.TagId, t.ArticleId});

        this.Property(t => t.TagId).HasColumnName("tag_id");
        this.Property(t => t.ArticleId).HasColumnName("article_id");
        this.Property(t => t.IsPrimary).HasColumnName("is_primary_tag");
        this.Property(t => t.CreatedDate).HasColumnName("created_date");


        this.HasOptional(t => t.ContentExternalLink)
            .WithMany(t => t.ContentTagAssignments)
            .HasForeignKey(t => new {t.TagId, t.ArticleId});
    }
}

The relation is definitely on the weird side as in LinkId would match up with ArticleId in ContentTagAssignment.

I've tried the following in ContentTagAssignmentMap:

    //this.HasOptional(t => t.ContentExternalLink)
//    .WithMany(t => t.ContentTagAssignments)
//    .HasForeignKey(t => t.ContentExternalLink);

this.HasOptional(x => x.ContentExternalLink)
    .WithMany(x=>x.ContentTagAssignments)
    .Map(x => x.MapKey("LinkId").HasColumnAnnotation("LinkId","ArticleId",null));

//this.Map(m =>
//    {
//        m.Properties(x => x.ArticleId);
//        m.ToTable("content_tag_assignment");
//    })
//    .Map(p =>
//    {
//        p.Properties(x => x.ContentExternalLink.LinkId);
//        p.ToTable("ContentExternalLink");
//    });

The commented relations doesn't work. The one that works (in that it doesn't fail on initial page load). However, when I try to access the List from the ContentExternalLink, I get the following error: {"Invalid column name 'LinkId'.\r\nInvalid column name 'LinkId'."}

So I'm even more lost...

Question

Basically, not every ContentExternalLink will have a tag assignment to it. But, if there are ContentTagAssignments to a ContentExternalLink, I should be able to get a list of them. Also, the TagId and ArticleId are strings (guids).

Does anyone have any suggestions?

Thank you!

AJ Tatum
  • 653
  • 2
  • 15
  • 35
  • Both models you have shown are the same (`ContentTagAssignmnent`) :) –  Oct 10 '16 at 12:38
  • @StephenMuecke doh! Thanks for letting me know, I've updated the ContentExternalLink model with the real model. – AJ Tatum Oct 10 '16 at 12:47
  • http://stackoverflow.com/q/20757594/861716 – Gert Arnold Oct 11 '16 at 08:21
  • @GertArnold I'm not sure how that post applies to my question. I have updated my post with more information. – AJ Tatum Oct 11 '16 at 13:38
  • 1
    Look at your `ContentTagAssignment` constructor. – Gert Arnold Oct 11 '16 at 13:55
  • Tried: public virtual ContentExternalLink ContentExternalLink { get { return this._contentExternalLink ?? (this._contentExternalLink = new ContentExternalLink()); } } But it still doesn't work. – AJ Tatum Oct 11 '16 at 15:34
  • Shouldn't the TagAssignment class just have the LinkId instead of a Link object? After all, the one-to-many is just a foreign key (the guid of the one) in the many table – Duston Oct 12 '16 at 17:27

3 Answers3

2

After the comments from ivan stoev, i see that the unique mistake here is setting optional when the foreign key is also a primary key (not nullable)

So the mapping will be:

public class ContentTagAssignmentMap : EntityTypeConfiguration<ContentTagAssignment>
{
    public ContentTagAssignmentMap()
    {
        this.ToTable("content_tag_assignment", "dbo");

        this.HasKey(t => new { t.TagId, t.ArticleId });

        this.Property(t => t.TagId).HasColumnName("tag_id");
        this.Property(t => t.ArticleId).HasColumnName("article_id");
        this.Property(t => t.IsPrimary).HasColumnName("is_primary_tag");
        this.Property(t => t.CreatedDate).HasColumnName("created_date");


        this.HasRequired(t => t.ContentExternalLink)
            .WithMany(t => t.ContentTagAssignments)
            .HasForeignKey(t => t.ArticleId)
            .WillCascadeOnDelete(false);
    }
}

Below is a working example with your model:

class Program
{
    static void Main(string[] args)
    {
        var ctx = new Context();
        ctx.Database.Delete();
        ctx.Database.CreateIfNotExists();

        Console.ReadKey();
    }
}

public class Context : DbContext
{
    public Context():base ("Teste")
    {

    }

    public DbSet<ContentExternalLink> ContentExternalLinks { get; set; }
    public DbSet<ContentTagAssignment> ContentTagAssignments { get; set; }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Configurations.Add(new ContentExternalLinkMap());
        modelBuilder.Configurations.Add(new ContentTagAssignmentMap());

        base.OnModelCreating(modelBuilder);
    }

}

public class ContentExternalLink
{
    public ContentExternalLink()
    {
        ContentTagAssignments = new List<ContentTagAssignment>();
    }

    [Key]
    public string LinkId { get; set; }
    public string LinkTypeId { get; set; }
    public string LinkTitle { get; set; }
    public string LinkUrl { get; set; }
    public string LinkSource { get; set; }
    public string LinkPhoneNumber { get; set; }
    public DateTime LinkDate { get; set; }
    public DateTime LinkCreatedDate { get; set; }
    public DateTime LinkModifiedDate { get; set; }

    public virtual ICollection<ContentTagAssignment> ContentTagAssignments { get; set; }
}

public class ContentTagAssignment
{
    public ContentTagAssignment()
    {
        this.ContentExternalLink = new ContentExternalLink();
    }

    [Key]
    public string TagId { get; set; }
    [Key]
    public string ArticleId { get; set; }
    public bool IsPrimary { get; set; }
    public DateTime CreatedDate { get; set; }

    public virtual ContentExternalLink ContentExternalLink { get; set; }
}

public class ContentExternalLinkMap : EntityTypeConfiguration<ContentExternalLink>
{
    public ContentExternalLinkMap()
    {
        this.ToTable("content_external_link", "dbo");

        this.HasKey(c => c.LinkId);

        this.Property(c => c.LinkId).HasColumnName("link_id");
        this.Property(c => c.LinkTypeId).HasColumnName("link_type_id");
        this.Property(c => c.LinkTitle).HasColumnName("link_title");
        this.Property(c => c.LinkUrl).HasColumnName("link_url");
        this.Property(c => c.LinkSource).HasColumnName("link_source");
        this.Property(c => c.LinkPhoneNumber).HasColumnName("link_phone_number");
        this.Property(c => c.LinkDate).HasColumnName("link_date");
        this.Property(c => c.LinkCreatedDate).HasColumnName("link_created_date");
        this.Property(c => c.LinkModifiedDate).HasColumnName("link_modified_date");
    }
}

public class ContentTagAssignmentMap : EntityTypeConfiguration<ContentTagAssignment>
{
    public ContentTagAssignmentMap()
    {
        this.ToTable("content_tag_assignment", "dbo");

        this.HasKey(t => new { t.TagId, t.ArticleId });

        this.Property(t => t.TagId).HasColumnName("tag_id");
        this.Property(t => t.ArticleId).HasColumnName("article_id");
        this.Property(t => t.IsPrimary).HasColumnName("is_primary_tag");
        this.Property(t => t.CreatedDate).HasColumnName("created_date");


        this.HasRequired(t => t.ContentExternalLink)
            .WithMany(t => t.ContentTagAssignments)
            .HasForeignKey(t => t.ArticleId)
            .WillCascadeOnDelete(false);
    }
}

Here is the result from the code above:

Created Tables

Community
  • 1
  • 1
Rafael Marques
  • 1,501
  • 15
  • 23
  • Sorry, I reverted back the proposed change. The FK is always at the `many` side, so it cannot be the `LinkId`. – Ivan Stoev Oct 17 '16 at 19:28
  • Its the key on the optional side that references the many side that you'll use. You need the FK on ContentTagAssignment that references ContentExternalLink, which is LinkId – Rafael Marques Oct 17 '16 at 19:35
  • 1
    Correct. But there is no `LinkId` property in the OP provided `ContentTagAssignment` entity, and according to the OP explanation, *LinkId would match up with ArticleId in ContentTagAssignment*, so my assumption is based on that statement (and is mentioned in the answer). – Ivan Stoev Oct 17 '16 at 19:39
  • Did not see that ArticleId would match LinkId. My mistake, sorry – Rafael Marques Oct 17 '16 at 19:43
1

There are two issues with your entity model/implementation.

Let start with the simlpler (mentioned in the Gert Arnold's comment). As a rule of thumb, you should never initialize a single navigation property inside the constructor (or any other place internally) because that would break the EF lazy loading behavior.

Shortly, simply remove

this.ContentExternalLink = new ContentExternalLink();

line from the ContentTagAssignment class constructor.

Now the main issue. EF6 supports only FK (foreign key) relationships that refer to the PK (primary key) of the principal end. Which means you cannot use a composite key (TagId, ArticleId) in the ContentTagAssignment entity to refer to a single PK (LinkId) of the ContentExternalLink entity as you are trying to set with the HasForeignKey(t => new {t.TagId, t.ArticleId}) call. The fact that these two columns form the PK of the dependent end (ContentTagAssignment) is irrelevant for the relationship. There is no other choice - you have to pick one of the two fields. And according to LinkId would match up with ArticleId in ContentTagAssignment, that should be the ArticleId field.

If my guess is correct, simply replace

this.HasOptional(t => t.ContentExternalLink)
    .WithMany(t => t.ContentTagAssignments)
    .HasForeignKey(t => new {t.TagId, t.ArticleId});

with

this.HasRequired(t => t.ContentExternalLink)
    .WithMany(t => t.ContentTagAssignments)
    .HasForeignKey(t => t.ArticleId);

and that (along with the constructor modification) should solve the issue.

Note that I also changed HasOptional to HasRequired because the ArticleId field is part of the PK, hence will not allow null values anyway.

Community
  • 1
  • 1
Ivan Stoev
  • 195,425
  • 15
  • 312
  • 343
0

I believe the issue is that you are trying to have a foreign key without indicating the correct join.

What you need to do is add a LinkId field to ContentTagAssignment which is the actual foreign key you should be using and join to that:

this.HasOptional(t => t.ContentExternalLink)
.WithMany(t => t.ContentTagAssignments)
.HasForeignKey(t => t.LinkId);

The reason is that you are telling it to go find a key (tagId, ArticleId) that just doesn't exist in ContentExternalLink.

JuanR
  • 7,405
  • 1
  • 19
  • 30