Hello there,

I'm considering using DataObjets 4.3.5 on a project and I need an ordered set.

I don't want to enumerate my entities directly as they may be included in multiple sets. Also I want the ordered set to work with PairTo (e.g., [Association(PairTo = "Parent"])

I've been looking around on the forum and have not found any soloution to this. I can't figure out a way to extend EntitySet for this, but I guess it is possible.

Do you have a suggestion on how to solve this problem?

Best regards, Rune

The solution below is our attempt, but it does not support the PairTo feature (and I guess alot of other goodies from EntitySet are lost).

using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Xtensive.Storage;

namespace Foo.DataModel.Lists
{
    /// <summary>
    /// Base class for DO.Net lists
    /// </summary>
    /// <typeparam name="TItem">The type of the item.</typeparam>
    public abstract class List<TItem> : Entity, ICollection<TItem>
        where TItem : IEntity
    {
        #region Instance Properties

        /// <summary>
        /// Gets the sorted items.
        /// </summary>
        /// <value>The sorted items.</value>
        public System.Collections.Generic.List<TItem> SortedItems
        {
            get
            {
                return Items.OrderBy(i => i.Index).Select(i => i.Entity).ToList();
            }
        }

        /// <summary>
        /// Gets or sets the items.
        /// </summary>
        /// <value>The items.</value>
        [Field]
        protected EntitySet<ItemUsage<TItem>> Items
        {
            get;
            private set;
        }

        #endregion

        #region Instance Methods

        /// <summary>
        /// Gets the first item from sorted items
        /// </summary>
        /// <returns></returns>
        public TItem FirstItem()
        {
            return SortedItems.Count > 0
                ? SortedItems[0]
                : default(TItem);
        }

        /// <summary>
        /// Gets the item by its index in the list instance.
        /// </summary>
        /// <param name="index">The index.</param>
        /// <returns></returns>
        public TItem GetByIndex(int index)
        {
            ItemUsage<TItem> item = Items.FirstOrDefault(i => i.Index == index);
            return item != null ? item.Entity : default(TItem);
        }

        /// <summary>
        /// Inserts the specified item.
        /// </summary>
        /// <param name="item">The item.</param>
        /// <param name="index">The index.</param>
        [Transactional]
        public void Insert(TItem item, int index)
        {
            Items.Where(i => i.Index >= index).ToList().ForEach(i => i.Index++);
            Items.Add(new ItemUsage<TItem>
                {
                    Entity = item,
                    Index = index
                });
        }

        /// <summary>
        /// Inserts items at first position.
        /// </summary>
        /// <param name="item">The item.</param>
        public void InsertFirst(TItem item)
        {
            Insert(item, 0);
        }

        /// <summary>
        /// Inserts items at last position.
        /// </summary>
        /// <param name="item">The item.</param>
        public void InsertLast(TItem item)
        {
            Insert(item, (int) Items.Count);
        }

        /// <summary>
        /// Gets the first item from sorted items
        /// </summary>
        /// <returns></returns>
        public TItem LastItem()
        {
            return SortedItems.Count > 0
                ? SortedItems[SortedItems.Count - 1]
                : default(TItem);
        }

        #endregion

        #region ICollection<TItem> Members

        /// <summary>
        /// Adds the specified item.
        /// </summary>
        /// <param name="item">The item.</param>
        public void Add(TItem item)
        {
            Items.Add(new ItemUsage<TItem>
                {
                    Entity = item, Index = (int) Items.Count
                });
        }

        /// <summary>
        /// Clears this instance.
        /// </summary>
        public void Clear()
        {
            Items.Clear();
        }

        /// <summary>
        /// Determines whether List instance contains the specified list.
        /// </summary>
        /// <param name="item">The item.</param>
        /// <returns>
        ///     <c>true</c> if list contains item; otherwise, <c>false</c>.
        /// </returns>
        public bool Contains(TItem item)
        {
            return Items.Any(i => i.Entity.Equals(item));
        }

        /// <summary>
        /// Copies the array of objects to list
        /// </summary>
        /// <param name="array">The array.</param>
        /// <param name="startIndex">The start index.</param>
        [Transactional]
        public void CopyTo(TItem[] array, int startIndex)
        {
            int index = startIndex;
            foreach (var item in array)
            {
                Insert(item, index);
                index++;
            }
        }

        /// <summary>
        /// Gets the count of items.
        /// </summary>
        /// <value>The count.</value>
        public int Count
        {
            get
            {
                return (int) Items.Count;
            }
        }

        /// <summary>
        /// Gets a value indicating whether this instance is read only.
        /// </summary>
        /// <value>
        ///     <c>true</c> if this instance is read only; otherwise, <c>false</c>.
        /// </value>
        public bool IsReadOnly
        {
            get
            {
                return Items.IsReadOnly;
            }
        }

        /// <summary>
        /// Removes the specified item.
        /// </summary>
        /// <param name="item">The item.</param>
        /// <returns></returns>
        [Transactional]
        public bool Remove(TItem item)
        {
            ItemUsage<TItem> itemUsage = Items.Where(iu => iu.Entity.Equals(item)).FirstOrDefault();
            int index = itemUsage.Index;
            bool result = Items.Remove(itemUsage);
            //if (!Items.Any( i => i.Index == index))
            //{
            //    Items.Where(i => i.Index > index).ToList().ForEach(i => i.Index--);
            //}

            return result;
        }

        /// <summary>
        /// Gets the enumerator.
        /// </summary>
        /// <returns></returns>
        public IEnumerator<TItem> GetEnumerator()
        {
            return SortedItems.GetEnumerator();
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return SortedItems.GetEnumerator();
        }

        #endregion
    }
}

namespace Foo.DataModel.Lists
{
    [HierarchyRoot]
    public class ItemUsage<TEntity> : Entity
    {
        [Field, Key]
        public int Id
        {
            get;
            private set;
        }

        [Field]
        public int Index
        {
            get;
            set;
        }

        [Field]
        public TEntity Entity
        {
            get;
            set;
        }
    }
}

asked Oct 20 '10 at 12:18

Rune%20H%C3%B8jsgaard's gravatar image

Rune Højsgaard
17114

edited Oct 22 '10 at 10:15

Alex%20Yakunin's gravatar image

Alex Yakunin
29714412


One Answer:

Sorry for delay - the question is actually a bit tricky.

The code you use should work, but it doesn't look as optimal:

  • Insertion & removal requires O(N) updates; any of such operations locks (or prevents concurrent updates of) lots of records.
  • The collection is always fully loaded (although normally that's ok - manually ordered collections are relatively small)
  • You use an additional entity (List<T>), that must be always fetched to access the collection
  • Really, you can't use this with pairing.

That was critics ;) On the other hand, it's not so easy to provide a general solution in this case that won't face at least some of these issues.

I recommend you to:

  • Avoid dealing with indexed sets at all. You might notice they're avoided in nearly any large scale database application. Use sorting instead of ordering by pre-defined index.
  • If you need custom ordering, but expects to see pretty large lists there (actually, a rare case), I'd prefer using string-based ordering. To move \ remove an item, you should normally do just one update and read just two records, that's a bit more clever. But when length of some index string will approach the limit, you'll need to update all the strings anyway.
  • Avoid using general solution in case you need it just once. Likely, you need to order the items just for menus - then add the API & ordering index just to that type.

I also can suggest more complex, but more efficient approach - here is the API I'd use:

public class EntityList<TOwner, TItem, TVariator> : 
  EntitySet<EntityListItem<TOwner, TItem, TVariator>>
{
  public EntityListAccessor<TOwner, TItem, TVariator> AsList { 
    get { return new EntityListAccessor<TOwner, TItem, TVariator>(this); }
  }

  protected EntityList(Entity owner, FieldInfo field)
    : base(owner, field)
  {}

  protected EntitySet(SerializationInfo info, StreamingContext context)
    : base(info, context)
  {}
}

[HierarchyRoot]
public sealed class EntityListItem<TOwner, TItem, TVariator> : Entity
{
  [Field, Key(0)]
  public TOwner Owner { get; private set; }

  [Field, Key(1)]
  public string Index { get; private set; }

  [Field, Association(OnTargetRemove = OnRemoveAction.Cascade)]
  public TItem Item { get; set; }

  internal EntityListItem(TOwner owner, string index TItem item)
    : base(owner, index)
  {
    Item = item;
  }
}

// Struct is a good option for such wrappers, because it 
// doesn't require any heap allocations, i.e. it uses just
// 4 or 8 bytes on stack.
public struct EntityListAccessor<TOwner, TItem, TVariator>
  : IEnumerable<KeyValuePair<string, TItem>>
{
  public EntityList<TOwner, TItem> EntitySet { get; private set; }

  public TItem this[string index] {
    get {
      // TODO: write the implementation by your own
    }
    set {
      // TODO: write the implementation by your own
    }
  }

  public KeyValuePair<string, TItem> this[int index] {
    get {
      // TODO: write the implementation by your own
    }
  }

  public void Add(TItem item)
  {
    // TODO: write the implementation by your own
  }

  public void Add(TItem item, int index)
  {
    // TODO: write the implementation by your own
  }

  public void Move(int oldIndex, int newIndex)
  {
    // TODO: write the implementation by your own
  }

  public void RemoveAt(int index)
  {
    // TODO: write the implementation by your own
  }

  public void Reindex()
  {
    // TODO: write the implementation by your own
  }

  IEnumerable<KeyValuePair<string, TItem>> GetEnumerator()
  {
    // Assuming count of items is small - otherwise
    // explicit order seems meaningless
    return (
      from item in entitySet.ToList()
      order by item.Index
      select item
      ).GetEnumerator(); 
  }

  IEnumerable IEnumerable.GetEnumerator()
  {
    return GetEnumerator();
  }

  internal EntityListAccessor(EntityList<TOwner, TItem> entitySet)
  {
    EntitySet = entitySet;
  }
}

Usage of these types should look like:

[HierarchyRoot]
public class Menu : Entity
{
  public sealed class _MenuItemsVariator { };
  [Field, Association(PairTo = "Owner")]
  EntityList<Menu, MenuItem, _MenuItemsVariator> Items { get; private set; }
}

...

var menu = ...;
var items = menu.Items.AsList;
items.Add(new MenuItem(...));
foreach (var item in items)
  Console.WriteLine("Item: {0}", item);
items.Remove(0);
items.Move(0,1);

This approach eliminates issues with too many updates and an additional entity.

  • Insertion & removal requires O(N) updates
  • An additional entity (List<T>), that must be always fetched to access the collection
  • You can try to extend this approach for pairing, but you must use different types there (I suspect, MasterEntityList<...>, SlaveEntityList<...>, MasterSlaveEntityListItem<TMaster, TSlave, TVariator>, MasterEntityListAccessor<...>, SlaveEntityListAccessor<...>).

But there are few inconvenient things as well:

  • You should always add [Field, Association(PairTo = "Owner")]
  • You need unique TVariator substitution to ensure there is a dedicated hierarchy for each generic instance of EntityListItem<TOwner, TItem, TVariator>. DO actually uses very similar approach for intermediate types in m-n links (they're generic instances of EntitySetItem<TOwner, TItem, TVariator> parameterized by variator type generated in Runtime), but currently I can't imagine how this could help here.

answered Oct 22 '10 at 10:12

Alex%20Yakunin's gravatar image

Alex Yakunin
29714412

edited Oct 22 '10 at 10:12

Your answer
Please start posting your answer anonymously - your answer will be saved within the current session and published after you log in or create a new account. Please try to give a substantial answer, for discussions, please use comments and please do remember to vote (after you log in)!
toggle preview

powered by OSQA