Welcome to mirror list, hosted at ThFree Co, Russian Federation.

ChangeSet.cs « Microsoft.Web.Http.Data « src - github.com/mono/aspnetwebstack.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: d6f4d83ef8c15adfdf1ca54ee3a04250f87ff670 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Web.Http.Common;
using System.Web.Http.Controllers;

namespace Microsoft.Web.Http.Data
{
    /// <summary>
    /// Represents a set of changes to be processed by a <see cref="DataController"/>.
    /// </summary>
    public sealed class ChangeSet
    {
        private IEnumerable<ChangeSetEntry> _changeSetEntries;

        /// <summary>
        /// Initializes a new instance of the ChangeSet class
        /// </summary>
        /// <param name="changeSetEntries">The set of <see cref="ChangeSetEntry"/> items this <see cref="ChangeSet"/> represents.</param>
        /// <exception cref="ArgumentNullException">if <paramref name="changeSetEntries"/> is null.</exception>
        public ChangeSet(IEnumerable<ChangeSetEntry> changeSetEntries)
        {
            if (changeSetEntries == null)
            {
                throw Error.ArgumentNull("changeSetEntries");
            }

            // ensure the changeset is valid
            ValidateChangeSetEntries(changeSetEntries);

            _changeSetEntries = changeSetEntries;
        }

        /// <summary>
        /// Gets the set of <see cref="ChangeSetEntry"/> items this <see cref="ChangeSet"/> represents.
        /// </summary>
        public ReadOnlyCollection<ChangeSetEntry> ChangeSetEntries
        {
            get { return _changeSetEntries.ToList().AsReadOnly(); }
        }

        /// <summary>
        /// Gets a value indicating whether any of the <see cref="ChangeSetEntry"/> items has an error.
        /// </summary>
        public bool HasError
        {
            get { return _changeSetEntries.Any(op => op.HasConflict || (op.ValidationErrors != null && op.ValidationErrors.Any())); }
        }

        /// <summary>
        /// Returns the original unmodified entity for the provided <paramref name="clientEntity"/>.
        /// </summary>
        /// <remarks>
        /// Note that only members marked with <see cref="RoundtripOriginalAttribute"/> will be set
        /// in the returned instance.
        /// </remarks>
        /// <typeparam name="TEntity">The entity type.</typeparam>
        /// <param name="clientEntity">The client modified entity.</param>
        /// <returns>The original unmodified entity for the provided <paramref name="clientEntity"/>.</returns>
        /// <exception cref="ArgumentNullException">if <paramref name="clientEntity"/> is null.</exception>
        /// <exception cref="ArgumentException">if <paramref name="clientEntity"/> is not in the change set.</exception>
        public TEntity GetOriginal<TEntity>(TEntity clientEntity) where TEntity : class
        {
            if (clientEntity == null)
            {
                throw Error.ArgumentNull("clientEntity");
            }

            ChangeSetEntry entry = _changeSetEntries.FirstOrDefault(p => Object.ReferenceEquals(p.Entity, clientEntity));
            if (entry == null)
            {
                throw Error.Argument(Resource.ChangeSet_ChangeSetEntryNotFound);
            }

            if (entry.Operation == ChangeOperation.Insert)
            {
                throw Error.InvalidOperation(Resource.ChangeSet_OriginalNotValidForInsert);
            }

            return (TEntity)entry.OriginalEntity;
        }

        /// <summary>
        /// Validates that the specified entries are well formed.
        /// </summary>
        /// <param name="changeSetEntries">The changeset entries to validate.</param>
        private static void ValidateChangeSetEntries(IEnumerable<ChangeSetEntry> changeSetEntries)
        {
            HashSet<int> idSet = new HashSet<int>();
            HashSet<object> entitySet = new HashSet<object>();
            foreach (ChangeSetEntry entry in changeSetEntries)
            {
                // ensure Entity is not null
                if (entry.Entity == null)
                {
                    throw Error.InvalidOperation(Resource.InvalidChangeSet, Resource.InvalidChangeSet_NullEntity);
                }

                // ensure unique client IDs
                if (idSet.Contains(entry.Id))
                {
                    throw Error.InvalidOperation(Resource.InvalidChangeSet, Resource.InvalidChangeSet_DuplicateId);
                }
                idSet.Add(entry.Id);

                // ensure unique entity instances - there can only be a single entry
                // for a given entity instance
                if (entitySet.Contains(entry.Entity))
                {
                    throw Error.InvalidOperation(Resource.InvalidChangeSet, Resource.InvalidChangeSet_DuplicateEntity);
                }
                entitySet.Add(entry.Entity);

                // entities must be of the same type
                if (entry.OriginalEntity != null && !(entry.Entity.GetType() == entry.OriginalEntity.GetType()))
                {
                    throw Error.InvalidOperation(Resource.InvalidChangeSet, Resource.InvalidChangeSet_MustBeSameType);
                }

                if (entry.Operation == ChangeOperation.Insert && entry.OriginalEntity != null)
                {
                    throw Error.InvalidOperation(Resource.InvalidChangeSet, Resource.InvalidChangeSet_InsertsCantHaveOriginal);
                }
            }

            // now that we have the full Id space, we can validate associations
            foreach (ChangeSetEntry entry in changeSetEntries)
            {
                if (entry.Associations != null)
                {
                    ValidateAssociationMap(entry.Entity.GetType(), idSet, entry.Associations);
                }

                if (entry.OriginalAssociations != null)
                {
                    ValidateAssociationMap(entry.Entity.GetType(), idSet, entry.OriginalAssociations);
                }
            }
        }

        /// <summary>
        /// Validates the specified association map.
        /// </summary>
        /// <param name="entityType">The entity type the association is on.</param>
        /// <param name="idSet">The set of all unique Ids in the changeset.</param>
        /// <param name="associationMap">The association map to validate.</param>
        private static void ValidateAssociationMap(Type entityType, HashSet<int> idSet, IDictionary<string, int[]> associationMap)
        {
            PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(entityType);

            foreach (var associationItem in associationMap)
            {
                // ensure that the member is an association member
                string associationMemberName = associationItem.Key;
                PropertyDescriptor associationMember = properties[associationMemberName];
                if (associationMember == null || associationMember.Attributes[typeof(AssociationAttribute)] == null)
                {
                    throw Error.InvalidOperation(Resource.InvalidChangeSet,
                                                 String.Format(CultureInfo.CurrentCulture, Resource.InvalidChangeSet_InvalidAssociationMember, entityType, associationMemberName));
                }

                // ensure that the id collection is not null
                if (associationItem.Value == null)
                {
                    throw Error.InvalidOperation(Resource.InvalidChangeSet,
                                                 String.Format(CultureInfo.CurrentCulture, Resource.InvalidChangeSet_AssociatedIdsCannotBeNull, entityType, associationMemberName));
                }
                // ensure that each Id specified is in the changeset
                foreach (int id in associationItem.Value)
                {
                    if (!idSet.Contains(id))
                    {
                        throw Error.InvalidOperation(Resource.InvalidChangeSet,
                                                     String.Format(CultureInfo.CurrentCulture, Resource.InvalidChangeSet_AssociatedIdNotInChangeset, id, entityType, associationMemberName));
                    }
                }
            }
        }

        /// <summary>
        /// Reestablish associations based on Id lists by adding the referenced entities
        /// to their association members
        /// </summary>
        internal void SetEntityAssociations()
        {
            // create a unique map from Id to entity instances, and update operations
            // so Ids map to the same instances, since during deserialization reference
            // identity is not maintained.
            var entityIdMap = _changeSetEntries.ToDictionary(p => p.Id, p => new { Entity = p.Entity, OriginalEntity = p.OriginalEntity });
            foreach (ChangeSetEntry changeSetEntry in _changeSetEntries)
            {
                object entity = entityIdMap[changeSetEntry.Id].Entity;
                if (changeSetEntry.Entity != entity)
                {
                    changeSetEntry.Entity = entity;
                }

                object original = entityIdMap[changeSetEntry.Id].OriginalEntity;
                if (original != null && changeSetEntry.OriginalEntity != original)
                {
                    changeSetEntry.OriginalEntity = original;
                }
            }

            // for all entities with associations, reestablish the associations by mapping the Ids
            // to entity instances and adding them to the association members
            HashSet<int> visited = new HashSet<int>();
            foreach (var entityGroup in _changeSetEntries.Where(p => (p.Associations != null && p.Associations.Count > 0) || (p.OriginalAssociations != null && p.OriginalAssociations.Count > 0)).GroupBy(p => p.Entity.GetType()))
            {
                Dictionary<string, PropertyDescriptor> associationMemberMap = TypeDescriptor.GetProperties(entityGroup.Key).Cast<PropertyDescriptor>().Where(p => p.Attributes[typeof(AssociationAttribute)] != null).ToDictionary(p => p.Name);
                foreach (ChangeSetEntry changeSetEntry in entityGroup)
                {
                    if (visited.Contains(changeSetEntry.Id))
                    {
                        continue;
                    }
                    visited.Add(changeSetEntry.Id);

                    // set current associations
                    if (changeSetEntry.Associations != null)
                    {
                        foreach (var associationItem in changeSetEntry.Associations)
                        {
                            PropertyDescriptor assocMember = associationMemberMap[associationItem.Key];
                            IEnumerable<object> children = associationItem.Value.Select(p => entityIdMap[p].Entity);
                            SetAssociationMember(changeSetEntry.Entity, assocMember, children);
                        }
                    }
                }
            }
        }

        internal bool Validate(HttpActionContext actionContext)
        {
            // Validate all entries except those with type None or Delete (since we don't want to validate
            // entites we're going to delete).
            bool success = true;
            IEnumerable<ChangeSetEntry> entriesToValidate = ChangeSetEntries.Where(
                p => (p.ActionDescriptor != null && p.Operation != ChangeOperation.None && p.Operation != ChangeOperation.Delete)
                     || (p.EntityActions != null && p.EntityActions.Any()));

            foreach (ChangeSetEntry entry in entriesToValidate)
            {
                // TODO: optimize by determining whether a type actually requires any validation?
                // TODO: support for method level / parameter validation?

                List<ValidationResultInfo> validationErrors = new List<ValidationResultInfo>();
                if (!DataControllerValidation.ValidateObject(entry.Entity, validationErrors, actionContext))
                {
                    entry.ValidationErrors = validationErrors.Distinct(EqualityComparer<ValidationResultInfo>.Default).ToList();
                    success = false;
                }

                // clear after each validate call, since we've already
                // copied over the errors
                actionContext.ModelState.Clear();
            }

            return success;
        }

        /// <summary>
        /// Adds the specified associated entities to the specified association member for the specified entity.
        /// </summary>
        /// <param name="entity">The entity</param>
        /// <param name="associationProperty">The association member (singleton or collection)</param>
        /// <param name="associatedEntities">Collection of associated entities</param>
        private static void SetAssociationMember(object entity, PropertyDescriptor associationProperty, IEnumerable<object> associatedEntities)
        {
            if (associatedEntities.Count() == 0)
            {
                return;
            }

            object associationValue = associationProperty.GetValue(entity);
            if (typeof(IEnumerable).IsAssignableFrom(associationProperty.PropertyType))
            {
                if (associationValue == null)
                {
                    throw Error.InvalidOperation(Resource.DataController_AssociationCollectionPropertyIsNull, associationProperty.ComponentType.Name, associationProperty.Name);
                }

                IList list = associationValue as IList;
                IEnumerable<object> associationSequence = null;
                MethodInfo addMethod = null;
                if (list == null)
                {
                    // not an IList, so we have to use reflection
                    Type associatedEntityType = TypeUtility.GetElementType(associationValue.GetType());
                    addMethod = associationValue.GetType().GetMethod("Add", BindingFlags.Public | BindingFlags.Instance, null, new Type[] { associatedEntityType }, null);
                    if (addMethod == null)
                    {
                        throw Error.InvalidOperation(Resource.DataController_InvalidCollectionMember, associationProperty.Name);
                    }
                    associationSequence = ((IEnumerable)associationValue).Cast<object>();
                }

                foreach (object associatedEntity in associatedEntities)
                {
                    // add the entity to the collection if it's not already there
                    if (list != null)
                    {
                        if (!list.Contains(associatedEntity))
                        {
                            list.Add(associatedEntity);
                        }
                    }
                    else
                    {
                        if (!associationSequence.Contains(associatedEntity))
                        {
                            addMethod.Invoke(associationValue, new object[] { associatedEntity });
                        }
                    }
                }
            }
            else
            {
                // set the reference if it's not already set
                object associatedEntity = associatedEntities.Single();
                object currentValue = associationProperty.GetValue(entity);
                if (!Object.Equals(currentValue, associatedEntity))
                {
                    associationProperty.SetValue(entity, associatedEntity);
                }
            }
        }
    }
}