【问题标题】:Add tags to talk for many to many relationship in Entity Framework在实体框架中添加标签以讨论多对多关系
【发布时间】:2025-12-27 18:50:16
【问题描述】:

我正在创建一个 API 端点,该端点创建一个新的 Talk,其中包含应该与谈话相关联的标签。我在我的域中的标签和对话之间建立了多对多关系,请参阅下面的关系。

标签.cs

using System;
using System.Collections.Generic;

namespace Conferency.Domain
{
    public class Tag : IAuditable
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public List<TalkTag> TalkTags { get; set; }
        public DateTime ModifiedAt { get; set; }
        public DateTime CreatedAt { get; set; }
    }
}

Talk.cs

using System;
using System.Collections.Generic;

namespace Conferency.Domain
{
    public class Talk : IAuditable
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Url { get; set; }
        public List<TalkTag> TalkTags { get; set; }
        public DateTime Presented { get; set; }
        public DateTime ModifiedAt { get; set; }
        public DateTime CreatedAt { get; set; }
    }
}

TalkTag.cs​​

using System;
using System.Collections.Generic;
using System.Text;

namespace Conferency.Domain
{
    public class TalkTag
    {
        public int TalkId { get; set; }
        public Talk Talk { get; set; }
        public int TagId { get; set; }
        public Tag Tag { get; set; }
    }
}

ConferencyContext.cs(删除无关代码)

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using System;
using Conferency.Domain;

namespace Conferency.Data
{
    public class ConferencyContext: DbContext
    {
        public DbSet<Talk> Talks { get; set; }
        public DbSet<Tag> Tags { get; set; }
        public DbSet<TalkTag> TagTalks { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<TalkTag>()
                .HasKey(s => new { s.TalkId, s.TagId });

            modelBuilder.Entity<TalkTag>()
                .HasOne(pt => pt.Talk)
                .WithMany(p => p.TalkTags)
                .HasForeignKey(pt => pt.TalkId);

            modelBuilder.Entity<TalkTag>()
                .HasOne(pt => pt.Tag)
                .WithMany(t => t.TalkTags)
                .HasForeignKey(pt => pt.TagId);

            base.OnModelCreating(modelBuilder);
        }
    }
}

TalkViewModel.cs

using System;
using System.Collections.Generic;

namespace Conferency.Application.Models
{
    public class TalkViewModel
    {
        public string Name { get; set; }
        public string Url { get; set; }
        public List<String> Tags { get; set; }
    }
}

问题是我不知道如何创建演讲及其标签(如果存在则附加,如果不存在则创建)。我不确定以什么顺序完成此操作。我是否必须查询每个标签以检查它们是否存在,或者是否有我可以使用的 findOrCreate 方法?如果尚未创建 Talk,如何创建 TalkTag 记录?有没有一种我不理解的优雅方法来完成这个?

TalkRepository.cs

using System.Collections.Generic;
using Conferency.Domain;
using Microsoft.EntityFrameworkCore;
using System;
using System.Linq;
using System.Threading.Tasks;

namespace Conferency.Data
{
    public class TalkRepository : ITalkRepository
    {
        private ConferencyContext _context;

        public TalkRepository(ConferencyContext context)
        {
            _context = context;
        }

        public void Add(Talk entity)
        {
            _context.Add(entity);
        }

        public void AddWithTags(Talk entity, List<String> tags)
        {
            // Create Talk
            // Query for each tag
            // Create if they don't exist
            // Attach to talk
            // ??
        }

        public IEnumerable<Talk> GetAllTalks()
        {
            return _context.Talks
                .Include(c => c.TalkTags)
                .OrderBy(c => c.Presented)
                .ToList();
        }

        public Talk GetTalk(int id)
        {
            return _context.Talks
                .Include(c => c.TalkTags)
                .Where(c => c.Id == id)
                .FirstOrDefault();
        }

        public async Task<bool> SaveAllAsync()
        {
            return (await _context.SaveChangesAsync()) > 0;
        }
    }
}

我是 c# 新手,我正在尝试学习最佳实践并熟悉 EF 和 ASP.NET Core,因此希望有人可以帮助指导我走上正确的道路。完整的解决方案在这里,如果你想看看https://github.com/bliitzkrieg/Conferency

我尝试自己解决它,但我得到一个 NullPointerException,这是我尝试的解决方案:

TalksController.cs

  [HttpPost()]
        public async Task<IActionResult> Post([FromBody]TalkViewModel model)
        {
            try
            {
                _logger.LogInformation("Creating a new Talk");

                List<Tag> tags = _tagRepo.FindOrCreateTags(model.Tags);

                Talk talk = new Talk { Name = model.Name, Url = model.Url };

                List<TalkTag> talkTags = new List<TalkTag>();
                tags.ForEach(tag =>
                {
                    var talkTag = new TalkTag { TagId = tag.Id, Talk = talk };
                    talkTags.Add(talkTag);
                });

                talk.TalkTags.AddRange(talkTags); // Exception being thrown here
                _repo.Add(talk);

                if (await _repo.SaveAllAsync())
                {
                    string newUri = Url.Link("TalkGet", new { id = talk.Id });
                    return Created(newUri, talk);
                }
                else
                {
                    _logger.LogWarning("Could not save Talk");
                }
            }
            catch (Exception ex)
            {
                _logger.LogError($"Threw exception while saving Talk: {ex}");
            }

            return BadRequest();
        }
    }

TagRepository.cs

using System;
using System.Collections.Generic;
using Conferency.Domain;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using System.Linq;

namespace Conferency.Data
{
    public class TagRepository: ITagRepository
    {
        private ConferencyContext _context;

        public TagRepository(ConferencyContext context)
        {
            _context = context;
        }

        public void Add(Tag entity)
        {
            _context.Add(entity);
        }

        public List<Tag> FindOrCreateTags(List<string> tags)
        {
            List<Tag> _tags = new List<Tag>();
            tags.ForEach(t =>
            {
                try
                {
                    var tag = _context.Tags
                       .Where(c => c.Name == t)
                       .FirstOrDefault();

                    if (tag != null)
                    {
                        _tags.Add(tag);
                    }
                    else
                    {
                        Tag created = new Tag { Name = t };
                        this.Add(created);
                        _tags.Add(created);
                    }
                }
                catch (Exception ex)
                {

                }

            });

            return _tags;
        }

        public async Task<bool> SaveAllAsync()
        {
            return (await _context.SaveChangesAsync()) > 0;
        }
    }
}

【问题讨论】:

  • 由于您的实体的所有属性都不是virtual,因此您似乎没有正确设置导航属性。
  • 我对虚拟实体了解不多,但我遵循了 EF 核心文档 (docs.microsoft.com/en-us/ef/core/modeling/relationships) 上的多对多指南。
  • 您也应该提供您的 TalkViewModel 代码。根据我对您的代码的了解,我猜您的 TalkViewModel 上需要一个 List 属性,该属性具有标签 ID、标签文本和布尔“已选择”属性。然后,当您将 TalkViewModel 传递给您的 repo 时,过滤掉选定的 TagViewModels 并为每个标签添加一个具有正确 TagId 的 TalkTag 到您的 TalkTags 属性中。 EF 应该注意在 SaveChanges() 上添加正确的 TalkId。
  • 如果您的用户可以在您的 Talk 表单上动态添加标签,请将其 ID 设置为 0,然后过滤这些 TagViewModel 并将它们作为新标签添加到您的 TalkTag 上的 Tag 属性中。让我知道这是否有效,我将提供正式答案。
  • 我添加了 TalkViewModel。今晚我会尝试你的解决方案并给你更新

标签: asp.net entity-framework asp.net-core entity-framework-core


【解决方案1】:

在您的 TalkViewModel 上添加具有以下属性的 List&lt;TagViewModel&gt; 属性:

public int TagId { get; set; }
public string TagName { get; set; }
public bool Selected { get; set; }

当您将 TalkViewModel 传递给您的存储库时,过滤掉选定的 TagViewModels,并为每个 TalkTag 添加一个带有正确 TagIdTalkTags 属性到您的 Talk 上。 EF 应该注意在_context.SaveChanges() 上添加正确的TalkId

如果Tag 不存在,请创建一个TalkTag,并使用新的Tag 和新的Talk 作为其属性,然后将其添加到您的_context。 EF 应该负责其余的工作。

【讨论】:

    【解决方案2】:

    您尚未初始化导致空指针的 TalkTags 集合。在初始化 Talk 对象时试试这个:

    Talk talk = new Talk { Name = model.Name, Url = model.Url, TalkTags = new List<TalkTag>() };
    

    您需要 TalkTag 对象的更多属性吗?否则,您可以只在 Tag 类中使用 List&lt;Talk&gt;,在 Talk 类中使用 List&lt;Tag&gt;,并且映射将由 EF 完成(将在 DB 中创建 TalkTag 表)。

    Michael Tranchida 已经描述了将对象添加到上下文的方法。

    【讨论】: