Files
linux-packaging-mono/external/aspnetwebstack/src/Microsoft.Web.Http.Data/ChangeSet.cs
Jo Shields a575963da9 Imported Upstream version 3.6.0
Former-commit-id: da6be194a6b1221998fc28233f2503bd61dd9d14
2014-08-13 10:39:27 +01:00

338 lines
16 KiB
C#

// Copyright (c) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information.
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;
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);
}
}
}
}
}