Reputation: 2400
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
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
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
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