使

使用 Entity Framework Core 的多对多关系使用 Entity Framework Core 的高级关系(续)

2025-06-10

与 Entity Framework Core 的多对多关系

与 Entity Framework Core 的高级关系(续)

本系列教程现已提供在线视频课程。您可以在 YouTube 上观看第一个小时的课程,或在 Udemy 上获取完整课程。您也可以继续阅读。尽情享受吧!:)

与 Entity Framework Core 的高级关系(续)

与技能的多对多关系

使用 Entity Framework Core 实现多对多关系与实现其他关系略有不同。

在我们的角色扮演游戏示例中,我们添加了一系列技能,所有角色都可以使用。这意味着,每个角色的某项特定技能无法升级。每个角色都可以从众多技能中选择。所以,一般来说,即使是骑士也可以投掷火球,法师也可以疯狂地击溃对手

Skill当然,首先要做的是添加模型。

我们创建一个新的 C# 类并添加属性IdNameDamage



public class Skill
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int Damage { get; set; }
}


Enter fullscreen mode Exit fullscreen mode

注意,我们这里没有添加 type 列表Character如果我们想要实现一对多关系,我们添加 type 列表。但是对于多对多关系,我们需要一个特殊的实现——那就是连接表

Entity Framework Core 目前无法自行创建连接表。因此,我们必须手动添加一个连接表,并告​​诉 Entity Framework 如何连接两个实体SkillCharacter

首先,让我们为这个实体添加模型。我们创建一个新的 C# 类并将其命名为CharacterSkill。现在,为了连接技能和角色,我们必须将它们添加为属性。因此,我们添加了CharacterSkill

此外,我们需要为该实体添加一个主键。它将是 和 的复合键SkillCharacter为了实现这一点,按照惯例,我们CharacterIdCharacterSkillId分别添加一个属性Skill



public class CharacterSkill
{
    public int CharacterId { get; set; }
    public Character Character { get; set; }
    public int SkillId { get; set; }
    public Skill Skill { get; set; }
}


Enter fullscreen mode Exit fullscreen mode

但这还不是全部的魔法。我们仍然需要告诉 Entity Framework Core,我们想要使用这两个 ID 作为复合主键。我们借助Fluent API来实现这一点。

我们将焦点转移到DataContext类上。首先,我们添加新的DbSet属性SkillsCharacterSkills



public DbSet<Skill> Skills { get; set; }
public DbSet<CharacterSkill> CharacterSkills { get; set; }


Enter fullscreen mode Exit fullscreen mode

之后,我们需要添加一些新内容。我们重写了方法OnModelCreating()。该方法接受一个ModelBuilder参数,该参数“定义实体的形状、实体之间的关系以及它们如何映射到数据库”。这正是我们需要的。

我们这里唯一需要配置的是由CharacterSkill组成的实体的复合键。我们使用 来实现CharacterIdSkillIdmodelBuilder.Entity<CharacterSkill>().HasKey(cs => new { cs.CharacterId, cs.SkillId });



protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<CharacterSkill>()
        .HasKey(cs => new { cs.CharacterId, cs.SkillId });
}


Enter fullscreen mode Exit fullscreen mode

就是这样。由于使用了CharacterId和 的命名约定SkillId,我们无需配置任何其他内容。否则,我们将不得不使用 Fluent API 来配置角色和技能之间的关系,例如HasOne()和 之类的方法WithMany()。但 Entity Framework Core 会识别这些,我们很快就能在迁移文件中看到正确的实现。

我们还要做最后一件事,那就是将CharacterSkill列表添加到Character模型中Skill

因此,在这两个 C# 类中,我们都添加了一个CharacterSkills类型为 的新属性List<CharacterSkill>



public class Skill
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int Damage { get; set; }
    public List<CharacterSkill> CharacterSkills { get; set; }
}


Enter fullscreen mode Exit fullscreen mode


public class Character
{
    public int Id { get; set; }
    public string Name { get; set; } = "Frodo";
    public int HitPoints { get; set; } = 100;
    public int Strength { get; set; } = 10;
    public int Defense { get; set; } = 10;
    public int Intelligence { get; set; } = 10;
    public RpgClass Class { get; set; } = RpgClass.Knight;
    public User User { get; set; }
    public Weapon Weapon { get; set; }
    public List<CharacterSkill> CharacterSkills { get; set; }
}


Enter fullscreen mode Exit fullscreen mode

好的。所有内容保存完成后,我们就可以运行迁移了。

首先,我们使用 添加新的迁移dotnet ef migrations add Skill

在创建的迁移文件中,您可以看到将为我们生成两个新表,SkillsCharacterSkills

CharacterSkill在迁移设计文件中,再往下一点,您现在可以看到加入实体与实体Character和之间的关系的配置Skill



modelBuilder.Entity("dotnet_rpg.Models.CharacterSkill", b =>
    {
        b.HasOne("dotnet_rpg.Models.Character", "Character")
            .WithMany("CharacterSkills")
            .HasForeignKey("CharacterId")
            .OnDelete(DeleteBehavior.Cascade)
            .IsRequired();
        b.HasOne("dotnet_rpg.Models.Skill", "Skill")
            .WithMany("CharacterSkills")
            .HasForeignKey("SkillId")
            .OnDelete(DeleteBehavior.Cascade)
            .IsRequired();
    });


Enter fullscreen mode Exit fullscreen mode

再次,由于使用了Id属性的命名约定,我们不必手动执行此操作。

现在是时候将此迁移添加到数据库中了dotnet ef database update

更新完成后,您可以刷新 SQL Server Management Studio 中的数据库并查看新表SkillsCharacterSkills正确的键。

SQL Server Management Studio 中的新表

太棒了!现在该往这些表格里填充一些内容了。

为角色扮演游戏角色添加技能

向数据库的技能池中添加新技能非常简单。我们需要一个服务、一个接口、一个控制器等等。我想说,我们更专注于添加 RPG 角色和这些技能之间的关联。

因此,我们不需要添加技能服务,而是使用 SQL Server Management Studio 在数据库中手动添加一些技能。

只需右键单击Skills表格并选择“编辑前 200 行”。

编辑前 200 行

现在我们可以添加一些技能,如火球、狂暴或暴风雪。

一些技能

太好了。现在我们可以专注于关系了。我已经剧透了,我们需要一些 DTO。让我们创建一个新文件夹,并添加具有属性和 的CharacterSkill新 C# 类AddCharacterSkillDtoCharacterIdSkillId



namespace dotnet_rpg.Dtos.CharacterSkill
{
    public class AddCharacterSkillDto
    {
        public int CharacterId { get; set; }
        public int SkillId { get; set; }
    }
}


Enter fullscreen mode Exit fullscreen mode

接下来,我们创建文件夹Skill并创建 DTO GetSkillDto,因为我们只需要它来显示角色的技能。我们需要的属性是NameDamage



namespace dotnet_rpg.Dtos.Skill
{
    public class GetSkillDto
    {
        public string Name { get; set; }
        public int Damage { get; set; }
    }
}


Enter fullscreen mode Exit fullscreen mode

之后,我们向 中添加另一个属性,GetCharacterDtoSkills类型List<GetSkillsDto>



public class GetCharacterDto
{
    public int Id { get; set; }
    public string Name { get; set; } = "Frodo";
    public int HitPoints { get; set; } = 100;
    public int Strength { get; set; } = 10;
    public int Defense { get; set; } = 10;
    public int Intelligence { get; set; } = 10;
    public RpgClass Class { get; set; } = RpgClass.Knight;
    public GetWeaponDto Weapon { get; set; }
    public List<GetSkillDto> Skills { get; set; }
}


Enter fullscreen mode Exit fullscreen mode

请注意,我们已经直接访问了技能,而无需CharacterSkill先使用连接实体。您稍后将看到我们是如何实现这一点的。

好的,DTO 已经准备好了,现在我们可以转到服务和控制器文件。

我们创建一个名为 的新文件夹CharacterSkillService,并添加一个名为 的新接口ICharacterSkillService

ServiceResponse我们只添加一个返回a 的方法,GetCharacterDto因为类似于 ,WeaponService这样我们就可以看到添加的技能了。我们调用该方法AddCharacterSkill(),并将 作为一个参数传入AddCharacterSkillDto。当然,在执行此操作时,我们必须添加一些 using 指令。



using System.Threading.Tasks;
using dotnet_rpg.Dtos.Character;
using dotnet_rpg.Dtos.CharacterSkill;
using dotnet_rpg.Models;

namespace dotnet_rpg.Services.CharacterSkillService
{
    public interface ICharacterSkillService
    {
        Task<ServiceResponse<GetCharacterDto>> AddCharacterSkill(AddCharacterSkillDto newCharacterSkill);
    }
}


Enter fullscreen mode Exit fullscreen mode

现在我们可以创建了CharacterSkillService。这项服务看起来与非常相似WeaponService。我们首先实现接口,然后自动ICharacterSkillService添加方法并添加关键字。AddCharacterSkill()async



public class CharacterSkillService : ICharacterSkillService
{
    public async Task<ServiceResponse<GetCharacterDto>> AddCharacterSkill(AddCharacterSkillDto newCharacterSkill)
    {
        throw new NotImplementedException();
    }
}


Enter fullscreen mode Exit fullscreen mode

在编写此方法的实际代码之前,我们先添加构造函数。类似于WeaponService我们注入的DataContextIHttpContextAccessorIMapper。我们添加 using 指令,从参数初始化所有字段,并且如果需要,在每个字段前面添加下划线。



private readonly DataContext _context;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IMapper _mapper;

public CharacterSkillService(DataContext context, IHttpContextAccessor httpContextAccessor, IMapper mapper)
{
    _mapper = mapper;
    _httpContextAccessor = httpContextAccessor;
    _context = context;
}


Enter fullscreen mode Exit fullscreen mode

现在来谈谈AddCharacterSkill()方法。

首先,我们初始化返回ServiceResponse并构建一个空的 try/catch 块。

如果发生异常,我们已经可以将Success的状态设置responsefalse,并将设置Message为异常消息。



public async Task<ServiceResponse<GetCharacterDto>> AddCharacterSkill(AddCharacterSkillDto newCharacterSkil
{
    ServiceResponse<GetCharacterDto> response = new ServiceResponse<GetCharacterDto>();
    try
    {
    }
    catch (Exception ex)
    {
        response.Success = false;
        response.Message = ex.Message;
    }
    return response;
}


Enter fullscreen mode Exit fullscreen mode

接下来将从Character数据库中接收CharacterId通过给出的正确信息AddCharacterSkillDto

再次,它与非常相似WeaponService

首先,我们Characters从 访问_context,并使用 的方法进行过滤FirstOrDefaultAsync(),并根据 进行过滤newCharacterSkill.CharacterId,此外,还通过 进行身份验证User。你还记得这一行很长的代码,它用于从 claims 中获取用户 ID,对吧?



Character character = await _context.Characters
    .FirstOrDefaultAsync(c => c.Id == newCharacterSkill.CharacterId &&
    c.User.Id == int.Parse(_httpContextAccessor.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier)));


Enter fullscreen mode Exit fullscreen mode

但这还不是全部。为了获取所有Weapon技能以及用户的相关内容,我们必须将它们包含在内

我们可以从 开始。添加Weapon之后,技能会变得更有趣一些。我们再次添加,但首先我们访问,然后使用访问 的child属性_context.Characters.Include(c => c.Weapon).Include()CharacterSkillsSkillCharacterSkills.ThenInclude()

这样,我们就可以从character存储在数据库中的每个属性中获取。



Character character = await _context.Characters
    .Include(c => c.Weapon)
    .Include(c => c.CharacterSkills).ThenInclude(cs => cs.Skill)
    .FirstOrDefaultAsync(c => c.Id == newCharacterSkill.CharacterId &&
    c.User.Id == int.Parse(_httpContextAccessor.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier)));


Enter fullscreen mode Exit fullscreen mode

搞定这些之后,我们添加常用的空值检查。所以,如果character是,null我们就设置Success状态,Message然后 并返回response



Character character = await _context.Characters
    .Include(c => c.Weapon)
    .Include(c => c.CharacterSkills).ThenInclude(cs => cs.Skill)
    .FirstOrDefaultAsync(c => c.Id == newCharacterSkill.CharacterId &&
    c.User.Id == int.Parse(_httpContextAccessor.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier))

if (character == null)
{
    response.Success = false;
    response.Message = "Character not found.";
    return response;
}


Enter fullscreen mode Exit fullscreen mode

接下来是Skill。通过参数给出的SkillIdnewCharacterSkill我们从数据库中获取技能。



Skill skill = await _context.Skills
    .FirstOrDefaultAsync(s => s.Id == newCharacterSkill.SkillId);


Enter fullscreen mode Exit fullscreen mode

与 类似character,如果我们找不到具有给定 的技能SkillId,我们将设置ServiceResponse并返回它。



if (skill == null)
{
    response.Success = false;
    response.Message = "Skill not found.";
    return response;
}


Enter fullscreen mode Exit fullscreen mode

现在我们拥有了创建新 所需的一切CharacterSkill

我们初始化一个新characterSkill对象,并将该对象的CharacterSkill属性设置为我们之前从数据库中获得的character和。skill



CharacterSkill characterSkill = new CharacterSkill
{
    Character = character,
    Skill = skill
};


Enter fullscreen mode Exit fullscreen mode

之后,我们将这个新内容添加CharacterSkill到数据库中AddAsync(characterSkill),将所有更改保存到数据库中,最后将设置response.Data为映射的character



await _context.CharacterSkills.AddAsync(characterSkill);
await _context.SaveChangesAsync();

response.Data = _mapper.Map<GetCharacterDto>(character);


Enter fullscreen mode Exit fullscreen mode

这就是整个AddCharacterSkill()方法。



public async Task<ServiceResponse<GetCharacterDto>> AddCharacterSkill(AddCharacterSkillDto newCharacterSkill)
{
    ServiceResponse<GetCharacterDto> response = new ServiceResponse<GetCharacterDto>();
    try
    {
        Character character = await _context.Characters
            .Include(c => c.Weapon)
            .Include(c => c.CharacterSkills).ThenInclude(cs => cs.Skill)
            .FirstOrDefaultAsync(c => c.Id == newCharacterSkill.CharacterId &&
            c.User.Id == int.Parse(_httpContextAccessor.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier))
        if (character == null)
        {
            response.Success = false;
            response.Message = "Character not found.";
            return response;
        }
        Skill skill = await _context.Skills
            .FirstOrDefaultAsync(s => s.Id == newCharacterSkill.SkillId);
        if (skill == null)
        {
            response.Success = false;
            response.Message = "Skill not found.";
            return response;
        }
        CharacterSkill characterSkill = new CharacterSkill
        {
            Character = character,
            Skill = skill
        };

        await _context.CharacterSkills.AddAsync(characterSkill);
        await _context.SaveChangesAsync();
        response.Data = _mapper.Map<GetCharacterDto>(character);
    }
    catch (Exception ex)
    {
        response.Success = false;
        response.Message = ex.Message;
    }
    return response;
}


Enter fullscreen mode Exit fullscreen mode

为了能够调用该服务,我们需要CharacterSkillController,所以让我们创建这个新的 C# 文件。

与往常一样,我们从 、和派生ControllerBase添加属性。我们需要用户信息,因此此控制器只能由经过身份验证的用户访问。[Route(“[controller]”)][ApiController][Authorize]



[Authorize]
[ApiController]
[Route("[controller]")]
public class CharacterSkillController : ControllerBase


Enter fullscreen mode Exit fullscreen mode

然后我们需要一个仅注入的构造函数ICharacterSkillService



private readonly ICharacterSkillService _characterSkillService;
public CharacterSkillController(ICharacterSkillService characterSkillService)
{
    _characterSkillService = characterSkillService;
}


Enter fullscreen mode Exit fullscreen mode

最后,我们添加一个带有as 参数的public async POST方法,该参数将传递给的方法AddCharacterSkill()AddCharacterSkillDtoAddCharacterSkill()_characterSkillService



[HttpPost]
public async Task<IActionResult> AddCharacterSkill(AddCharacterSkillDto newCharacterSkill)
{
    return Ok(await _characterSkillService.AddCharacterSkill(newCharacterSkill));
}


Enter fullscreen mode Exit fullscreen mode

到目前为止控制器。

现在我们在文件中注册新的服务Startup.cs。和往常一样,我们services.AddScoped()在方法中使用 for ConfigureServices()



services.AddScoped<ICharacterSkillService, CharacterSkillService>();


Enter fullscreen mode Exit fullscreen mode

最后一件事是对 进行改变AutoMapperProfile

简单的部分是一张新的地图GetSkillDto



CreateMap<Skill, GetSkillDto>();


Enter fullscreen mode Exit fullscreen mode

现在事情变得更有趣了。我已经告诉过你,我们想要直接访问角色的技能,而不显示连接实体CharacterSkill。我们可以借助 AutoMapper 和函数来实现Select()

首先,我们利用-MapForMember()函数<Character, GetCharacterDto>。通过此函数,我们可以为映射类型的特定成员定义特殊的映射。

在我们的例子中,我们希望正确设置SkillsDTO。

为了做到这一点,我们访问Character对象,并从该对象(即函数MapFrom())中抓取CharacterSkills每个中选择SkillCharacterSkill



CreateMap<Character, GetCharacterDto>()
    .ForMember(dto => dto.Skills, c => c.MapFrom(c => c.CharacterSkills.Select(cs => cs.Skill)));


Enter fullscreen mode Exit fullscreen mode

这就是我们直接掌握技能的方法。

太棒了!现在该测试一下了。

确保用户已登录并已获取正确的令牌。然后,我们可以使用http://localhost:5000/characterskill带有 HTTP 方法 的URL POST。调用主体由characterId和组成skillId



{
    "characterid" : 5,
    "skillid" : 1
}


Enter fullscreen mode Exit fullscreen mode

执行此调用后,我们将获得完整的 RPG 角色及其武器和新技能。



{
    "data": {
        "id": 5,
        "name": "Frodo",
        "hitPoints": 200,
        "strength": 10,
        "defense": 10,
        "intelligence": 10,
        "class": 1,
        "weapon": {
            "name": "The Master Sword",
            "damage": 10
        },
        "skills": [
            {
                "name": "Fireball",
                "damage": 30
            }
        ]
    },
    "success": true,
    "message": null
}


Enter fullscreen mode Exit fullscreen mode

当我们添加另一项技能时,我们会看到完整的技能阵列。



{
    "data": {
        "id": 5,
        "name": "Frodo",
        "hitPoints": 200,
        "strength": 10,
        "defense": 10,
        "intelligence": 10,
        "class": 1,
        "weapon": {
            "name": "The Master Sword",
            "damage": 10
        },
        "skills": [
            {
                "name": "Fireball",
                "damage": 30
            },
            {
                "name": "Frenzy",
                "damage": 20
            }
        ]
    },
    "success": true,
    "message": null
}


Enter fullscreen mode Exit fullscreen mode

在数据库中,您还可以看到连接表已填充新的 ID。

角色技能表

完美!RPG角色装备了武器和技能。

请随意尝试一下。

如果您想使用GetCharacterById()中的方法CharacterService查看任何角色的装备,请确保添加Include()如前所示的方法,这意味着包括Weapon以及SkillsCharacterSkills



public async Task<ServiceResponse<GetCharacterDto>> GetCharacterById(int id)
{
    ServiceResponse<GetCharacterDto> serviceResponse = new ServiceResponse<GetCharacterDto>();
    Character dbCharacter = 
        await _context.Characters
        .Include(c => c.Weapon)
        .Include(c => c.CharacterSkills).ThenInclude(cs => cs.Skill)
        .FirstOrDefaultAsync(c => c.Id == id && c.User.Id == GetUserId());
    serviceResponse.Data = _mapper.Map<GetCharacterDto>(dbCharacter);
    return serviceResponse;
}


Enter fullscreen mode Exit fullscreen mode

当你所有的 RPG 角色都设置好后,我想是时候战斗了!

概括

恭喜!您已在应用程序中实现了所有类型的关系。

但这还不是全部。

在本章中,首先,你学习了如何通过 Web 服务调用获取已验证的用户,并从数据库中获取基于该用户的正确数据。这样,你就能向每位用户展示她自己的角色。

之后,我们讨论了一对一的关系。一个RPG角色现在可以装备一件武器,而且只能装备这一件武器。

关于多对多关系,我们将技能添加到角色中,并添加必要的连接实体或表CharacterSkills。角色可以拥有多项技能,技能也可以拥有多个角色。

除此之外,您还创建了所有必要的服务和控制器来添加武器和技能,并且您学习了如何包含更深的嵌套实体以及如何使用 AutoMapper定义自定义映射。

在下一章中,我们将更进一步,实现让RPG角色相互战斗的功能。


这就是本系列教程的第 11 部分。希望对你有所帮助。想要收到下一部分的通知,只需在dev.to上关注我或订阅我的新闻通讯即可。你将第一时间知道。

下次再见!

小心。


接下来:.NET Core 3.1 不仅仅是 CRUD

图片由 cornecoba 在freepik.com上创建。


但请稍等,还有更多!

鏂囩珷鏉ユ簮锛�https://dev.to/_patrickgod/many-to-many-relationship-with-entity-framework-core-4059
PREV
为什么感恩能让你成为更好的程序员
NEXT
如何保持动力