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.