Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
239 views
in Technique[技术] by (71.8m points)

c# - How to map recursive relation on self in Entity Framework code-first approach

All I want to create is basic recursive category. Category is root if RootCategory_Id is set to null and it belongs to some other category if it is set to some id. I've added category with two child-categories in Seed() method to test and it does not work. (I've checked DB afterwards, there are inserted)

Category model

public class Category
{
    public int ID { get; set; }
    public Category RootCategory { get; set; } // This one works good, it also creates "RootCategory_Id" in database on "update-database"

    public ICollection<Category> ChildCategories { get; set; } // This is always null, how to map it correctly?

    public string Name { get; set; }
}

Seed method

protected override void Seed(Test.Infrastructure.TestDataContext context)
{
    context.Categories.Add(new Category() {
        Name = "First Category", ChildCategories = new List<Category>() {
            new Category(){
                Name = "Second Category"
            },
            new Category(){
                Name = "Third Category"
            }
        }
    });

    context.SaveChanges();
}

This is how I tested that it does not work

public ActionResult Test()
{
    // After checking DB my root is 4, and two categories that have RootCategory_Id set to 4
    var c = _db.Categories.Where(x => x.ID == 4).Single();
    return Content(c.ChildCategories.FirstOrDefault().Name); // Always returns null, even c.ChildCategories.Count() returns 'null'
}

Picture of what I want to achieve

this was generated from database-first approach using linq-to-sql
enter image description here

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

I didn't really want to necro-thread here, but I've been searching for methods on how to solve this exact problem.

Here's my solution; it's a bit long winded, but it allows a much more scalable approach to Code First programming. It also introduces the Strategy pattern to allow for SoC while remaining as POCO as possible.

Step 1: Create the entity primitives and interfaces.

IEntity Interface:

/// <summary>
///     Represents an entity used with Entity Framework Code First.
/// </summary>
public interface IEntity
{
    /// <summary>
    ///     Gets or sets the identifier.
    /// </summary>
    /// <value>
    ///     The identifier.
    /// </value>
    int Id { get; set; }
}

IRecursiveEntity Interface:

/// <summary>
///     Represents a recursively hierarchical Entity.
/// </summary>
/// <typeparam name="TEntity"></typeparam>
public interface IRecursiveEntity <TEntity> where TEntity : IEntity
{
    /// <summary>
    ///     Gets or sets the parent item.
    /// </summary>
    /// <value>
    ///     The parent item.
    /// </value>
    TEntity Parent { get; set; }

    /// <summary>
    ///     Gets or sets the child items.
    /// </summary>
    /// <value>
    ///     The child items.
    /// </value>
    ICollection<TEntity> Children { get; set; }
}

Entity Abstract Class:

/// <summary>
///     Acts as a base class for all entities used with Entity Framework Code First.
/// </summary>
public abstract class Entity : IEntity
{
    /// <summary>
    ///     Gets or sets the identifier.
    /// </summary>
    /// <value>
    ///     The identifier.
    /// </value>
    public int Id { get; set; }
}

RecursiveEntity Abstract Class:

/// <summary>
///     Acts as a base class for all recursively hierarchical entities.
/// </summary>
/// <typeparam name="TEntity">The type of the entity.</typeparam>
public abstract class RecursiveEntity<TEntity> : Entity, IRecursiveEntity<TEntity> 
    where TEntity : RecursiveEntity<TEntity>
{
    #region Implementation of IRecursive<TEntity>

    /// <summary>
    ///     Gets or sets the parent item.
    /// </summary>
    /// <value>
    ///     The parent item.
    /// </value>
    public virtual TEntity Parent { get; set; }

    /// <summary>
    ///     Gets or sets the child items.
    /// </summary>
    /// <value>
    ///     The child items.
    /// </value>
    public virtual ICollection<TEntity> Children { get; set; }

    #endregion
}

Note: Some people have suggested edits to this post regarding this class. The class needs to only accept RecursiveEntity<TEntity> as a constraint, rather than IEntity so that it is constrained to only handle recursive entities. This helps alleviate type mismatch exceptions. If you use IEntity instead, you will need to add some exception handling to counter mismatched base types.

Using IEntity will give perfectly valid code, but it will not work as expected under all circumstances. Using the topmost root available, isn't always the best practice, and in this case, we need to constrain further down the inheritence tree than at that root level. I hope that makes sense. It is something I played around with at first, but I had huge problems when populating the database; especially during Entity Framework Migrations, where you have no fine grained debugging control.

During testing, it also didn't seem to play nice with IRecursiveEntity<TEntity> either. I may return to this soon, as I'm refreshing an old project that uses it, but the way it is written here is fully valid and working, and I remember tweaking it until it worked as expected. I think there was a trade off between code elegance and inheritance heirarchy, where using a higher level class meant casting a lot of properties between IEntity and IRecursiveEntity<IEntity>, which in turn, gave a lower performance and looked ugly.

Step 2: Derive the RecursiveEntity.

I've used the example from the original question...

Category Concrete Class:

public class Category : RecursiveEntity<Category>
{
    /// <summary>
    ///     Gets or sets the name of the category.
    /// </summary>
    /// <value>
    ///     The name of the category.
    /// </value>
    public string Name { get; set; }
}

I've stripped out everything from the class apart from non-derived properties. The Category derives all its other properties from its self-associated generic inheritance of the RecursiveEntity class.

Step 3: Extension Methods (Optional).

To make the whole thing more managable, I've added some extension methods to easily add new children to any parent item. The difficulty, I found, was that you need to set both ends of the one-to-many relationship and merely adding the child to the list didn't allow you to handle them in the way they are intended. It's a simple fix that saves a huge amount of time in the long run.

RecursiveEntityEx Static Class:

/// <summary>
///     Adds functionality to all entities derived from the RecursiveEntity base class.
/// </summary>
public static class RecursiveEntityEx
{
    /// <summary>
    ///     Adds a new child Entity to a parent Entity.
    /// </summary>
    /// <typeparam name="TEntity">The type of recursive entity to associate with.</typeparam>
    /// <param name="parent">The parent.</param>
    /// <param name="child">The child.</param>
    /// <returns>The parent Entity.</returns>
    public static TEntity AddChild<TEntity>(this TEntity parent, TEntity child)
        where TEntity : RecursiveEntity<TEntity>
    {
        child.Parent = parent;
        if (parent.Children == null)
            parent.Children = new HashSet<TEntity>();
        parent.Children.Add(child);
        return parent;
    }

    /// <summary>
    ///     Adds child Entities to a parent Entity.
    /// </summary>
    /// <typeparam name="TEntity">The type of recursive entity to associate with.</typeparam>
    /// <param name="parent">The parent.</param>
    /// <param name="children">The children.</param>
    /// <returns>The parent Entity.</returns>
    public static TEntity AddChildren<TEntity>(this TEntity parent, IEnumerable<TEntity> children)
        where TEntity : RecursiveEntity<TEntity>
    {
        children.ToList().ForEach(c => parent.AddChild(c));
        return parent;
    }
}

Once you have all of that in place, you can seed it thusly:

Seed Method

/// <summary>
///     Seeds the specified context.
/// </summary>
/// <param name="context">The context.</param>
protected override void Seed(Test.Infrastructure.TestDataContext context)
{
    // Generate the root element.
    var root = new Category { Name = "First Category" };

    // Add a set of children to the root element.
    root.AddChildren(new HashSet<Category>
    {
        new Category { Name = "Second Category" },
        new Category { Name = "Third Category" }
    });

    // Add a single child to the root element.
    root.AddChild(new Category { Name = "Fourth Category" });

    // Add the root element to the context. Child elements will be saved as well.
    context.Categories.AddOrUpdate(cat => cat.Name, root);

    // Run the generic seeding method.
    base.Seed(context);
}

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...