Many-to-Many Relationship Done Right in the Entity Framework: Multi-Clients & Users

Tika Pahadi
Aug 8, 2018 · 3 min read

I ❤ clean code. I even love it more when things can be simplified and the level of exposure (encapsulation) is limited to exactly what is needed.

In a real-world scenario, a client has many users. A user also may belong to multiple clients. This fundamental many-to-many relationship could look something below. The UserClient is a necessary map but it’s exposure is completely unnecessary. More importantly, in the API (or Controller), I do not want to see it- and I do not have to know it.

public class User
{
public int Id { get; set; }
public string Email { get; set; }

... other user properties/members
public ICollection<UserClient> UserClients { get; set; }
}

public class Client
{
public int Id { get; set; }
public string Name { get; set; }
... other client properties/members public ICollection<UserClient> UserClients { get; set; }
}
// Oh Database God, this one.
public class UserClient
{
public int Id { get; set; }
public int ClientId { get; set; }
public User User { get; set; }
public int UserId { get; set; }
public Client Client { get; set; }
}

And, GET user/1:

Notice how difficult it becomes to navigate to the clients. And adding a new client to the user is a nightmare (Yuk! Squared).

{
"Id": 1,
"Email": "user@example.com"
"UserClients": [
{
Id: 1,
ClientId: 1,
UserId: 1,
Client: {
"Id": 1,
"Name": "First Client"
}
}
]
}

The problem with this design is that not only it exposes the relationship, it becomes very difficult to add the relationship.

So, let’s add a Clients property in our User class to read/apply the relationship: The get method fetches the Clients from the relationship whereas the set method creates a Client/User relationship (map).

public IEnumerable<Client> Clients
{
get => UserClients.Select(r => r.Client);
set => UserClients = value.Select(v => new UserClient()
{
ClientId = v.Id
}).ToList();
}

We also have to make the Clients [NotMapped]. ClientUsers is set to [JsonIgnore] so that the API is not exposed to the Json Serializer. One last more thing is to change the behavior of the Serializer to apply Replace object rather than automatic handling. It can be achieved:

[JsonProperty(ObjectCreationHandling  = ObjectCreationHandling.Replace)]

So, our final user class looks like: (Client is also very analogous)

public class User
{
public int Id { get; set; }
public string Email { get; set; }

[JsonIgnore]
private ICollection<UserClient> UserClients { get; set; }

[NotMapped]
[JsonProperty(
ObjectCreationHandling = ObjectCreationHandling.Replace
)]
public IEnumerable<Client> Clients
{
get => UserClients.Select(r => r.Client);
set => UserClients = value.Select(v => new UserClient()
{
ClientId = v.Id
}).ToList();
}
}

One last thing….

You might want to update the DbContext to make sure the foreign key relationships are set properly.

protected override void OnModelCreating(ModelBuilder modelBuilder) {
modelBuilder.Entity<UserClient>()
.HasKey(t => new { t.UserId, t.ClientId })
.Property(e => e.Id).ValueGeneratedOnAdd();
}

Results (JSON over API):

— — — — — — — — — — — — — —

POST: users (Create a new User: Assume Client 1 Exists):

{
"Email": "user@example.com"
"Clients": [
{
"Id": 1
}
]
}

GET: users/1

{
"Id": 1,
"Email": "user@example.com"
"Clients": [
{
"Id": 1,
"Name": "First Client"
}
.... more clients of the user in the array
]
}

GET: Clients/1

{
"Id": 1,
"Name": "Client 1"
"Users": [
{
"Id": 1,
"Email": "user@example.com"
}
... more users of the client
]
}

Clean, right?

Gotcha:

While you are doing a query, make sure to include the relationship properly, otherwise the results will be null and therefore fail: i.e. while fetching users:

var users = return _db.Users
.Include(x => x.UserClients) // Join Relationship
.ThenInclude(x => x.Clients) // Join Clients

Happy Coding!

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch

Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore

Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store