简体   繁体   中英

How do I add and update items using Entity Framework Core 5 that have many to many relationships?

I've been struggling with this all evening and still don't fully understand how Entity Framework Core works with many to many relationships.

I have a TransportProvider class and a Tag class. It's a many to many relationship. When adding a new TransportProvider you can assign tags. If the tag already exists in the database I'd like to add that existing tag, otherwise I'd like to insert a new tag. This is what I have for my TransportProvider class:

public class TransportProvider
{      
    public int ID { get; set; }

    [Display(Name = "Company name")]
    [Required]
    [StringLength(200)]
    public string CompanyName { get; set; }

    ... standard properties

    public bool Disabled { get; set; }

    [NotMapped]
    public string SelectedTags { get; set; }

    public ICollection<Tag> Tags { get; set; }
}

My tag class:

public class Tag
{
    public int ID { get; set; }

    [Required]
    [StringLength(100)]
    public string Name { get; set; }

    public ICollection<TransportProvider> TransportProviders { get; set; }
}

And this is my controller function that creates a new transport provider:

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create([Bind("ID,CompanyName,ContactName,ContactTelephone1,ContactTelephone2,ContactEmail,CompanyWebsite,AddressLine1,AddressLine2,Suburb,Province,PostCode,Country,Lat,Lng,SelectedTags,Notes,Disabled")] TransportProvider transportProvider)
{
    if (ModelState.IsValid)
    {
        var selectedTags = !string.IsNullOrEmpty(transportProvider.SelectedTags) ? transportProvider.SelectedTags.Split(',') : new string[0];

        _context.TransportProviders.Add(transportProvider); 

        foreach (var selectedTag in selectedTags)
        {
            var tag = _context.Tags.SingleOrDefault(t => t.Name.ToLower() == selectedTag);
            if (tag == null)
            {
                tag = new Tag();
                tag.Name = selectedTag;     
                
            }

            transportProvider.Tags.Add(tag);                    
        }

        await _context.SaveChangesAsync();

        return RedirectToAction(nameof(Index));
    }

    return View(transportProvider);
}

and finally my context class:

public class AntelopeContext : DbContext
{
    public AntelopeContext(DbContextOptions<AntelopeContext> options) : base(options)
    {
    }

    public DbSet<TransportProvider> TransportProviders { get; set; }
    public DbSet<Tag> Tags { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<TransportProvider>().ToTable("TransportProvider");
        modelBuilder.Entity<Tag>().ToTable("Tag");
    }
}

If I try and execute this code I get a NullReferenceException for the line:

transportProvider.Tags.Add(tag); 

I don't know why this is so difficult to do. All I want to do is add tags to a transport provider. If the tag is new it needs to insert a new tag record. If not then it just has to link the existing tag.

How do I do this?

Thanks

Many to many relationships require a collection navigation property on both sides. They will be discovered by convention like other types of relationships.

public class TransportProvider
{
    public int TransportProviderId { get; set; }
    public string CompanyName { get; set; }
    public bool Disabled { get; set; }

    public ICollection<Tag> Tags { get; set; }
}

public class Tag
{
    public int TagId { get; set; }
    
    public string Name { get; set;}

    public ICollection<TransportProvider> TransportProviders { get; set; }
}


The way this relationship is implemented in the database is by a join table that contains foreign keys to both TransferProvider and Tag. For example this is what EF will create in a relational database for the above model.

CREATE TABLE [TransportProvider] (
    [TransportProviderId] int NOT NULL IDENTITY,
    [CompanyName] nvarchar(max) NULL,
    [Disable]    bit NULL,
    CONSTRAINT [PK_TransportProvider] PRIMARY KEY ([TransportProviderId])
);

CREATE TABLE [Tag] (
    [TagId] int NOT NULL IDENTITY,
    [Name]  nvarchar(max) NULL,
    CONSTRAINT [PK_Tag] PRIMARY KEY ([TagId])
);

CREATE TABLE [TransportProviderTag] (
    [TransportProviderId] int NOT NULL,
    [TagId]  int NOT NULL,
    CONSTRAINT [PK_TransportProviderTag] PRIMARY KEY ([TransportProviderId], [TagId]),
    CONSTRAINT [FK_TransportProviderTag_TransportProviders_TransportProviderId] FOREIGN KEY ([TransportProviderId]) REFERENCES [TransferProviders] ([TransferProviderId]) ON DELETE CASCADE,
    CONSTRAINT [FK_TransportProviderTag_Tags_TagId] FOREIGN KEY ([TagId]) REFERENCES [Tags] ([TagId]) ON DELETE CASCADE
);

Internally, EF creates an entity type to represent the join table that will be referred to as the join entity type.

This is a code first approach. You have first to create TransferProvider and Tag, and then add what row with them in TransferProviderTag table

Since you didn't bind the Tags property, it will default be null , you need to initialize the Tags in TransportProvider firstly.

public async Task<IActionResult> Create([Bind("ID,CompanyName,ContactName,ContactTelephone1,ContactTelephone2,ContactEmail,CompanyWebsite,AddressLine1,AddressLine2,Suburb,Province,PostCode,Country,Lat,Lng,SelectedTags,Notes,Disabled")] TransportProvider transportProvider)
{

    transportProvider.Tags = new List<Tag>();

    //...
}

Finally. I got it working, I'm not sure this is the 'correct' way. but it seems to work.

I was under the impression that EF Core 5 didn't require joining tables in many-to-many relationships. However when I tried to execute without a joining table I was getting an error about a joining table not being present. I therefore added one as suggested.

I then manually created the TransportProvider, manually checked for a Tag and created if it didn't exist, then manually entered the joining table record. I still feel this probably isn't the most efficient way of doing things, but it works. Code in case anyone is interested:

public async Task<IActionResult> Create([Bind("ID,CompanyName,ContactName,ContactTelephone1,ContactTelephone2,ContactEmail,CompanyWebsite,AddressLine1,AddressLine2,Suburb,Province,PostCode,Country,Lat,Lng,SelectedTags,Notes,Disabled")] TransportProvider transportProvider)
{
    if (ModelState.IsValid)
    {
        var selectedTags = !string.IsNullOrEmpty(transportProvider.SelectedTags) ? transportProvider.SelectedTags.Split(',') : new string[0];

        transportProvider.TransportProviderTags = new List<TransportProviderTag>();

        _context.TransportProviders.Add(transportProvider);

        await _context.SaveChangesAsync();

        foreach (var selectedTag in selectedTags)
        {
            var tag = _context.Tags.SingleOrDefault(t => t.Name.ToLower() == selectedTag);
            if (tag == null)
            {
                tag = new Tag();
                tag.Name = selectedTag;
                _context.Tags.Add(tag);

                await _context.SaveChangesAsync();
            }

            var tpt = new TransportProviderTag();
            tpt.TransportProviderID = transportProvider.ID;
            tpt.TagID = tag.ID;
            transportProvider.TransportProviderTags.Add(tpt);

            await _context.SaveChangesAsync();
        }

        return RedirectToAction(nameof(Index));
    }

    return View(transportProvider);
}

Updated context class:

public class AntelopeContext : DbContext
{
    public AntelopeContext(DbContextOptions<AntelopeContext> options) : base(options)
    {
    }

    public DbSet<TransportProvider> TransportProviders { get; set; }
    public DbSet<Tag> Tags { get; set; }
    public DbSet<TransportProviderTag> TransportProviderTags { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<TransportProvider>().ToTable("TransportProvider");
        modelBuilder.Entity<Tag>().ToTable("Tag");
        modelBuilder.Entity<TransportProviderTag>().ToTable("TransportProviderTag");

        modelBuilder.Entity<TransportProviderTag>()
            .HasKey(tpt => new { tpt.TransportProviderID, tpt.TagID });
        modelBuilder.Entity<TransportProviderTag>()
            .HasOne(tpt => tpt.TransportProvider)
            .WithMany(tp => tp.TransportProviderTags)
            .HasForeignKey(tpt => tpt.TransportProviderID);
        modelBuilder.Entity<TransportProviderTag>()
            .HasOne(tpt => tpt.Tag)
            .WithMany(t => t.TransportProviderTags)
            .HasForeignKey(tpt => tpt.TagID);
    }
}

And thanks @MilutinSpaic and @mj1313 for steering me in the right direction. Hopefully this will help someone else

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