在Entity Framework中创建不带循环引用的领域模型

15

我已经找到了一个可行的解决方案(使用DTO和AutoMapper),如下所示,但我更希望得到一份列出不同解决方法的答案,并给出示例。如果有这样的答案,我会将其标记为最佳答案。

我的实体模型中有从子实体到父实体的导航属性。我的项目一直运行良好。然后我开始使用AutoFixture进行单元测试,测试失败了,AutoFixture说我有一个循环引用。

现在,我意识到像这样的循环引用导航属性在Entity Framework中是可以的,但我发现了这篇文章(Use value of a parent property when creating a complex child in AutoFixture),其中AutoFixture的创建者Mark Seemann说:

“顺便说一句,我已经好几年没有编写具有循环引用的API了,因此完全可以避免那些父/子关系。”

所以,我想了解如何重构领域模型以避免子/父关系。

以下是相关的实体类、存储库方法以及我在View中使用导致循环引用���属性。 完美的答案将解释我可以选择的不同选项,并提供示例,以及每种方法的基本优缺点。

注意:导致循环引用的属性是UserTeam模型中的User。

Models:

public class UserProfile
{
    public UserProfile()
    {
        UserTeams = new HashSet<UserTeam>();
        Games = new HashSet<Game>();
    }

    [Key]
    [DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)]
    public int UserId { get; set; }
    public string UserName { get; set; }       

    public virtual ICollection<UserTeam> UserTeams { get; set; }
    public virtual ICollection<Game> Games { get; set; }
}


public class Game
{
    public Game()
    {
        UserTeams = new HashSet<UserTeam>();
    }

    public int Id { get; set; }
    public int CreatorId { get; set; }

    public virtual ICollection<UserTeam> UserTeams { get; set; }
}


public class UserTeam
{
    public UserTeam()
    {
        UserTeam_Players = new HashSet<UserTeam_Player>();
    }

    public int Id { get; set; }
    public int UserId { get; set; }
    public int GameId { get; set; }

    public virtual UserProfile User { get; set; }
    public virtual ICollection<UserTeam_Player> UserTeam_Players { get; set; }
}

仓库模式

public IEnumerable<Game> GetAllGames()
    {
        using (DataContext)
        {             
            var _games = DataContext.Games
                 .Include(x => x.UserTeams)
                 .Include(x => x.UserTeams.Select(y => y.User))
                 .ToList();
            if (_games == null)
            {
                // log error
                return null;
            }
            return _games;
        }
    }

查看

@model IEnumerable<Game>
@foreach (var item in Model){
    foreach (var userteam in item.UserTeams){
        <p>@userteam.User.UserName</p>
    }
}

现在,如果我移除 'User' 导航属性,我将无法执行 '@userteam.User.UserName'。

那么,如何重构领域模型以删除循环引用,同时能够轻松地遍历游戏,并执行类似 UserTeam.User.Username 的操作?


2
我认为在这里需要一些背景信息。当我说“我已经多年没有编写带有循环引用的 API”时,我忘记说的是我多年没有使用过 ORM(例如 Entity Framework)了。 - Mark Seemann
2
@MarkSeemann - 啊。 :) - jag
1
值得一提的是,在任何ORM中拥有双向导航属性都违反了聚合根模式。虽然没有人说你必须使用聚合根,但领域驱动设计解释了什么是聚合根以及为什么你应该关心它。简而言之,使用双向导航属性时,不清楚哪个数据结构是相关数据的权威所有者。 - Mark Seemann
@MarkSeemann "...我已经多年没有使用ORM(如[EF])了..." - 对于我们好奇的以微软为中心的人来说,你一直在使用哪些绕过这些问题的东西? - Jeff
@Lumirris 文件/二进制大对象,主要是... 如果我必须与关系型数据库交互,那就使用Simple.Data。 - Mark Seemann
3个回答

6

我之前遇到过一个与AutoFixture和EntityFramework相关的类似问题。我的解决方案是向AutoFixture添加一个扩展,允许您使用几个递归来构建SUT。这个扩展最近已被AutoFixture采纳。

但是我理解你的问题并不是关于如何使AutoFixture构造递归数据结构,这确实是可能的,而是关于如何创建没有递归的领域模型。

首先,你有树形或图形结构。在这里,除了递归之外,任何东西都意味着通过松散耦合的节点ID进行间接引用。您需要逐个查询遍历整个树,或者缓存整个树并通过节点键查找遍历,这取决于树的大小,这可能是不切实际的。在这种情况下,让EF为您完成工作非常方便。

另一种常见的结构是类似于用户/游戏场景的双向导航结构。在这种情况下,将导航流程简化为单个方向通常不会有太大的不便。如果省略其中一个方向(例如从游戏到团队),仍然可以轻松查询给定游戏的所有团队。因此:用户具有游戏列表和团队列表。团队具有游戏列表。游戏没有导航参考。要获取特定游戏的所有用户,您可以编写以下内容:
var users = (from user in DataContext.Users
            from game in user.Games
            where game.Name == 'Chess'
            select user).Distinct()

感谢@Holstebroe的回答。是否有一种“最佳实践”方法,还是一种“千条万绪,归根结底,只要结果正确”的情况,你只需要选择其中一种并使用它?就极简代码而言,坚持使用递归引用并使用您的AutoFixture扩展方法似乎是最佳选择,但也许这不太符合纯粹主义者的口味? - jag
肯定有好的和不好的方法来解决问题,但也有不同的问题需要解决。有时你必须处理遗留系统,有时你会面临性能问题,有时你会受到Entity Framework或类似框架模型的限制。 - Holstebroe
在你的情况下,你的游戏/玩家模型不太适合扩展。比如说你有一个像《魔兽世界》那样大的游戏,你不希望游戏拥有所有团队的集合。在使用 EF 时,我会小心地使用导航属性。更喜欢“具有”而不是“与之关联”。这意味着“具有”依赖性不能独立存在。一个游戏可以独立存在,没有用户或团队。 - Holstebroe

1
我找到了一个解决方案(使用DTO和AutoMapper),该方案如下,但我仍然希望有一个列出不同方法的答案,并且提供示例,特别是这是否是一个理想的解决方案,或者我应该坚持原来的导航属性,摆脱AutoFixture,并在进行json序列化时只是利用其他解决方法(如属性等)...

因此,在我的视图模型中,我添加了几个类:

public class GameDTO
{
    public int Id { get; set; }
    public int CreatorId { get; set; }

    public ICollection<UserTeamDTO> UserTeamsDTO { get; set; }
}

public class UserTeamDTO : UserTeam
{
    public UserProfile User { get; set; }
}

在我的控制器中,我使用AutoMapper将存储库中的Game / UserTeam对象映射到我的DTO对象,并返回IList _gamesDto到视图。
var _games = _gameRepository.GetAllGames();

IList<GameDTO> _gamesDto = new List<GameDTO>();
IList<UserTeamDTO> _userteamsDto = new List<UserTeamDTO>();
GameDTO _gameDto = new GameDTO();
UserTeamDTO _userteamDto = new UserTeamDTO();
Mapper.CreateMap<Game, GameDTO>();
Mapper.CreateMap<UserTeam, UserTeamDTO>();

foreach (Game _game in _games)
{
    foreach (UserTeam _userteam in _game.UserTeams)
    {
        _userteamDto = Mapper.Map<UserTeamDTO>(_userteam);
        _userteamDto.User = _userRepository.GetUser(_userteam.UserId);
        _userteamsDto.Add(_userteamDto);
    }

    _gameDto = Mapper.Map<GameDTO>(_game);
    _gameDto.UserTeamsDTO = _userteamsDto;
    _gamesDto.Add(_gameDto);
}

1
我最近也遇到了类似的问题,这也影响了序列化JSON对象。我决定从我的数据模型中删除循环引用。
首先,我删除了创建循环引用的多余导航属性。我确保我的数据树有意义。这使我清楚了解哪些对象拥有哪些关系。
这也使EF无法自动推断我的关系。我不得不使用FluentAPI指定一对多和多对多的关系。我在这里找到了一个解决方案:https://dev59.com/8WQn5IYBdhLWcg3wmH8s#16719203 希望这有所帮助。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接