Workaround for missing n:m mapping feature in Entity Framework Core
A lot of guys are complaining on GitHub that implicit mapping for many-to-many relationships is missing for Entity Framework Core.
I don't see this missing feature as too critical. With some clever patterns you can implement workarounds without affecting your business domain objects too much.
I'll try to explain how I solved this.
Disclaimer: I didn't test this particular example, so I don't know if it compiles etc., but I hope you get my idea. You can use this code how you want, I release it here under the MIT License.
My domain model
I have a very basic domain object model. It's (almost) completely free of aspects which are important for ORMs. This are the classes which are used by my application logic.
For example it doesn't contain properties for the Id properties, Ids for navigation properties and no join entity collections.
Example:
public class Post
{
public Post()
{
}
public string Title { get; set; }
public ICollection<Tag> Tags { get; protected set; }
public virtual Person Author { get; set; }
}
Extended classes
For the use with the entity framework, I inherit from Post (an all other classes) and create a join entity class in a separate assembly.
Because this is a lot work if your model is complex, I wrote a T4 template which does all the magic.
Example:
[Table("Post")]
internal class PostEf : Post
{
public Post()
{
this.Tags = new new ManyToManyCollectionAdapter<TagEf, PostTag>(this.JoinedPostTags, joinEntity => joinEntity.Tag, entity => new PostTag { Post = this, PostId = this.Id, Tag = (TagEf)entity, TagId = ((TagEf)entity).Id});
}
public int Id { get; set; }
public ICollection<PostTag> JoinedPostTags { get; } = new List<PostTag>();
public int? AuthorId { get; set; }
[ForeignKey("AuthorId")]
public AuthorEf RawAuthor
{
get { return base.Author as AuthorEf; }
set { base.Author = value; }
}
[NotMapped]
public override Person Author
{
get { return base.Author; }
set
{
base.Author = value;
this.AutorId = this.RawAuthor?.Id;
}
}
}
internal partial class PostTag
{
public int PostId { get; set; }
public PostEf Post { get; set; }
public int TagId { get; set; }
public TagEf Tag { get; set; }
}
n:m Collection Adapter
As you can see, I use a class which is called ManyToManyCollectionAdapter:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
/// <summary>
/// A many-to-many collection adapter which adapts beween <typeparamref name="T"/> and <typeparamref name="TJoin"/>.
/// </summary>
/// <remarks>
/// Usually in our object model we don't define collections of join entities and their types.
/// This is done automatically by our T4 templates.
/// </remarks>
/// <typeparam name="T">The type which is in a many-to-many relationship.</typeparam>
/// <typeparam name="TJoin">The type of the join entity. It contains a property of <typeparamref name="T"/>.</typeparam>
internal class ManyToManyCollectionAdapter<T, TJoin> : ICollection<T>
{
/// <summary>
/// The raw collection, which is usually the collection which is mapped by entity framework.
/// </summary>
private readonly ICollection<TJoin> rawCollection;
/// <summary>
/// The function to create a new join entity.
/// </summary>
private readonly Func<T, TJoin> createJoinEntityFunction;
/// <summary>
/// The function to extract the instance of <typeparamref name="T"/> out of <typeparamref name="TJoin"/>.
/// </summary>
private readonly Func<TJoin, T> extractFunction;
/// <summary>
/// Initializes a new instance of the <see cref="ManyToManyCollectionAdapter{T, TJoin}"/> class.
/// </summary>
/// <param name="rawCollection">The raw collection, which is usually the collection which is mapped by entity framework.</param>
/// <param name="extractFunction">The function to extract the instance of <typeparamref name="T"/> out of <typeparamref name="TJoin"/>.</param>
/// <param name="createJoinEntityFunction">The function to create a new join entity.</param>
public ManyToManyCollectionAdapter(ICollection<TJoin> rawCollection, Func<TJoin, T> extractFunction, Func<T, TJoin> createJoinEntityFunction)
{
this.rawCollection = rawCollection;
this.createJoinEntityFunction = createJoinEntityFunction;
this.extractFunction = extractFunction;
}
/// <inheritdoc />
public int Count => this.rawCollection.Count;
/// <inheritdoc/>
public bool IsReadOnly => this.rawCollection.IsReadOnly;
/// <inheritdoc/>
public IEnumerator<T> GetEnumerator()
{
return this.rawCollection.Select(this.extractFunction).GetEnumerator();
}
/// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator()
{
return this.GetEnumerator();
}
/// <inheritdoc/>
public void Add(T item)
{
this.rawCollection.Add(this.createJoinEntityFunction(item));
}
/// <inheritdoc/>
public void Clear()
{
this.rawCollection.Clear();
}
/// <inheritdoc/>
public bool Contains(T item)
{
return this.rawCollection.Any(i => object.Equals(this.extractFunction(i), item));
}
/// <inheritdoc/>
public void CopyTo(T[] array, int arrayIndex)
{
this.rawCollection.Select(this.extractFunction).ToList().CopyTo(array, arrayIndex);
}
/// <inheritdoc/>
public bool Remove(T item)
{
var joinItem = this.rawCollection.FirstOrDefault(i => object.Equals(this.extractFunction(i), item));
if (joinItem != null)
{
return this.rawCollection.Remove(joinItem);
}
return false;
}
}
I have a collection adapter for 1:n relationships, too (CollectionAdapter<TClass, TEfCore> : ICollection<TClass>
). It's similar and a bit simpler than this one.
Adapting your DbContext
As you saw, I create inherited classes to work with the entity framework. To ensure that the entity framework does not work with the base classes, you have to ignore these.
Additionally you have to define the many-to-many relationships, too. I'm generating this with a T4 template as well.
Example:
public class ExtendedTypeContext : Microsoft.EntityFrameworkCore.DbContext
{
/// <inheritdoc/>
protected override void OnModelCreating(Microsoft.EntityFrameworkCore.ModelBuilder modelBuilder)
{
modelBuilder.Ignore<Post>();
modelBuilder.Ignore<Tag>();
modelBuilder.Ignore<Person>();
modelBuilder.Entity<PostEf>().HasMany(entity => entity.JoinedPostTags).WithOne(join => join.PostEf);
modelBuilder.Entity<PostTag>().HasKey(join => new { join.PostId, join.TagId });
}
}
Creating new objects in your application
Because your application shouldn't know anything about your entity framework classes and just uses your very basic domain model,
you have to make sure somehow that you create entity framework classes without actually knowing their type.
One solution is to set up and configure a IoC/DI container. Every time you need to create a new object, you write something like
"container.Create()" instead of "new Post()". "container.Create()" would return a new instance of PostEf in this case.
Final notes
I hope I helped some guys which face the same problems with the Entity Framework Core.
I think my approach with T4 templates is flexible enough to even implement lazy-loading or INotifyPropertyChanged into the extended "Ef" classes. I'm not an expert for EF6, but I think it does the same by creating such types in-memory.
A note on naming: I only added the "Ef"-prefix here for a better understanding. In my actual code I removed them, because these classes are in different assemblies and namespaces.
If you have questions, don't hesitate to ask :)