Creating a domain model without circular references in Entity Framework
Asked Answered
C

3

15

I have found a solution that works (using DTOs and AutoMapper), which is reproduced below, but I would prefer an answer that lists the different approaches to the problem with examples and this will be marked as the answer if received.

In my entity model I have a navigation property that goes from a child entity to the parent entity. My project was working swimmingly. Then I began to use AutoFixture for unit testing, and testing failed, AutoFixture saying I had a circular reference.

Now, I realise that circular reference navigation properties like this are OK within Entity Framework, but I found this post (Use value of a parent property when creating a complex child in AutoFixture), where Mark Seemann, the creator of AutoFixture states:

"For the record, I haven't written an API with a circular reference for years, so it's quite possible to avoid those Parent/Child relations."

So, I want to understand HOW a domain model can be refactored to avoid child/parent relations.

Below are the entity classes in question, the repository method, and how I use the property causing the circular reference in my View. The perfect answer would explain the different options I could choose from with examples, and the basic pros/cons of each approach.

Note: The property causing the circular reference is User, in the UserTeam model.

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; }
}

Repository Method

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;
        }
    }

View

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

Now, if I remove the 'User' navigation property, I wouldn't be able to do '@userteam.User.UserName'

So, how do I refactor the domain model to remove the circular reference, whilst being able to easily loop through Games, and do something like UserTeam.User.Username?

Combustor answered 7/11, 2013 at 15:56 Comment(5)
I think a bit of context is in order here. When I stated that "I haven't written an API with a circular reference for years", what I forgot to say is that I also haven't used an ORM (like Entity Framework) for years.Accountable
@MarkSeemann - Ah. :)Combustor
FWIW, having two-way navigation properties in any ORM violates the Aggregate Root pattern. While no-one says that you have to use Aggregate Roots, Domain-Driven Design explains both what an Aggregate Root is, and why you should care. In short, with two-way navigation properties, it's unclear which data structure is the authoritative owner of associated data.Accountable
@MarkSeemann "...I also haven't used an ORM (like [EF]) for years..." - for us curious Microsoft-centric folks, what have you been using that sidesteps these issues?Spinner
@Lumirris Files/blobs, mostly... And Simple.Data if I must talk to a RDBMS.Accountable
S
6

I had a similar problem with AutoFixture and EntityFramework a while ago. My solution was to add an extension to AutoFixture, that allows you to build a SUT with a few recursions. That extension has recently been adopted in AutoFixture.

But I understand that your question was not about how to make AutoFixture construct recursive data structures, which is indeed possible, but how to create domain models without recursion.

First, you have tree or graph structures. Here anything but recursion would mean indirection through loose coupled node ids. Instead of defining an association, you would have to traverse the tree query-by-query or cache the whole thing and traverse by node-key lookup, which may be impractical depending on the tree-size. Here it is very convenient to make EF do the work for you.

The other common structure is a two-way navigational structure similar to your user / game scenario. Here it is often not that inconvenient to prune the navigation flow to a single direction. If you omit one direction, say from game to team, you can still easily query all teams for a given game. So: User has a list of games and a list of teams. Team has a list of games. Games have no navigational reference to either. To get all users for a specific game you could write something like:

var users = (from user in DataContext.Users
            from game in user.Games
            where game.Name == 'Chess'
            select user).Distinct()
Sluiter answered 11/11, 2013 at 9:17 Comment(3)
Thanks for your answer @Holstebroe. Is there a 'best practice' approach, or is it a kind of 'many ways to skin a cat' situation, and you just pick one and go with it? In terms of minimalist code, sticking with the recursive references and using your AutoFixture extension approach seems to be the best option, but perhaps this doesn't sit well with the purists?Combustor
There are surely good ways and bad ways to skin a cat, but there are also different cats. Sometimes you have to work with legacy systems, sometimes you have performance concerns, sometimes you are restricted by how Entity Framework or similar prefers models.Sluiter
In your case your game / player model does not scale well. Say you have World of Warcraft sized game. You wouldn't like Game to have a collection of all teams. When using EF I try to be careful about using navigational properties. Prefer for "Has a" rather than "is associated with". Meaning that the "has a" dependency can't stand alone. A game can stand alone without a user or a team.Sluiter
C
1

I have found a solution that works (using DTOs and AutoMapper), which is reproduced below, but I would still prefer an answer that lists the different approaches to the problem with examples, in particular whether this is a desirable solution, or whether I should stick with the navigation properties as they were, get rid of AutoFixture, and when it comes to serializing for json just utilise other work arounds (attributes etc)...

So, in my View Model, I added a couple of classes:

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; }
}

And in my controller, I use AutoMapper to map the Game / UserTeam objects from the repository to my DTO objects, and return the IList _gamesDto to the View.

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);
}
Combustor answered 7/11, 2013 at 17:37 Comment(0)
D
1

I had a similar problem recently which also impacted serializing JSON objects. I decided to remove the circular references from my data model.

I first removed the redundant navigation properties which were creating the circular references. I made sure that my resulting tree of data made sense. This allowed me to make it clear which objects own which relationships.

This also made EF unable to automatically reason about my relationships. I had to specify the One-to-Many and Many-to-Many relationships using the FluentAPI. I found a solution here: https://mcmap.net/q/759876/-map-many-to-many-relationship-without-navigation-property

Hope this is helpful.

Daugava answered 24/3, 2015 at 15:35 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.