Monday, August 25, 2008

Delegate powered collection?

So, I'm playing around with some LINQ to SQL ideas. One of my ideas centers around the idea of trying to abstract one of my many-to-many relationships away from callers into the datacontext. To do this, I've created what I call a Delegate Powered Collection (better names are welcome).

It's really very simple - in place of method implementations, the ICollection members simply call a delegate passed in during the collection's initialization. The delegates are all Func and Action delegates, and so accept llambda functions as arguments. I haven't written a constructor (yet?) but I imagine it'd be pretty messy to do so.

Anyway, here's the code:

    1 public class DelegatePoweredCollection<T> : ICollection<T> {

    2     public Action<T> AddFunction { get; set; }

    3     public Action ClearFunction { get; set; }

    4     public Func<T, bool> ContainsFunction { get; set; }

    5     public Action<T[], int> CopyToFunction { get; set; }

    6     public Func<int> CountFunction { get; set; }

    7     public Func<bool> IsReadOnlyFunction { get; set; }

    8     public Func<T, bool> RemoveFunction { get; set; }

    9     public Func<IEnumerator<T>> GetEnumeratorFunction { get; set; }

   10 

   11     #region ICollection<T> Members

   12 

   13     public void Add(T item) {

   14         if (AddFunction == null)

   15             throw new NotImplementedException();

   16         AddFunction.Invoke(item);

   17     }

   18 

   19     public void Clear() {

   20         if (ClearFunction == null)

   21             throw new NotImplementedException();

   22         ClearFunction.Invoke();

   23     }

   24 

   25     public bool Contains(T item) {

   26         if (ContainsFunction == null)

   27             throw new NotImplementedException();

   28         return ContainsFunction.Invoke(item);

   29     }

   30 

   31     public void CopyTo(T[] array, int arrayIndex) {

   32         if (CopyToFunction == null)

   33             throw new NotImplementedException();

   34         CopyToFunction.Invoke(array, arrayIndex);

   35     }

   36 

   37     public int Count {

   38         get {

   39             if (CountFunction == null)

   40                 throw new NotImplementedException();

   41             return CountFunction.Invoke();

   42         }

   43     }

   44 

   45     public bool IsReadOnly {

   46         get {

   47             if (IsReadOnlyFunction == null)

   48                 throw new NotImplementedException();

   49             return IsReadOnlyFunction.Invoke();

   50         }

   51     }

   52 

   53     public bool Remove(T item) {

   54         if (RemoveFunction == null)

   55             throw new NotImplementedException();

   56         return RemoveFunction.Invoke(item);

   57     }

   58 

   59     #endregion

   60 

   61     #region IEnumerable<T> Members

   62 

   63     public IEnumerator<T> GetEnumerator() {

   64         if (GetEnumeratorFunction == null)

   65             throw new NotImplementedException();

   66         return GetEnumeratorFunction.Invoke();

   67     }

   68 

   69     #endregion

   70 

   71     #region IEnumerable Members

   72 

   73     IEnumerator IEnumerable.GetEnumerator() {

   74         return GetEnumerator();

   75     }

   76 

   77     #endregion

   78 }

As you can see, this implementation just passes off execution to the delegates passed in as properties. If the delegate for a particular method is null, then it throws a NotImplementedException.



Here's how I use it:

    1 public partial class Account {

    2     private DelegatePoweredCollection<Group> _groups;

    3 

    4     private void SetupGroupsList() {

    5         _groups = new DelegatePoweredCollection<Group>();

    6         _groups.AddFunction = g => AddToGroup(g);

    7         _groups.ClearFunction = () => RemoveFromAllGroups();

    8         _groups.ContainsFunction = g => UserIsInGroup(g);

    9         _groups.CountFunction = () => this.AccountToGroupMaps

   10             .Count(map => map.Account == this);

   11         _groups.GetEnumeratorFunction = () => GetAllGroupsForAccount();

   12         _groups.IsReadOnlyFunction = () => false;

   13         _groups.RemoveFunction = g => RemoveFromGroup(g);

   14     }

   15 

   16     /// <summary>

   17     /// Gets an ICollection of Groups that this account belongs to

   18     /// </summary>

   19     public ICollection<Group> Groups {

   20         get {

   21             if (_groups == null) SetupGroupsList();

   22             return _groups;

   23         }

   24     }

   25 

   26     private void AddToGroup(Group group) {

   27         // Check to see if a group map already exists.

   28         if (this.AccountToGroupMaps.Any(

   29             map => map.Group == group && map.Account == this)

   30         ) {

   31             // We don't need to add it again.

   32             return;

   33         }

   34 

   35         // Create new group map between this account and

   36         // the specified group

   37         AccountToGroupMap newMap = new AccountToGroupMap();

   38         newMap.Group = group;

   39         newMap.Account = this;

   40 

   41         // Add the mapping to both the group and the account.

   42         this.AccountToGroupMaps.Add(newMap);

   43         group.AccountToGroupMaps.Add(newMap);

   44     }

   45 

   46     private bool RemoveFromGroup(Group group) {

   47         // Remove the group map between this account and the

   48         // specified group

   49         AccountToGroupMap map = this.AccountToGroupMaps

   50             .SingleOrDefault(m => m.Group == group);

   51 

   52         // If the mapping doesn't exist, the user isn't in the

   53         // group to begin with - just return.

   54         if (map == null) return false;

   55 

   56         // Remove the mapping from both the account and the group.

   57         this.AccountToGroupMaps.Remove(map);

   58         group.AccountToGroupMaps.Remove(map);

   59 

   60         return true;

   61     }

   62 

   63     private void RemoveFromAllGroups() {

   64         var maps = this.AccountToGroupMaps

   65             .Where(map => map.Account == this);

   66         foreach (var m in maps) {

   67             this.AccountToGroupMaps.Remove(m);

   68         }

   69     }

   70 

   71     private bool UserIsInGroup(Group g) {

   72         return this.AccountToGroupMaps

   73             .Any(map => map.Account == this && map.Group == g);

   74     }

   75 

   76     private IEnumerator<Group> GetAllGroupsForAccount() {

   77         foreach (var group in this.AccountToGroupMaps

   78             .Where(map => map.Account == this)

   79             .Select(map => map.Group)) {

   80             yield return group;

   81         }

   82     }

   83 }

This particular implementation hides the mapping table in between my m:n association, allowing calling code to treat the Groups collection like any other collection, with the actual mapping being done behind the scenes.

I looked around for a bit, but couldn't find any similar implementations - did I just not do enough research? Have I invented a wheel better made elsewhere? Any comments? Let me know what you think. =)

As for performance - I haven't clocked it or tested it at all, but I'm willing to bet it's easily vastly slower than a 'real' collection implementation - but I think in this case the tradeoff between exposing that mapping object outside of the datacontext is worth a little bit lower performance, especially in this case - this isn't a mission critical section of the application.

No comments: