sterix24
sterix24

Reputation: 2400

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

Upvotes: 1

Views: 1361

Answers (3)

sterix24
sterix24

Reputation: 2400

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

Upvotes: 0

mj1313
mj1313

Reputation: 8459

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>();

    //...
}

Upvotes: 1

mspaic96
mspaic96

Reputation: 91

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

Upvotes: 1

Related Questions