diff --git a/src/Microsoft.AspNet.OData.Shared/Common/ODataPathHelper.cs b/src/Microsoft.AspNet.OData.Shared/Common/ODataPathHelper.cs new file mode 100644 index 0000000000..ffe69f31b6 --- /dev/null +++ b/src/Microsoft.AspNet.OData.Shared/Common/ODataPathHelper.cs @@ -0,0 +1,68 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +using System.Collections.Generic; +using System.Linq; +using Microsoft.OData.UriParser; + +namespace Microsoft.AspNet.OData.Common +{ + /// + /// Helper methods for . + /// + internal static class ODataPathHelper + { + /// + /// Get the keys from a . + /// + /// The to extract the keys. + /// Dictionary of keys. + public static Dictionary KeySegmentAsDictionary(KeySegment keySegment) + { + if (keySegment == null) + { + throw Error.ArgumentNull(nameof(keySegment)); + } + + return keySegment.Keys.ToDictionary(d => d.Key, d => d.Value); + } + + /// + /// Get the position of the next in a list of . + /// + /// List of . + /// Current position in the list of . + /// Position of the next if it exists, or -1 otherwise. + public static int GetNextKeySegmentPosition(IReadOnlyList pathSegments, int currentPosition) + { + if (pathSegments == null) + { + throw Error.ArgumentNull(nameof(pathSegments)); + } + + if (currentPosition < 0 || currentPosition >= pathSegments.Count) + { + return -1; + } + + if (pathSegments[currentPosition] is KeySegment) + { + currentPosition++; + } + + for (int i = currentPosition; i < pathSegments.Count; i++) + { + if (pathSegments[i] is KeySegment) + { + return i; + } + } + + return -1; + } + } +} diff --git a/src/Microsoft.AspNet.OData.Shared/DefaultEdmODataAPIHandler.cs b/src/Microsoft.AspNet.OData.Shared/DefaultEdmODataAPIHandler.cs new file mode 100644 index 0000000000..a7eaf2218f --- /dev/null +++ b/src/Microsoft.AspNet.OData.Shared/DefaultEdmODataAPIHandler.cs @@ -0,0 +1,192 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using Microsoft.OData.Edm; + +namespace Microsoft.AspNet.OData +{ + /// + /// This is the default patch handler for non-CLR types. This class has default get, create, delete and updateRelatedObject + /// methods that are used to patch an original collection when the collection is provided. + /// + internal class DefaultEdmODataAPIHandler : EdmODataAPIHandler + { + private IEdmEntityType entityType; + private ICollection originalList; + + /// + /// Initializes a new instance of the class. + /// + /// Original collection of the type which needs to be updated. + /// The Edm entity type of the collection. + public DefaultEdmODataAPIHandler(ICollection originalList, IEdmEntityType entityType) + { + Debug.Assert(entityType != null, "entityType != null"); + + this.entityType = entityType; + this.originalList = originalList ?? new List(); + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")] + public override ODataAPIResponseStatus TryGet(IDictionary keyValues, out IEdmStructuredObject originalObject, out string errorMessage) + { + ODataAPIResponseStatus status = ODataAPIResponseStatus.Success; + errorMessage = string.Empty; + originalObject = null; + + Debug.Assert(keyValues != null, "keyValues != null"); + + try + { + originalObject = GetFilteredItem(keyValues); + + if (originalObject == null) + { + status = ODataAPIResponseStatus.NotFound; + } + } + catch (Exception ex) + { + status = ODataAPIResponseStatus.Failure; + errorMessage = ex.Message; + } + + return status; + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")] + public override ODataAPIResponseStatus TryCreate(IDictionary keyValues, out IEdmStructuredObject createdObject, out string errorMessage) + { + createdObject = null; + errorMessage = string.Empty; + + try + { + createdObject = new EdmEntityObject(entityType); + originalList.Add(createdObject); + + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")] + public override ODataAPIResponseStatus TryDelete(IDictionary keyValues, out string errorMessage) + { + errorMessage = string.Empty; + + try + { + EdmStructuredObject originalObject = GetFilteredItem(keyValues); + + if (originalObject != null) + { + originalList.Remove(originalObject); + } + + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")] + public override ODataAPIResponseStatus TryAddRelatedObject(IEdmStructuredObject resource, out string errorMessage) + { + errorMessage = string.Empty; + + try + { + originalList.Add(resource); + + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + + public override EdmODataAPIHandler GetNestedHandler(IEdmStructuredObject parent, string navigationPropertyName) + { + IEdmNavigationProperty navProperty = entityType.NavigationProperties().FirstOrDefault(navProp => navProp.Name == navigationPropertyName); + + if (navProperty == null) + { + return null; + } + + IEdmEntityType nestedEntityType = navProperty.ToEntityType(); + + object obj; + + if (parent.TryGetPropertyValue(navigationPropertyName, out obj)) + { + ICollection nestedList = obj as ICollection; + + return new DefaultEdmODataAPIHandler(nestedList, nestedEntityType); + } + + return null; + } + + /// + /// Filter the object based on the set of keys. + /// + /// Key-value pairs for the object keys. + /// The filtered object. + /// There will only be very few key elements usually, mostly 1, so performance wont be impacted. + private EdmStructuredObject GetFilteredItem(IDictionary keyValues) + { + if (originalList.Count == 0) + { + return null; + } + + foreach (EdmStructuredObject item in originalList) + { + bool isMatch = true; + + foreach (KeyValuePair keyValue in keyValues) + { + object value; + if (item.TryGetPropertyValue(keyValue.Key, out value)) + { + if (!Equals(value, keyValue.Value)) + { + // Not a match, so try the next one + isMatch = false; + break; + } + } + } + + if (isMatch) + { + return item; + } + } + + return null; + } + } +} diff --git a/src/Microsoft.AspNet.OData.Shared/DefaultODataAPIHandler.cs b/src/Microsoft.AspNet.OData.Shared/DefaultODataAPIHandler.cs new file mode 100644 index 0000000000..476964e7be --- /dev/null +++ b/src/Microsoft.AspNet.OData.Shared/DefaultODataAPIHandler.cs @@ -0,0 +1,178 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace Microsoft.AspNet.OData +{ + /// + /// This is the default ODataAPIHandler for CLR types. This class has default get, create and update + /// methods that are used to patch an original collection when the collection is provided. + /// + /// + internal class DefaultODataAPIHandler : ODataAPIHandler where TStructuralType :class + { + Type clrType; + ICollection originalList; + + /// + /// Initializes a new instance of the class. + /// + /// Original collection of the type which needs to be updated. + public DefaultODataAPIHandler(ICollection originalList) + { + this.clrType = typeof(TStructuralType); + this.originalList = originalList ?? new List(); + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")] + public override ODataAPIResponseStatus TryGet(IDictionary keyValues, out TStructuralType originalObject, out string errorMessage) + { + ODataAPIResponseStatus status = ODataAPIResponseStatus.Success; + errorMessage = string.Empty; + originalObject = default(TStructuralType); + + try + { + originalObject = GetFilteredItem(keyValues); + + if (originalObject == null) + { + status = ODataAPIResponseStatus.NotFound; + } + } + catch (Exception ex) + { + status = ODataAPIResponseStatus.Failure; + errorMessage = ex.Message; + } + + return status; + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")] + public override ODataAPIResponseStatus TryCreate(IDictionary keyValues, out TStructuralType createdObject, out string errorMessage) + { + createdObject = default(TStructuralType); + errorMessage = string.Empty; + + try + { + if (clrType.GetConstructor(Type.EmptyTypes) != null) + { + createdObject = Activator.CreateInstance(clrType) as TStructuralType; + originalList.Add(createdObject); + + return ODataAPIResponseStatus.Success; + } + else + { + errorMessage = "Type has not parameterless constructor"; + return ODataAPIResponseStatus.Failure; + } + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")] + public override ODataAPIResponseStatus TryDelete(IDictionary keyValues, out string errorMessage) + { + errorMessage = string.Empty; + + try + { + TStructuralType originalObject = GetFilteredItem(keyValues); + originalList.Remove(originalObject); + + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + + public override IODataAPIHandler GetNestedHandler(TStructuralType parent, string navigationPropertyName) + { + foreach (PropertyInfo property in clrType.GetProperties()) + { + if (property.Name == navigationPropertyName) + { + Type type = typeof(DefaultODataAPIHandler<>).MakeGenericType(property.PropertyType.GetGenericArguments()[0]); + + return Activator.CreateInstance(type, property.GetValue(parent)) as IODataAPIHandler; + } + } + + return null; + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")] + public override ODataAPIResponseStatus TryAddRelatedObject(TStructuralType resource, out string errorMessage) + { + errorMessage = string.Empty; + + try + { + originalList.Add(resource); + + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + + /// + /// Filter the object based on the set of keys. + /// + /// Key-value pairs for the object keys. + /// The filtered object. + /// There will only be very few key elements usually, mostly 1, so performance wont be impacted. + private TStructuralType GetFilteredItem(IDictionary keyValues) + { + if (originalList.Count == 0) + { + return default(TStructuralType); + } + + foreach (TStructuralType item in originalList) + { + bool isMatch = true; + foreach (KeyValuePair keyValue in keyValues) + { + PropertyInfo propertyInfo = clrType.GetProperty(keyValue.Key); + + if (!Equals(propertyInfo.GetValue(item), keyValue.Value)) + { + // Not a match, so try the next one + isMatch = false; + break; + } + } + if (isMatch) + { + return item; + } + } + + return default(TStructuralType); + } + } +} diff --git a/src/Microsoft.AspNet.OData.Shared/DeltaOfTStructuralType.cs b/src/Microsoft.AspNet.OData.Shared/DeltaOfTStructuralType.cs index d0105e4cd1..f60e4ec76b 100644 --- a/src/Microsoft.AspNet.OData.Shared/DeltaOfTStructuralType.cs +++ b/src/Microsoft.AspNet.OData.Shared/DeltaOfTStructuralType.cs @@ -18,6 +18,7 @@ using System.Runtime.Serialization; using Microsoft.AspNet.OData.Builder; using Microsoft.AspNet.OData.Common; +using Microsoft.AspNet.OData.Extensions; using Microsoft.AspNet.OData.Formatter; using Microsoft.OData.UriParser; @@ -416,12 +417,31 @@ public override IEnumerable GetUnchangedPropertyNames() return _updatableProperties.Intersect(_allProperties.Keys).Except(GetChangedPropertyNames()); } + /// + /// Returns all known properties in this as an + /// of property Names. Does not include the names of the changed dynamic + /// properties. + /// + internal IEnumerable GetAllPropertyNames() + { + // UpdatableProperties could include arbitrary strings, filter by _allProperties + //return _updatableProperties.Intersect(_allProperties.Keys).Except(GetChangedPropertyNames()); + return GetChangedPropertyNames().Concat(GetUnchangedPropertyNames()); + } + /// /// Copies the changed property values from the underlying entity (accessible via ) /// to the entity recursively. /// /// The entity to be updated. public TStructuralType CopyChangedValues(TStructuralType original) + { + return CopyChangedValues(original, null, null); + } + + [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity")] + [SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily")] + internal TStructuralType CopyChangedValues(TStructuralType original, ODataAPIHandler apiHandler = null, ODataAPIHandlerFactory apiHandlerFactory = null) { if (original == null) { @@ -447,7 +467,7 @@ public TStructuralType CopyChangedValues(TStructuralType original) CopyChangedDynamicValues(original); - CopyChangedNestedProperties(original); + CopyChangedNestedProperties(original, apiHandler, apiHandlerFactory); return original; } @@ -496,14 +516,65 @@ public TStructuralType Patch(TStructuralType original) /// /// Overwrites the entity with the values stored in this delta. /// - /// The semantics of this operation are equivalent to a HTTP PUT operation, hence the name. /// The entity to be updated. + /// API Handler for the entity. + public void Patch(TStructuralType original, IODataAPIHandler apiHandler) + { + Patch(original, apiHandler, null); + } + + /// + /// Overwrites the entity with the values stored in this delta. + /// + /// The entity to be updated. + /// API Handler for the entity. + /// API Handler Factory. + public void Patch(TStructuralType original, IODataAPIHandler apiHandler, ODataAPIHandlerFactory apiHandlerFactory) + { + Debug.Assert(apiHandler != null, "apiHandler != null"); + + if (IsComplexType) + { + original = ReassignComplexDerivedType(original, _structuredType, original.GetType(), ExpectedClrType) as TStructuralType; + } + + CopyChangedValues(original, apiHandler as ODataAPIHandler, apiHandlerFactory); + } + + /// + /// Overwrites the entity with the values stored in this Delta. + /// + /// The entity to be updated. + /// The semantics of this operation are equivalent to a HTTP PUT operation, hence the name. public void Put(TStructuralType original) { CopyChangedValues(original); CopyUnchangedValues(original); } + /// + /// Update the instance object properties with the properties from the @odata.id object. + /// + /// The @odata.id object. + internal void UpdateODataIdObject(TStructuralType original) + { + if (original == null) + { + throw Error.ArgumentNull(nameof(original)); + } + + if (!_structuredType.IsInstanceOfType(original)) + { + throw Error.Argument(nameof(original), SRResources.DeltaTypeMismatch, _structuredType, original.GetType()); + } + + IEnumerable> propertiesToCopy = GetUnchangedPropertyNames().Select(s => _allProperties[s]); + foreach (PropertyAccessor propertyToCopy in propertiesToCopy) + { + propertyToCopy.Copy(original, _instance); + } + } + /// /// Creates a new object of the derived type in a delta request. /// @@ -795,7 +866,9 @@ private void CopyChangedDynamicValues(TStructuralType targetEntity) /// Copies changed nested properties and leaves the unchanged properties /// /// The structural object - private void CopyChangedNestedProperties(TStructuralType original) + /// API Handler for the entity. + /// API Handler Factory + private void CopyChangedNestedProperties(TStructuralType original, ODataAPIHandler apiHandler = null, ODataAPIHandlerFactory apiHandlerFactory = null) { // For nested resources. foreach (string nestedResourceName in _deltaNestedResources.Keys) @@ -804,42 +877,57 @@ private void CopyChangedNestedProperties(TStructuralType original) dynamic deltaNestedResource = _deltaNestedResources[nestedResourceName]; dynamic originalNestedResource = null; - if (!TryGetPropertyRef(original, nestedResourceName, out originalNestedResource)) - { - throw Error.Argument(nestedResourceName, SRResources.DeltaNestedResourceNameNotFound, - nestedResourceName, original.GetType()); - } - - if (originalNestedResource == null) + if (deltaNestedResource is IDeltaSet) { - // When patching original target of null value, directly set nested resource. - dynamic instance = deltaNestedResource.GetInstance(); - - // Recursively patch up the instance with the nested resources. - deltaNestedResource.CopyChangedValues(instance); + if (apiHandler != null) + { + IODataAPIHandler nestedApiHandler = apiHandler.GetNestedHandler(original, nestedResourceName); - _allProperties[nestedResourceName].SetValue(original, instance); + if (nestedApiHandler != null) + { + deltaNestedResource.CopyChangedValues(nestedApiHandler, apiHandlerFactory); + } + } } else { - // Recursively patch the subtree. - Contract.Assert(TypedDelta.IsDeltaOfT(((object)deltaNestedResource).GetType()), nestedResourceName + "'s corresponding value should be Delta type but is not."); - - Type newType = deltaNestedResource.StructuredType; - Type originalType = originalNestedResource.GetType(); + if (!TryGetPropertyRef(original, nestedResourceName, out originalNestedResource)) + { + throw Error.Argument(nestedResourceName, SRResources.DeltaNestedResourceNameNotFound, + nestedResourceName, original.GetType()); + } - if (deltaNestedResource.IsComplexType && newType != originalType) + if (originalNestedResource == null) { - originalNestedResource = ReassignComplexDerivedType( - originalNestedResource, - newType, - originalType, - deltaNestedResource.ExpectedClrType); + // When patching original target of null value, directly set nested resource. + dynamic instance = deltaNestedResource.GetInstance(); + + // Recursively patch up the instance with the nested resources. + deltaNestedResource.CopyChangedValues(instance); - _structuredType.GetProperty(nestedResourceName).SetValue(original, (object)originalNestedResource); + _allProperties[nestedResourceName].SetValue(original, instance); } + else + { + // Recursively patch the subtree. + Contract.Assert(TypedDelta.IsDeltaOfT(((object)deltaNestedResource).GetType()), nestedResourceName + "'s corresponding value should be Delta type but is not."); - deltaNestedResource.CopyChangedValues(originalNestedResource); + Type newType = deltaNestedResource.StructuredType; + Type originalType = originalNestedResource.GetType(); + + if (deltaNestedResource.IsComplexType && newType != originalType) + { + originalNestedResource = ReassignComplexDerivedType( + originalNestedResource, + newType, + originalType, + deltaNestedResource.ExpectedClrType); + + _structuredType.GetProperty(nestedResourceName).SetValue(original, (object)originalNestedResource); + } + + deltaNestedResource.CopyChangedValues(originalNestedResource); + } } } } diff --git a/src/Microsoft.AspNet.OData.Shared/DeltaSetOfT.cs b/src/Microsoft.AspNet.OData.Shared/DeltaSetOfT.cs index 90c32c3944..4a095c53da 100644 --- a/src/Microsoft.AspNet.OData.Shared/DeltaSetOfT.cs +++ b/src/Microsoft.AspNet.OData.Shared/DeltaSetOfT.cs @@ -8,7 +8,12 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Linq; +using Microsoft.AspNet.OData.Builder; using Microsoft.AspNet.OData.Common; +using Microsoft.AspNet.OData.Extensions; +using Org.OData.Core.V1; namespace Microsoft.AspNet.OData { @@ -46,5 +51,401 @@ protected override void InsertItem(int index, IDeltaSetItem item) base.InsertItem(index, item); } + + /// + /// Patch for DeltaSet, a collection for Delta. + /// + /// Original collection of the type which needs to be updated. + /// /// DeltaSet response. + public DeltaSet Patch(ICollection originalCollection) + { + ODataAPIHandler apiHandler = new DefaultODataAPIHandler(originalCollection); + + return CopyChangedValues(apiHandler); + } + + /// + /// Patch for DeltaSet, a collection for Delta. + /// + /// API Handler for the entity. + /// DeltaSet response. + public DeltaSet Patch(ODataAPIHandler apiHandlerOfT) + { + return Patch(apiHandlerOfT, null); + } + + /// + /// Patch for DeltaSet, a collection for Delta. + /// + /// API Handler for the entity. + /// API Handler Factory. + /// DeltaSet response. + public DeltaSet Patch(ODataAPIHandler apiHandlerOfT, ODataAPIHandlerFactory apiHandlerFactory) + { + Debug.Assert(apiHandlerOfT != null, "apiHandlerOfT != null"); + + return CopyChangedValues(apiHandlerOfT, apiHandlerFactory); + } + + /// + /// Get the keys and use the keys to find the original object to patch from the collection. + /// + /// API Handler for the entity. + /// API Handler Factory. + /// DeltaSet response. + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")] + internal DeltaSet CopyChangedValues(IODataAPIHandler apiHandler, ODataAPIHandlerFactory apiHandlerFactory = null) + { + //Here we are getting the keys and using the keys to find the original object + //to patch from the collection. + + ODataAPIHandler apiHandlerOfT = apiHandler as ODataAPIHandler; + + Debug.Assert(apiHandlerOfT != null, "apiHandlerOfT != null"); + + DeltaSet deltaSet = CreateDeltaSet(); + + foreach (Delta changedObj in Items) + { + ODataAPIHandler handler = apiHandlerOfT; + bool hasODataId = false; + + if (apiHandlerFactory != null) + { + //{ + // "@odata.context":"http://localhost:6285/odata/$metadata#Customers/$delta", + // "value":[ + // { + // "@odata.type":"CustomersApiEF.Models.Customer", + // "Id":1, + // "Name":"Customer1", + // "Orders@odata.delta":[ + // { + // "@odata.id":"Customers(3)/Orders(1005)", + // "Quantity": 1005 + // }, + // { + // "Id": 2000, + // "Price":200, + // "Quantity":90 + // } + // ] + // } + // ] + //} + + // If we have a request payload above and we are handling the changed values in Orders, + // The apiHandlerOfT is OrdersAPIHandler with parent as Customers(1). + // The object with "@odata.id" will have OrdersAPIHandler with parent as Customers(3). + // The object with id 2000 will have OrdersAPIHandler similar to apiHandlerOfT. + // The codebelow ensures we use the correct handler. + // NOTE: Handler when we have odata.id will always be different than the parent handler. + // However this is true if handlers are applied correctly. + + ODataAPIHandler odataPathApiHandler = apiHandlerFactory.GetHandler(changedObj.ODataPath) as ODataAPIHandler; + + if (odataPathApiHandler != null && changedObj.ODataPath.Any()) + { + // Using a 3rd party library to compare objects + // https://github.com/GregFinzer/Compare-Net-Objects/wiki/Getting-Started + // TODO: + // a) Confirm to perf regressions are caused byt his library. + // b) Investigate if there is a better library. + + KellermanSoftware.CompareNetObjects.CompareLogic compare = new KellermanSoftware.CompareNetObjects.CompareLogic(); + compare.Config.CaseSensitive = false; + compare.Config.ComparePrivateProperties = true; + compare.Config.ComparePrivateFields = true; + + KellermanSoftware.CompareNetObjects.ComparisonResult result = compare.Compare(handler, odataPathApiHandler); + + if (!result.AreEqual) + { + handler = odataPathApiHandler; + hasODataId = true; + } + } + } + + DataModificationOperationKind operation = DataModificationOperationKind.Update; + + //Get filtered item based on keys + TStructuralType original = null; + string errorMessage = string.Empty; + string getErrorMessage = string.Empty; + + try + { + Dictionary keyValues = new Dictionary(); + keyValues = changedObj.ODataPath.GetKeys(); + + bool containsKeyValue = false; + + foreach (string key in _keys) + { + keyValues.TryGetValue(key, out object value); + + if (value != null && !string.IsNullOrEmpty(value.ToString())) + { + containsKeyValue = true; + continue; + } + } + + ODataAPIResponseStatus odataAPIResponseStatus; + + if (containsKeyValue) + { + odataAPIResponseStatus = handler.TryGet(keyValues, out original, out getErrorMessage); + } + else + { + odataAPIResponseStatus = ODataAPIResponseStatus.NotFound; + } + + DeltaDeletedEntityObject deletedObj = changedObj as DeltaDeletedEntityObject; + + if (odataAPIResponseStatus == ODataAPIResponseStatus.Failure) + { + IDeltaSetItem deltaSetItem = changedObj; + DataModificationExceptionType dataModificationExceptionType = new DataModificationExceptionType(operation); + dataModificationExceptionType.MessageType = new MessageType { Message = getErrorMessage }; + + deltaSetItem.TransientInstanceAnnotationContainer.AddResourceAnnotation(SRResources.DataModificationException, dataModificationExceptionType); + deltaSet.Add(deltaSetItem); + + continue; + } + + if (deletedObj != null) + { + operation = DataModificationOperationKind.Delete; + + if (odataAPIResponseStatus == ODataAPIResponseStatus.NotFound) + { + // Handle Failed Operation - Delete when the object doesn't exist. + IDeltaSetItem deltaSetItem = changedObj; + DataModificationExceptionType dataModificationExceptionType = new DataModificationExceptionType(operation); + dataModificationExceptionType.MessageType = new MessageType { Message = "Object to delete not found." }; + + deltaSetItem.TransientInstanceAnnotationContainer.AddResourceAnnotation(SRResources.DataModificationException, dataModificationExceptionType); + deltaSet.Add(deltaSetItem); + + continue; + } + + // Confirm if we actually need to patch. + // changedObj.CopyChangedValues(original, handler, apiHandlerFactory); + + if (handler.TryDelete(keyValues, out errorMessage) != ODataAPIResponseStatus.Success) + { + // Handle Failed Operation - Delete + IDeltaSetItem changedObject = HandleFailedOperation(changedObj, operation, original, errorMessage); + deltaSet.Add(changedObject); + + continue; + } + + deltaSet.Add(deletedObj); + } + else + { + if (odataAPIResponseStatus == ODataAPIResponseStatus.NotFound) + { + operation = DataModificationOperationKind.Insert; + + if (handler.TryCreate(keyValues, out original, out errorMessage) != ODataAPIResponseStatus.Success) + { + //Handle a failed Operation - create + IDeltaSetItem changedObject = HandleFailedOperation(changedObj, operation, original, errorMessage); + deltaSet.Add(changedObject); + continue; + } + } + else if (odataAPIResponseStatus == ODataAPIResponseStatus.Success) + { + operation = DataModificationOperationKind.Update; + + if (hasODataId) + { + ODataAPIResponseStatus linkResponseStatus = apiHandlerOfT.TryAddRelatedObject(original, out errorMessage); + + if (linkResponseStatus == ODataAPIResponseStatus.Failure) + { + IDeltaSetItem changedObject = HandleFailedOperation(changedObj, operation, original, errorMessage); + deltaSet.Add(changedObject); + } + } + } + else + { + //Handle a failed operation + IDeltaSetItem changedObject = HandleFailedOperation(changedObj, operation, original, getErrorMessage); + deltaSet.Add(changedObject); + continue; + } + + if (hasODataId) + { + changedObj.UpdateODataIdObject(original); + } + + // Patch for addition/update. This will call Delta for each item in the collection. + // This will work in cases where we use delegates to create objects. + changedObj.CopyChangedValues(original, handler, apiHandlerFactory); + + deltaSet.Add(changedObj); + } + } + catch (Exception ex) + { + //For handling the failed operations. + IDeltaSetItem changedObject = HandleFailedOperation(changedObj, operation, original, ex.Message); + deltaSet.Add(changedObject); + } + } + + return deltaSet; + } + + private DeltaSet CreateDeltaSet() + { + Type type = typeof(DeltaSet<>).MakeGenericType(_clrType); + + return Activator.CreateInstance(type, _keys) as DeltaSet; + } + + private IDeltaSetItem HandleFailedOperation(Delta changedObj, DataModificationOperationKind operation, TStructuralType originalObj, string errorMessage) + { + IDeltaSetItem deltaSetItem = null; + DataModificationExceptionType dataModificationExceptionType = new DataModificationExceptionType(operation); + dataModificationExceptionType.MessageType = new MessageType { Message = errorMessage }; + + // This handles the DataModificationException. It adds the Core.DataModificationException annotation and also copies other instance annotations. + // The failed operation will be based on the protocol + switch (operation) + { + case DataModificationOperationKind.Update: + deltaSetItem = changedObj; + break; + case DataModificationOperationKind.Insert: + { + deltaSetItem = CreateDeletedEntityForFailedOperation(changedObj); + break; + } + case DataModificationOperationKind.Delete: + { + deltaSetItem = CreateEntityObjectForFailedOperation(changedObj, originalObj); + break; + } + } + + deltaSetItem.TransientInstanceAnnotationContainer = changedObj.TransientInstanceAnnotationContainer; + deltaSetItem.TransientInstanceAnnotationContainer.AddResourceAnnotation(SRResources.DataModificationException, dataModificationExceptionType); + + Debug.Assert(deltaSetItem != null, "deltaSetItem != null"); + + return deltaSetItem; + } + + private IDeltaSetItem CreateEntityObjectForFailedOperation(Delta changedObj, TStructuralType originalObj) + { + Type type = typeof(Delta<>).MakeGenericType(_clrType); + + Delta deltaObject = Activator.CreateInstance(type, _clrType, null, null, false, + changedObj.InstanceAnnotationsPropertyInfo) as Delta; + + SetProperties(originalObj, deltaObject); + + if (deltaObject.InstanceAnnotationsPropertyInfo != null) + { + object instanceAnnotationValue; + changedObj.TryGetPropertyValue(deltaObject.InstanceAnnotationsPropertyInfo.Name, out instanceAnnotationValue); + if (instanceAnnotationValue != null) + { + IODataInstanceAnnotationContainer instanceAnnotations = instanceAnnotationValue as IODataInstanceAnnotationContainer; + + if (instanceAnnotations != null) + { + deltaObject.TrySetPropertyValue(deltaObject.InstanceAnnotationsPropertyInfo.Name, instanceAnnotations); + } + } + } + + return deltaObject; + } + + private void SetProperties(TStructuralType originalObj, Delta edmDeltaEntityObject) + { + foreach (string property in edmDeltaEntityObject.GetUnchangedPropertyNames()) + { + edmDeltaEntityObject.TrySetPropertyValue(property, _clrType.GetProperty(property).GetValue(originalObj)); + } + } + + private DeltaDeletedEntityObject CreateDeletedEntityForFailedOperation(Delta changedObj) + { + Type type = typeof(DeltaDeletedEntityObject<>).MakeGenericType(changedObj.ExpectedClrType); + + DeltaDeletedEntityObject deletedObject = Activator.CreateInstance(type, _clrType, null, null, false, + changedObj.InstanceAnnotationsPropertyInfo) as DeltaDeletedEntityObject; + + foreach (string property in changedObj.GetAllPropertyNames()) + { + SetPropertyValues(changedObj, deletedObject, property); + } + + if (changedObj.InstanceAnnotationsPropertyInfo != null) + { + object annotationValue; + if (changedObj.TryGetPropertyValue(changedObj.InstanceAnnotationsPropertyInfo.Name, out annotationValue)) + { + IODataInstanceAnnotationContainer instanceAnnotations = annotationValue as IODataInstanceAnnotationContainer; + + if (instanceAnnotations != null) + { + deletedObject.TrySetPropertyValue(changedObj.InstanceAnnotationsPropertyInfo.Name, instanceAnnotations); + } + } + } + + deletedObject.TransientInstanceAnnotationContainer = changedObj.TransientInstanceAnnotationContainer; + + ValidateForDeletedEntityId(_keys, deletedObject); + + return deletedObject; + } + + //This is for ODL to work to set id as empty, because if there are missing keys, id wouldnt be set and we need to set it as empty. + // In a failed create request with no Id, we will create a DeltaDeletedEntityObject in a compensating transaction. + private static void ValidateForDeletedEntityId(IList keys, DeltaDeletedEntityObject edmDeletedObject) + { + bool hasNullKeys = false; + for (int i = 0; i < keys.Count; i++) + { + object value; + edmDeletedObject.TryGetPropertyValue(keys[i], out value); + + if (value == null) + { + hasNullKeys = true; + break; + } + } + + if (hasNullKeys) + { + edmDeletedObject.Id = new Uri(string.Empty); + } + } + + private static void SetPropertyValues(Delta changedObj, DeltaDeletedEntityObject edmDeletedObject, string property) + { + object objectVal; + if (changedObj.TryGetPropertyValue(property, out objectVal)) + { + edmDeletedObject.TrySetPropertyValue(property, objectVal); + } + } } } diff --git a/src/Microsoft.AspNet.OData.Shared/EdmChangedObjectCollection.cs b/src/Microsoft.AspNet.OData.Shared/EdmChangedObjectCollection.cs index 26f2851eea..a575ebc18b 100644 --- a/src/Microsoft.AspNet.OData.Shared/EdmChangedObjectCollection.cs +++ b/src/Microsoft.AspNet.OData.Shared/EdmChangedObjectCollection.cs @@ -5,11 +5,16 @@ // //------------------------------------------------------------------------------ +using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Diagnostics.Contracts; using System.Linq; using Microsoft.AspNet.OData.Common; +using Microsoft.AspNet.OData.Extensions; using Microsoft.OData.Edm; +using Microsoft.OData.UriParser; +using Org.OData.Core.V1; namespace Microsoft.AspNet.OData { @@ -43,7 +48,12 @@ public EdmChangedObjectCollection(IEdmEntityType entityType, IList + /// Represents EntityType of the changedobject + /// + public IEdmEntityType EntityType { get { return _entityType; } } + /// public IEdmTypeReference GetEdmType() { @@ -60,6 +70,315 @@ private void Initialize(IEdmEntityType entityType) _entityType = entityType; _edmType = new EdmDeltaCollectionType(new EdmEntityTypeReference(_entityType, isNullable: true)); _edmTypeReference = new EdmCollectionTypeReference(_edmType); + + } + + /// + /// Patch for types without underlying CLR types. + /// + /// Original collection of the type which needs to be updated. + /// ChangedObjectCollection response. + internal EdmChangedObjectCollection Patch(ICollection originalCollection) + { + EdmODataAPIHandler apiHandler = new DefaultEdmODataAPIHandler(originalCollection, _entityType); + + return CopyChangedValues(apiHandler); + } + + /// + /// Patch for EdmChangedObjectCollection, a collection for IEdmChangedObject. + /// + /// API Handler for the entity. + /// API Handler Factory. + /// ChangedObjectCollection response. + internal EdmChangedObjectCollection Patch(EdmODataAPIHandler apiHandler, EdmODataAPIHandlerFactory apiHandlerFactory) + { + return CopyChangedValues(apiHandler, apiHandlerFactory); + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling")] + internal EdmChangedObjectCollection CopyChangedValues(EdmODataAPIHandler apiHandler, EdmODataAPIHandlerFactory apiHandlerFactory = null) + { + EdmChangedObjectCollection changedObjectCollection = new EdmChangedObjectCollection(_entityType); + IEdmStructuralProperty[] keys = _entityType.Key().ToArray(); + + foreach (IEdmChangedObject changedObj in Items) + { + DataModificationOperationKind operation = DataModificationOperationKind.Update; + IEdmStructuredObject original = null; + string errorMessage = string.Empty; + string getErrorMessage = string.Empty; + IDictionary keyValues = GetKeyValues(keys, changedObj); + + try + { + EdmEntityObject deltaEntityObject = changedObj as EdmEntityObject; + + ODataAPIResponseStatus odataAPIResponseStatus = apiHandler.TryGet(deltaEntityObject.ODataPath.GetKeys(), out original, out getErrorMessage); + + EdmDeltaDeletedEntityObject deletedObj = changedObj as EdmDeltaDeletedEntityObject; + if (odataAPIResponseStatus == ODataAPIResponseStatus.Failure || (deletedObj != null && odataAPIResponseStatus == ODataAPIResponseStatus.NotFound)) + { + DataModificationExceptionType dataModificationExceptionType = new DataModificationExceptionType(operation); + dataModificationExceptionType.MessageType = new MessageType { Message = getErrorMessage }; + + deletedObj.TransientInstanceAnnotationContainer.AddResourceAnnotation(SRResources.DataModificationException, dataModificationExceptionType); + + changedObjectCollection.Add(deletedObj); + + continue; + } + + if (deletedObj != null) + { + operation = DataModificationOperationKind.Delete; + + PatchItem(deletedObj, original as EdmStructuredObject, apiHandler, apiHandlerFactory); + + if (apiHandler.TryDelete(keyValues, out errorMessage) != ODataAPIResponseStatus.Success) + { + //Handle Failed Operation - Delete + if (odataAPIResponseStatus == ODataAPIResponseStatus.Success) + { + IEdmChangedObject changedObject = HandleFailedOperation(deletedObj, operation, original, keys, errorMessage, apiHandler); + changedObjectCollection.Add(changedObject); + continue; + } + } + + changedObjectCollection.Add(deletedObj); + } + else + { + if (odataAPIResponseStatus == ODataAPIResponseStatus.NotFound) + { + operation = DataModificationOperationKind.Insert; + + if (apiHandler.TryCreate(keyValues, out original, out errorMessage) != ODataAPIResponseStatus.Success) + { + //Handle failed Operation - create + IEdmChangedObject changedObject = HandleFailedOperation(deltaEntityObject, operation, original, keys, errorMessage, apiHandler); + changedObjectCollection.Add(changedObject); + continue; + } + } + else if (odataAPIResponseStatus == ODataAPIResponseStatus.Success) + { + operation = DataModificationOperationKind.Update; + } + else + { + //Handle failed operation + IEdmChangedObject changedObject = HandleFailedOperation(deltaEntityObject, operation, null, keys, getErrorMessage, apiHandler); + changedObjectCollection.Add(changedObject); + continue; + } + + //Patch for addition/update. + PatchItem(deltaEntityObject, original as EdmStructuredObject, apiHandler, apiHandlerFactory); + + changedObjectCollection.Add(changedObj); + } + } + catch (Exception ex) + { + //Handle Failed Operation + IEdmChangedObject changedObject = HandleFailedOperation(changedObj as EdmEntityObject, operation, original, keys, ex.Message, apiHandler); + + Contract.Assert(changedObject != null); + changedObjectCollection.Add(changedObject); + } + } + + return changedObjectCollection; + } + + private static IDictionary GetKeyValues(IEdmStructuralProperty[] keys, IEdmChangedObject changedObj) + { + IDictionary keyValues = new Dictionary(); + + foreach (IEdmStructuralProperty key in keys) + { + object value; + changedObj.TryGetPropertyValue(key.Name, out value); + + if (value != null) + { + keyValues.Add(key.Name, value); + } + } + + return keyValues; + } + + private void PatchItem(EdmStructuredObject changedObj, EdmStructuredObject originalObj, EdmODataAPIHandler apiHandler, EdmODataAPIHandlerFactory apiHandlerFactory = null) + { + if (apiHandlerFactory != null && changedObj is EdmEntityObject entityObject && entityObject.ODataPath != null) + { + ApplyODataId(entityObject.ODataPath, originalObj, apiHandlerFactory); + } + + foreach (string propertyName in changedObj.GetChangedPropertyNames()) + { + ApplyProperties(changedObj, originalObj, propertyName, apiHandler, apiHandlerFactory); + } + } + + /// + /// This applies ODataId parsed Navigation paths, get the value identified by that and copy it on original object, for typeless entities + /// + private void ApplyODataId(ODataPath oDataPath, EdmStructuredObject original, EdmODataAPIHandlerFactory apiHandlerFactory) + { + EdmODataAPIHandler edmApiHandler = apiHandlerFactory.GetHandler(oDataPath); + + if (edmApiHandler == null) + { + return; + } + + IEdmStructuredObject referencedObj; + string error; + + if (edmApiHandler.TryGet(oDataPath.GetKeys(), out referencedObj, out error) == ODataAPIResponseStatus.Success) + { + EdmStructuredObject structuredObj = referencedObj as EdmStructuredObject; + + foreach (string propertyName in structuredObj.GetChangedPropertyNames()) + { + ApplyProperties(structuredObj, original, propertyName, edmApiHandler, apiHandlerFactory); + } + + foreach (string propertyName in structuredObj.GetUnchangedPropertyNames()) + { + ApplyProperties(structuredObj, original, propertyName, edmApiHandler, apiHandlerFactory); + } + } + } + + private void ApplyProperties(EdmStructuredObject changedObj, EdmStructuredObject originalObj, string propertyName, EdmODataAPIHandler apiHandler, EdmODataAPIHandlerFactory apiHandlerFactory = null) + { + object value; + if (changedObj.TryGetPropertyValue(propertyName, out value)) + { + EdmChangedObjectCollection changedColl = value as EdmChangedObjectCollection; + if (changedColl != null) + { + EdmODataAPIHandler apiHandlerNested = apiHandler.GetNestedHandler(originalObj, propertyName); + if (apiHandlerNested != null) + { + changedColl.CopyChangedValues(apiHandlerNested, apiHandlerFactory); + } + else + { + object obj; + originalObj.TryGetPropertyValue(propertyName, out obj); + + ICollection edmColl = obj as ICollection; + + changedColl.Patch(edmColl); + } + } + else + { + //call patchitem if its single structuredobj + EdmStructuredObject structuredObj = value as EdmStructuredObject; + + if (structuredObj != null) + { + object obj; + originalObj.TryGetPropertyValue(propertyName, out obj); + + EdmStructuredObject origStructuredObj = obj as EdmStructuredObject; + + if (origStructuredObj == null) + { + if (structuredObj is EdmComplexObject) + { + origStructuredObj = new EdmComplexObject(structuredObj.ActualEdmType as IEdmComplexType); + } + else + { + origStructuredObj = new EdmEntityObject(structuredObj.ActualEdmType as IEdmEntityType); + } + + originalObj.TrySetPropertyValue(propertyName, origStructuredObj); + } + + PatchItem(structuredObj, origStructuredObj, apiHandler, apiHandlerFactory); + } + else + { + originalObj.TrySetPropertyValue(propertyName, value); + } + } + } + } + + private IEdmChangedObject HandleFailedOperation(EdmEntityObject changedObj, DataModificationOperationKind operation, IEdmStructuredObject originalObj, + IEdmStructuralProperty[] keys, string errorMessage, EdmODataAPIHandler apiHandler) + { + IEdmChangedObject edmChangedObject = null; + DataModificationExceptionType dataModificationExceptionType = new DataModificationExceptionType(operation); + dataModificationExceptionType.MessageType = new MessageType { Message = errorMessage }; + + // This handles the Data Modification exception. This adds Core.DataModificationException annotation and also copy other instance annotations. + //The failed operation will be based on the protocol + switch (operation) + { + case DataModificationOperationKind.Update: + edmChangedObject = changedObj as IEdmChangedObject; + break; + case DataModificationOperationKind.Insert: + { + EdmDeltaDeletedEntityObject edmDeletedObject = new EdmDeltaDeletedEntityObject(EntityType); + PatchItem(edmDeletedObject, changedObj, apiHandler); + + ValidateForDeletedEntityId(keys, edmDeletedObject); + + edmDeletedObject.TransientInstanceAnnotationContainer = changedObj.TransientInstanceAnnotationContainer; + edmDeletedObject.PersistentInstanceAnnotationsContainer = changedObj.PersistentInstanceAnnotationsContainer; + + edmDeletedObject.AddDataException(dataModificationExceptionType); + edmChangedObject = edmDeletedObject; + break; + } + case DataModificationOperationKind.Delete: + { + EdmDeltaEntityObject edmEntityObject = new EdmDeltaEntityObject(EntityType); + PatchItem(originalObj as EdmStructuredObject, edmEntityObject, apiHandler); + + edmEntityObject.TransientInstanceAnnotationContainer = changedObj.TransientInstanceAnnotationContainer; + edmEntityObject.PersistentInstanceAnnotationsContainer = changedObj.PersistentInstanceAnnotationsContainer; + + edmEntityObject.AddDataException(dataModificationExceptionType); + edmChangedObject = edmEntityObject; + break; + } + } + + return edmChangedObject; + } + + //This is for ODL to work to set id as empty, because if there are missing keys, id wouldnt be set and we need to set it as empty. + private static void ValidateForDeletedEntityId(IEdmStructuralProperty[] keys, EdmDeltaDeletedEntityObject edmDeletedObject) + { + bool hasNullKeys = false; + for (int i = 0; i < keys.Length; i++) + { + object value; + if (edmDeletedObject.TryGetPropertyValue(keys[i].Name, out value)) + { + hasNullKeys = true; + break; + } + } + + if (hasNullKeys) + { + edmDeletedObject.Id = string.Empty; + } } } } diff --git a/src/Microsoft.AspNet.OData.Shared/EdmODataAPIHandler.cs b/src/Microsoft.AspNet.OData.Shared/EdmODataAPIHandler.cs new file mode 100644 index 0000000000..ac8ba88d5c --- /dev/null +++ b/src/Microsoft.AspNet.OData.Shared/EdmODataAPIHandler.cs @@ -0,0 +1,60 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +using System.Collections.Generic; + +namespace Microsoft.AspNet.OData +{ + /// + /// Handler class to handle user's methods for get, create, delete and updateRelatedObject. + /// This is the handler for data modification where there is no CLR type. + /// + public abstract class EdmODataAPIHandler : IODataAPIHandler + { + /// + /// Create a new object. + /// + /// Key-value pairs for the entity keys. + /// The created object (Typeless). + /// Any error message in case of an exception. + /// The status of the TryCreate method, statuses are . + public abstract ODataAPIResponseStatus TryCreate(IDictionary keyValues, out IEdmStructuredObject createdObject, out string errorMessage); + + /// + /// Get the original object based on key-value pairs. + /// + /// Key-value pairs for the entity keys. + /// Object to return. + /// Any error message in case of an exception. + /// The status of the TryGet method, statuses are . + public abstract ODataAPIResponseStatus TryGet(IDictionary keyValues, out IEdmStructuredObject originalObject, out string errorMessage); + + /// + /// Delete the object based on key-value pairs. + /// + /// Key-value pairs for the entity keys. + /// Any error message in case of an exception. + /// The status of the TryDelete method, statuses are . + public abstract ODataAPIResponseStatus TryDelete(IDictionary keyValues, out string errorMessage); + + /// + /// Add related object. + /// + /// The object to be added. + /// Any error message in case of an exception. + /// The status of the AddRelatedObject method . + public abstract ODataAPIResponseStatus TryAddRelatedObject(IEdmStructuredObject resource, out string errorMessage); + + /// + /// Get the API handler for the nested type. + /// + /// Parent instance. + /// The name of the navigation property for the handler. + /// The EdmODataApiHandler for the navigation property. + public abstract EdmODataAPIHandler GetNestedHandler(IEdmStructuredObject parent, string navigationPropertyName); + } +} diff --git a/src/Microsoft.AspNet.OData.Shared/EdmODataAPIHandlerFactory.cs b/src/Microsoft.AspNet.OData.Shared/EdmODataAPIHandlerFactory.cs new file mode 100644 index 0000000000..1ac397443f --- /dev/null +++ b/src/Microsoft.AspNet.OData.Shared/EdmODataAPIHandlerFactory.cs @@ -0,0 +1,49 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +using System; +using Microsoft.OData.Edm; +using Microsoft.OData.UriParser; + +namespace Microsoft.AspNet.OData +{ + /// + /// Factory class for OData API Handlers for typeless entities. + /// + internal abstract class EdmODataAPIHandlerFactory + { + protected EdmODataAPIHandlerFactory(IEdmModel model) + { + Model = model; + } + + /// + /// The IEdmModel for the factory. + /// + public IEdmModel Model { get; } + + /// + /// Get the handler depending on OData path. + /// + /// OData path corresponding to an @odata.id. + /// ODataAPIHandler for the specified OData path. + public abstract EdmODataAPIHandler GetHandler(ODataPath odataPath); + + /// + /// Get the handler based on the OData path uri string. + /// + /// OData path uri string. + /// ODataAPIHandler for the specified odata path uri string. + internal EdmODataAPIHandler GetHandler(string path) + { + ODataUriParser parser = new ODataUriParser(this.Model, new Uri(path, UriKind.Relative)); + ODataPath odataPath = parser.ParsePath(); + + return this.GetHandler(odataPath); + } + } +} diff --git a/src/Microsoft.AspNet.OData.Shared/Extensions/ODataPathExtensions.cs b/src/Microsoft.AspNet.OData.Shared/Extensions/ODataPathExtensions.cs new file mode 100644 index 0000000000..74f013e815 --- /dev/null +++ b/src/Microsoft.AspNet.OData.Shared/Extensions/ODataPathExtensions.cs @@ -0,0 +1,89 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNet.OData.Common; +using Microsoft.OData.UriParser; + +namespace Microsoft.AspNet.OData.Extensions +{ + /// + /// Extension methods for . + /// + internal static class ODataPathExtensions + { + /// + /// Get keys from the last . + /// + /// . + /// Dictionary of keys. + public static Dictionary GetKeys(this ODataPath path) + { + Dictionary keys = new Dictionary(); + + if (path == null) + { + throw Error.ArgumentNull(nameof(path)); + } + + if (path.Count == 0) + { + return keys; + } + + List pathSegments = path.AsList(); + + KeySegment keySegment = pathSegments.OfType().LastOrDefault(); + + if (keySegment == null) + { + return keys; + } + + keys = ODataPathHelper.KeySegmentAsDictionary(keySegment); + + return keys; + } + + /// + /// Return the last segment in the path, which is not a or . + /// + /// The . + /// An . + public static ODataPathSegment GetLastNonTypeNonKeySegment(this ODataPath path) + { + if (path == null) + { + throw Error.ArgumentNull(nameof(path)); + } + + // If the path is Employees(2)/NewFriends(2)/Namespace.MyNewFriend where Namespace.MyNewFriend is a type segment, + // This method will return NewFriends NavigationPropertySegment. + + List pathSegments = path.AsList(); + int position = path.Count - 1; + + while (position >= 0 && (pathSegments[position] is TypeSegment || pathSegments[position] is KeySegment)) + { + --position; + } + + return position < 0 ? null : pathSegments[position]; + } + + /// + /// Returns a list of in an . + /// + /// The . + /// List of . + public static List GetSegments(this ODataPath path) + { + return path.AsList(); + } + } +} diff --git a/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataResourceDeserializerHelpers.cs b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataResourceDeserializerHelpers.cs index d67e445e87..ba3b2b974e 100644 --- a/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataResourceDeserializerHelpers.cs +++ b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataResourceDeserializerHelpers.cs @@ -339,9 +339,9 @@ internal static Routing.ODataPath ApplyIdToPath(ODataDeserializerContext readCon { // Note: may be null if the payload did not include key values, // but still need to add the key so the path is semantically correct. - // Key value type is not validated, so just use string. + // Key value type is not validated, so just use empty string. // Consider adding tests to ODL to ensure we don't validate key property type in future. - keyValue = "Null"; + keyValue = string.Empty; } keys.Add(new KeyValuePair(keyName, keyValue)); @@ -371,7 +371,7 @@ internal static IList CreateKeyProperties(Uri id, ODataDeserializ return properties; } - ODataPath odataPath = ODataResourceDeserializerHelpers.GetODataPath(id.OriginalString, readContext); + ODataPath odataPath = GetODataPath(id.OriginalString, readContext); if (odataPath != null) { diff --git a/src/Microsoft.AspNet.OData.Shared/Formatter/ODataInputFormatterHelper.cs b/src/Microsoft.AspNet.OData.Shared/Formatter/ODataInputFormatterHelper.cs index 8ecd8ed6ce..947d901355 100644 --- a/src/Microsoft.AspNet.OData.Shared/Formatter/ODataInputFormatterHelper.cs +++ b/src/Microsoft.AspNet.OData.Shared/Formatter/ODataInputFormatterHelper.cs @@ -80,6 +80,7 @@ internal static object ReadFromStream( oDataReaderSettings.BaseUri = baseAddress; oDataReaderSettings.Validations = oDataReaderSettings.Validations & ~ValidationKinds.ThrowOnUndeclaredPropertyForNonOpenType; oDataReaderSettings.Version = version; + oDataReaderSettings.MaxProtocolVersion = version; IODataRequestMessage oDataRequestMessage = getODataRequestMessage(); diff --git a/src/Microsoft.AspNet.OData.Shared/IDeltaSet.cs b/src/Microsoft.AspNet.OData.Shared/IDeltaSet.cs index 32ef31963d..166f22d210 100644 --- a/src/Microsoft.AspNet.OData.Shared/IDeltaSet.cs +++ b/src/Microsoft.AspNet.OData.Shared/IDeltaSet.cs @@ -13,7 +13,7 @@ namespace Microsoft.AspNet.OData /// since we need to check in a few places (like deserializer) whether the object is a deltaset and the {TStructuralType} is not available, /// we need a marker interface which can be used in these checks. /// - internal interface IDeltaSet + public interface IDeltaSet { } } diff --git a/src/Microsoft.AspNet.OData.Shared/IODataAPIHandler.cs b/src/Microsoft.AspNet.OData.Shared/IODataAPIHandler.cs new file mode 100644 index 0000000000..2ef3a76597 --- /dev/null +++ b/src/Microsoft.AspNet.OData.Shared/IODataAPIHandler.cs @@ -0,0 +1,21 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +namespace Microsoft.AspNet.OData +{ + /// + /// Base interface for ODataAPIHandler. + /// + /// + /// This is being implemented by ODataAPIHandler{TStructuralType} which has a method returning nested ODataApiHandler. + /// A generic empty interface is needed since the nested patch handler will be of different type. + /// + public interface IODataAPIHandler + { + + } +} diff --git a/src/Microsoft.AspNet.OData.Shared/Microsoft.AspNet.OData.Shared.projitems b/src/Microsoft.AspNet.OData.Shared/Microsoft.AspNet.OData.Shared.projitems index 7c8741acdc..45e2ae0377 100644 --- a/src/Microsoft.AspNet.OData.Shared/Microsoft.AspNet.OData.Shared.projitems +++ b/src/Microsoft.AspNet.OData.Shared/Microsoft.AspNet.OData.Shared.projitems @@ -25,6 +25,7 @@ + @@ -59,6 +60,7 @@ + @@ -84,6 +86,14 @@ + + + + + + + + diff --git a/src/Microsoft.AspNet.OData.Shared/ODataAPIHandler.cs b/src/Microsoft.AspNet.OData.Shared/ODataAPIHandler.cs new file mode 100644 index 0000000000..41cf609fc0 --- /dev/null +++ b/src/Microsoft.AspNet.OData.Shared/ODataAPIHandler.cs @@ -0,0 +1,194 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Microsoft.OData.Edm; +using Microsoft.OData.UriParser; + +namespace Microsoft.AspNet.OData +{ + /// + /// The handler class for handling users' get, create, delete and updateRelatedObject methods. + /// This is the handler for data modification where there is a CLR type. + /// + public abstract class ODataAPIHandler: IODataAPIHandler where TStructuralType : class + { + /// + /// Create a new object. + /// + /// The key-value pair of the object to be created. Optional. + /// The created object. + /// Any error message in case of an exception. + /// The status of the TryCreate method . + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "2#")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "1#")] + public abstract ODataAPIResponseStatus TryCreate(IDictionary keyValues, out TStructuralType createdObject, out string errorMessage); + + /// + /// Get the original object based on key-values. + /// + /// Key-value pair for the entity keys. + /// Object to return. + /// Any error message in case of an exception. + /// The status of the TryGet method . + public abstract ODataAPIResponseStatus TryGet(IDictionary keyValues, out TStructuralType originalObject, out string errorMessage); + + /// + /// Delete the object based on key-value pairs. + /// + /// Key-value pair for the entity keys. + /// Any error message in case of an exception. + /// The status of the TryGet method . + public abstract ODataAPIResponseStatus TryDelete(IDictionary keyValues, out string errorMessage); + + /// + /// Add related object. + /// + /// The object to be added. + /// Any error message in case of an exception. + /// The status of the AddRelatedObject method . + public abstract ODataAPIResponseStatus TryAddRelatedObject(TStructuralType resource, out string errorMessage); + + /// + /// Get the ODataAPIHandler for the nested type. + /// + /// Parent instance. + /// The name of the navigation property for the handler. + /// The type of Nested ODataAPIHandler. + public abstract IODataAPIHandler GetNestedHandler(TStructuralType parent, string navigationPropertyName); + + /// + /// The parent object. + /// + internal TStructuralType ParentObject { get; set; } + + /// + /// Apply OdataId for a resource with OdataID container. + /// + /// resource to apply odata id on. + /// The model. + internal virtual void UpdateLinkedObjects(TStructuralType resource, IEdmModel model) + { + if (resource != null && model != null) + { + this.ParentObject = resource; + CheckAndApplyODataId(resource, model); + } + } + + private ODataPath GetODataPath(string path, IEdmModel model) + { + ODataUriParser parser = new ODataUriParser(model, new Uri(path, UriKind.Relative)); + ODataPath odataPath = parser.ParsePath(); + + return odataPath; + } + + private void CheckAndApplyODataId(object obj, IEdmModel model) + { + Type type = obj.GetType(); + PropertyInfo property = type.GetProperties().FirstOrDefault(s => s.PropertyType == typeof(ODataIdContainer)); + if (property != null && property.GetValue(obj) is ODataIdContainer container && container != null) + { + ODataPath odataPath = GetODataPath(container.ODataId, model); + object res = ApplyODataIdOnContainer(obj, odataPath); + foreach (PropertyInfo prop in type.GetProperties()) + { + object resVal = prop.GetValue(res); + + if (resVal != null) + { + prop.SetValue(obj, resVal); + } + } + } + else + { + foreach (PropertyInfo prop in type.GetProperties().Where(p => !p.PropertyType.IsPrimitive)) + { + object propVal = prop.GetValue(obj); + if (propVal == null) + { + continue; + } + + if (propVal is IEnumerable lst) + { + foreach (object item in lst) + { + if (item.GetType().IsPrimitive) + { + break; + } + + CheckAndApplyODataId(item, model); + } + } + else + { + CheckAndApplyODataId(propVal, model); + } + } + } + } + + private object ApplyODataIdOnContainer(object obj, ODataPath odataPath) + { + KeySegment keySegment = odataPath.LastOrDefault() as KeySegment; + Dictionary keys = keySegment?.Keys.ToDictionary(x => x.Key, x => x.Value); + if (keys != null) + { + TStructuralType returnedObject; + string error; + if (this.ParentObject.Equals(obj)) + { + if (this.TryGet(keys, out returnedObject, out error) == ODataAPIResponseStatus.Success) + { + return returnedObject; + } + else + { + if (this.TryCreate(keys, out returnedObject, out error) == ODataAPIResponseStatus.Success) + { + return returnedObject; + } + else + { + return null; + } + } + } + else + { + IODataAPIHandler apiHandlerNested = this.GetNestedHandler(this.ParentObject, odataPath.LastSegment.Identifier); + object[] getParams = new object[] { keys, null, null }; + if (apiHandlerNested.GetType().GetMethod(nameof(TryGet)).Invoke(apiHandlerNested, getParams).Equals(ODataAPIResponseStatus.Success)) + { + return getParams[1]; + } + else + { + if (apiHandlerNested.GetType().GetMethod(nameof(TryCreate)).Invoke(apiHandlerNested, getParams).Equals(ODataAPIResponseStatus.Success)) + { + return getParams[1]; + } + else + { + return null; + } + } + } + } + + return null; + } + } +} diff --git a/src/Microsoft.AspNet.OData.Shared/ODataAPIHandlerFactory.cs b/src/Microsoft.AspNet.OData.Shared/ODataAPIHandlerFactory.cs new file mode 100644 index 0000000000..4c0a69fb0f --- /dev/null +++ b/src/Microsoft.AspNet.OData.Shared/ODataAPIHandlerFactory.cs @@ -0,0 +1,53 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +using System; +using Microsoft.OData.Edm; +using Microsoft.OData.UriParser; + +namespace Microsoft.AspNet.OData +{ + /// + /// Factory class for OData API handlers for entities mapped to CLR types. + /// + public abstract class ODataAPIHandlerFactory + { + /// + /// Creates an instance of an ODataAPIHandlerFactory with the given model. + /// + /// The IEdmModel for the API handler factory. + protected ODataAPIHandlerFactory(IEdmModel model) + { + Model = model; + } + + /// + /// The IEdmModel for the factory. + /// + public IEdmModel Model { get; } + + /// + /// Get the handler depending on OData path. + /// + /// OData path corresponding to an @odata.id. + /// ODataAPIHandler for the specified OData path. + public abstract IODataAPIHandler GetHandler(ODataPath odataPath); + + /// + /// Get the handler based on the OData path uri string. + /// + /// OData path uri string. + /// ODataAPIHandler for the specified odata path uri string. + internal IODataAPIHandler GetHandler(string path) + { + ODataUriParser parser = new ODataUriParser(this.Model, new Uri(path, UriKind.Relative)); + ODataPath odataPath = parser.ParsePath(); + + return this.GetHandler(odataPath); + } + } +} diff --git a/src/Microsoft.AspNet.OData.Shared/ODataAPIResponseStatus.cs b/src/Microsoft.AspNet.OData.Shared/ODataAPIResponseStatus.cs new file mode 100644 index 0000000000..45f3fd39e4 --- /dev/null +++ b/src/Microsoft.AspNet.OData.Shared/ODataAPIResponseStatus.cs @@ -0,0 +1,28 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +namespace Microsoft.AspNet.OData +{ + /// + /// Enum for Patch status. + /// + public enum ODataAPIResponseStatus + { + /// + /// Success status. + /// + Success, + /// + /// Failure status. + /// + Failure, + /// + /// Resource not found. + /// + NotFound + } +} diff --git a/src/Microsoft.AspNet.OData/Microsoft.AspNet.OData.csproj b/src/Microsoft.AspNet.OData/Microsoft.AspNet.OData.csproj index 6316ef1bec..026eb67593 100644 --- a/src/Microsoft.AspNet.OData/Microsoft.AspNet.OData.csproj +++ b/src/Microsoft.AspNet.OData/Microsoft.AspNet.OData.csproj @@ -21,6 +21,9 @@ + + ..\..\sln\packages\CompareNETObjects.4.78.0\lib\net45\KellermanSoftware.Compare-NET-Objects.dll + ..\..\sln\packages\Microsoft.Extensions.DependencyInjection.1.0.0\lib\netstandard1.1\Microsoft.Extensions.DependencyInjection.dll diff --git a/src/Microsoft.AspNet.OData/packages.config b/src/Microsoft.AspNet.OData/packages.config index ed1042cb4e..1c9b8177c9 100644 --- a/src/Microsoft.AspNet.OData/packages.config +++ b/src/Microsoft.AspNet.OData/packages.config @@ -1,5 +1,6 @@  + diff --git a/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.csproj b/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.csproj index ec685d7d1d..073b329e8b 100644 --- a/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.csproj +++ b/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.csproj @@ -41,6 +41,7 @@ + diff --git a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNet/Microsoft.Test.E2E.AspNet.OData.csproj b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNet/Microsoft.Test.E2E.AspNet.OData.csproj index 4e6f47d4a0..f26146d159 100644 --- a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNet/Microsoft.Test.E2E.AspNet.OData.csproj +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNet/Microsoft.Test.E2E.AspNet.OData.csproj @@ -27,6 +27,9 @@ ..\..\..\..\sln\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.SqlServer.dll True + + ..\..\..\..\sln\packages\CompareNETObjects.4.78.0\lib\net452\KellermanSoftware.Compare-NET-Objects.dll + ..\..\..\..\sln\packages\Microsoft.Data.Edm.5.8.4\lib\net40\Microsoft.Data.Edm.dll True diff --git a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNet/packages.config b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNet/packages.config index bb6206205f..51d319a09d 100644 --- a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNet/packages.config +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNet/packages.config @@ -1,5 +1,6 @@  + diff --git a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNetCore/Microsoft.Test.E2E.AspNetCore.OData.csproj b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNetCore/Microsoft.Test.E2E.AspNetCore.OData.csproj index 5231c901fe..1602733856 100644 --- a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNetCore/Microsoft.Test.E2E.AspNetCore.OData.csproj +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNetCore/Microsoft.Test.E2E.AspNetCore.OData.csproj @@ -1402,6 +1402,21 @@ OpenType\TypedTest.cs + + BulkOperation\BulkOperationTest.cs + + + BulkOperation\BulkOperationDataModel.cs + + + BulkOperation\BulkOperationEdmModel.cs + + + BulkOperation\BulkOperationController.cs + + + BulkOperation\BulkOperationPatchHandlers.cs + ParameterAlias\ParameterAliasDataSource.cs @@ -1662,6 +1677,9 @@ ..\..\..\..\sln\packages\EntityFramework.6.2.0\lib\net45\EntityFramework.SqlServer.dll + + ..\..\..\..\sln\packages\CompareNETObjects.4.78.0\lib\net46\KellermanSoftware.Compare-NET-Objects.dll + ..\..\..\..\sln\packages\Microsoft.AspNetCore.Authentication.Abstractions.2.0.1\lib\netstandard2.0\Microsoft.AspNetCore.Authentication.Abstractions.dll diff --git a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNetCore/packages.config b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNetCore/packages.config index 8bfa286a8e..599d3b625c 100644 --- a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNetCore/packages.config +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNetCore/packages.config @@ -1,5 +1,6 @@  + diff --git a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNetCore3x/BulkOperation/EFTests/BulkOperationControllerEF.cs b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNetCore3x/BulkOperation/EFTests/BulkOperationControllerEF.cs new file mode 100644 index 0000000000..aa87466576 --- /dev/null +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNetCore3x/BulkOperation/EFTests/BulkOperationControllerEF.cs @@ -0,0 +1,200 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +using System; +using System.Collections.Generic; +using System.Diagnostics.Contracts; +using System.Linq; +using Microsoft.AspNet.OData; +using Microsoft.AspNet.OData.Extensions; +using Microsoft.AspNet.OData.Routing; +using Microsoft.EntityFrameworkCore; +using Microsoft.Test.E2E.AspNet.OData.Common.Controllers; +using Xunit; +using static Microsoft.Test.E2E.AspNet.OData.BulkOperation.APIHandlerFactoryEF; + +namespace Microsoft.Test.E2E.AspNet.OData.BulkOperation +{ + public class EmployeesControllerEF : TestODataController + { + public EmployeesControllerEF() + { + + } + + public static List employees; + public static List friends; + + internal DbSet GenerateData(EmployeeDBContext context) + { + if (context.Employees.Any()) + { + return context.Employees; + } + + var friends = GenerateDataOrders(context); + + employees = new List(); + employees.Add(new Employee { ID = 1, Name = "Employee1", Friends = friends.Where(x => x.Id == 1 || x.Id == 2).ToList() }); + employees.Add(new Employee { ID = 2, Name = "Employee2", Friends = friends.Where(x => x.Id == 3 || x.Id == 4).ToList() }); + employees.Add(new Employee { ID = 3, Name = "Employee3", Friends = friends.Where(x => x.Id == 5 || x.Id == 6).ToList() }); + + context.Employees.AddRange(employees); + + context.SaveChanges(); + + return context.Employees; + } + + internal DbSet GenerateDataOrders(EmployeeDBContext context) + { + if (context.Friends.Any()) + { + return context.Friends; + } + + friends = new List(); + friends.Add(new Friend { Id = 1, Age = 10, Orders = new List() { new Order { Id = 1, Price = 5 }, new Order { Id = 2, Price = 5 } } }); + friends.Add(new Friend { Id = 2, Age = 20, Orders = new List() { new Order { Id = 10, Price = 5 }, new Order { Id = 20, Price = 5 } } }); + friends.Add(new Friend { Id = 3, Age = 30, Orders = new List() { new Order { Id = 3, Price = 5 }, new Order { Id = 4, Price = 5 } } }); + friends.Add(new Friend { Id = 4, Age = 40, Orders = new List() { new Order { Id = 30, Price = 5 }, new Order { Id = 40, Price = 5 } } }); + friends.Add(new Friend { Id = 5, Age = 50, Orders = new List() { new Order { Id = 5, Price = 5 }, new Order { Id = 6, Price = 5 } } }); + friends.Add(new Friend { Id = 6, Age = 60, Orders = new List() { new Order { Id = 50, Price = 5 }, new Order { Id = 60, Price = 5 } } }); + + context.Friends.AddRange(friends); + + context.SaveChanges(); + + return context.Friends; + } + + + [ODataRoute("Employees")] + [HttpPatch] + public ITestActionResult PatchEmployees([FromBody] DeltaSet coll) + { + using (var dbContext = CreateDbContext()) + { + GenerateData(dbContext); + + Assert.NotNull(coll); + + var returncoll = coll.Patch(new EmployeeEFPatchHandler(dbContext), new APIHandlerFactoryEF(Request.GetModel(), dbContext)); + + + return Ok(returncoll); + } + } + + private EmployeeDBContext CreateDbContext() + { + var buiilder = new DbContextOptionsBuilder().UseInMemoryDatabase(Guid.NewGuid().ToString()); + var dbContext = new EmployeeDBContext(buiilder.Options); + return dbContext; + } + + [ODataRoute("Employees({key})")] + public ITestActionResult Patch(int key, [FromBody] Delta delta) + { + using (var dbContext = CreateDbContext()) + { + GenerateData(dbContext); + + delta.TrySetPropertyValue("ID", key); // It is the key property, and should not be updated. + object obj; + delta.TryGetPropertyValue("Friends", out obj); + + var employee = dbContext.Employees.First(x => x.ID == key); + + try + { + delta.Patch(employee, new EmployeeEFPatchHandler(dbContext), new APIHandlerFactoryEF(Request.GetModel(), dbContext)); + + } + catch (ArgumentException ae) + { + return BadRequest(ae.Message); + } + + employee = dbContext.Employees.First(x => x.ID == key); + + ValidateFriends(key, employee); + + return Ok(employee); + } + } + + private static void ValidateFriends(int key, Employee employee) + { + if (key == 1 && employee.Name == "SqlUD") + { + Contract.Assert(employee.Friends.Count == 2); + Contract.Assert(employee.Friends[0].Id == 2); + Contract.Assert(employee.Friends[1].Id == 3); + } + else if (key == 1 && employee.Name == "SqlFU") + { + Contract.Assert(employee.Friends.Count == 3); + Contract.Assert(employee.Friends[0].Id == 345); + Contract.Assert(employee.Friends[1].Id == 400); + Contract.Assert(employee.Friends[2].Id == 900); + } + else if (key == 1 && employee.Name == "SqlMU") + { + Contract.Assert(employee.Friends.Count == 3); + Contract.Assert(employee.Friends[0].Id == 2); + Contract.Assert(employee.Friends[1].Id == 1); + Contract.Assert(employee.Friends[1].Name == "Test_1"); + Contract.Assert(employee.Friends[2].Id == 3); + } + else if (key == 1 && employee.Name == "SqlMU1") + { + Contract.Assert(employee.Friends.Count == 2); + Contract.Assert(employee.Friends[0].Id == 2); + Contract.Assert(employee.Friends[1].Id == 3); + } + } + + [ODataRoute("Employees({key})/Friends")] + [HttpPatch] + public ITestActionResult PatchFriends(int key, [FromBody] DeltaSet friendColl) + { + using (var dbContext = CreateDbContext()) + { + GenerateData(dbContext); + + Employee originalEmployee = dbContext.Employees.SingleOrDefault(c => c.ID == key); + Assert.NotNull(originalEmployee); + + var changedObjColl = friendColl.Patch(originalEmployee.Friends); + + return Ok(changedObjColl); + } + } + + public ITestActionResult Get(int key) + { + using (var dbContext = CreateDbContext()) + { + var emp = dbContext.Employees.SingleOrDefault(e => e.ID == key); + return Ok(emp); + } + } + + [ODataRoute("Employees({key})/Friends")] + public ITestActionResult GetFriends(int key) + { + using (var dbContext = CreateDbContext()) + { + var emp = dbContext.Employees.SingleOrDefault(e => e.ID == key); + return Ok(emp.Friends); + } + } + + + } +} \ No newline at end of file diff --git a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNetCore3x/BulkOperation/EFTests/BulkOperationPatchHandlersEF.cs b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNetCore3x/BulkOperation/EFTests/BulkOperationPatchHandlersEF.cs new file mode 100644 index 0000000000..2bf7f987c7 --- /dev/null +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNetCore3x/BulkOperation/EFTests/BulkOperationPatchHandlersEF.cs @@ -0,0 +1,430 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNet.OData; +using Microsoft.EntityFrameworkCore; +using Microsoft.OData.Edm; +using Microsoft.OData.UriParser; + +namespace Microsoft.Test.E2E.AspNet.OData.BulkOperation +{ + internal class EmployeeDBContext : DbContext + { + public EmployeeDBContext() + { + + } + + public EmployeeDBContext(DbContextOptions options) : base(options) + { + + } + + public DbSet Employees { get; set; } + public DbSet Friends { get; set; } + + protected override void OnModelCreating(Microsoft.EntityFrameworkCore.ModelBuilder modelBuilder) + { + modelBuilder.Entity().HasKey(c => c.ID); + modelBuilder.Entity().Ignore(c => c.SkillSet); + modelBuilder.Entity().Ignore(c => c.NewFriends); + modelBuilder.Entity().Ignore(c => c.UnTypedFriends); + modelBuilder.Entity().Ignore(c => c.InstanceAnnotations); + modelBuilder.Entity().Ignore(c => c.FavoriteSports); + + modelBuilder.Entity().Ignore(c => c.InstanceAnnotations); + modelBuilder.Entity().Ignore(c => c.InstanceAnnotations); + + modelBuilder.Entity().HasKey(c => c.Id); + + modelBuilder.Entity().Ignore(c => c.Container); + modelBuilder.Entity().Ignore(c => c.Container); + } + } + + internal class APIHandlerFactoryEF : ODataAPIHandlerFactory + { + EmployeeDBContext dbContext; + + public APIHandlerFactoryEF(IEdmModel model, EmployeeDBContext dbContext) : base(model) + { + this.dbContext = dbContext; + } + + public override IODataAPIHandler GetHandler(ODataPath odataPath) + { + if (odataPath != null) + { + var pathItems = odataPath; + } + + return null; + } + + internal class EmployeeEFPatchHandler : ODataAPIHandler + { + EmployeeDBContext dbContext = null; + + public EmployeeEFPatchHandler(EmployeeDBContext dbContext) + { + this.dbContext = dbContext; + } + + public override ODataAPIResponseStatus TryCreate(IDictionary keyValues, out Employee createdObject, out string errorMessage) + { + createdObject = null; + errorMessage = string.Empty; + + try + { + createdObject = new Employee(); + dbContext.Employees.Add(createdObject); + + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + + public override ODataAPIResponseStatus TryDelete(IDictionary keyValues, out string errorMessage) + { + errorMessage = string.Empty; + + try + { + var id = keyValues.First().Value.ToString(); + var customer = dbContext.Employees.First(x => x.ID == Int32.Parse(id)); + + dbContext.Employees.Remove(customer); + + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + + public override ODataAPIResponseStatus TryGet(IDictionary keyValues, out Employee originalObject, out string errorMessage) + { + ODataAPIResponseStatus status = ODataAPIResponseStatus.Success; + errorMessage = string.Empty; + originalObject = null; + + try + { + var id = keyValues["ID"].ToString(); + originalObject = dbContext.Employees.First(x => x.ID == Int32.Parse(id)); + + if (originalObject == null) + { + status = ODataAPIResponseStatus.NotFound; + } + + } + catch (Exception ex) + { + status = ODataAPIResponseStatus.Failure; + errorMessage = ex.Message; + } + + return status; + } + + public override IODataAPIHandler GetNestedHandler(Employee parent, string navigationPropertyName) + { + switch (navigationPropertyName) + { + case "Friends": + return new FriendEFPatchHandler(parent); + case "NewFriends": + return new NewFriendEFPatchHandler(parent); + default: + return null; + } + } + + public override ODataAPIResponseStatus TryAddRelatedObject(Employee resource, out string errorMessage) + { + throw new NotImplementedException(); + } + } + + internal class FriendEFPatchHandler : ODataAPIHandler + { + Employee employee; + public FriendEFPatchHandler(Employee employee) + { + this.employee = employee; + } + + public override ODataAPIResponseStatus TryCreate(IDictionary keyValues, out Friend createdObject, out string errorMessage) + { + createdObject = null; + errorMessage = string.Empty; + + try + { + createdObject = new Friend(); + employee.Friends.Add(createdObject); + + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + + public override ODataAPIResponseStatus TryDelete(IDictionary keyValues, out string errorMessage) + { + errorMessage = string.Empty; + + try + { + var id = keyValues.First().Value.ToString(); + var friend = employee.Friends.First(x => x.Id == Int32.Parse(id)); + + employee.Friends.Remove(friend); + + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + + public override ODataAPIResponseStatus TryGet(IDictionary keyValues, out Friend originalObject, out string errorMessage) + { + ODataAPIResponseStatus status = ODataAPIResponseStatus.Success; + errorMessage = string.Empty; + originalObject = null; + + try + { + var id = keyValues["Id"].ToString(); + if (employee.Friends == null) + { + status = ODataAPIResponseStatus.NotFound; + } + else + { + originalObject = employee.Friends.FirstOrDefault(x => x.Id == Int32.Parse(id)); + } + + if (originalObject == null) + { + status = ODataAPIResponseStatus.NotFound; + } + + } + catch (Exception ex) + { + status = ODataAPIResponseStatus.Failure; + errorMessage = ex.Message; + } + + return status; + } + + public override IODataAPIHandler GetNestedHandler(Friend parent, string navigationPropertyName) + { + return new OrderEFPatchHandler(parent); + } + + public override ODataAPIResponseStatus TryAddRelatedObject(Friend resource, out string errorMessage) + { + throw new NotImplementedException(); + } + } + + internal class NewFriendEFPatchHandler : ODataAPIHandler + { + Employee employee; + public NewFriendEFPatchHandler(Employee employee) + { + this.employee = employee; + } + + public override ODataAPIResponseStatus TryCreate(IDictionary keyValues, out NewFriend createdObject, out string errorMessage) + { + createdObject = null; + errorMessage = string.Empty; + + try + { + createdObject = new NewFriend(); + employee.NewFriends.Add(createdObject); + + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + + public override ODataAPIResponseStatus TryDelete(IDictionary keyValues, out string errorMessage) + { + errorMessage = string.Empty; + + try + { + var id = keyValues.First().Value.ToString(); + var friend = employee.NewFriends.First(x => x.Id == Int32.Parse(id)); + + employee.NewFriends.Remove(friend); + + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + + public override ODataAPIResponseStatus TryGet(IDictionary keyValues, out NewFriend originalObject, out string errorMessage) + { + ODataAPIResponseStatus status = ODataAPIResponseStatus.Success; + errorMessage = string.Empty; + originalObject = null; + + try + { + var id = keyValues["Id"].ToString(); + originalObject = employee.NewFriends.First(x => x.Id == Int32.Parse(id)); + + if (originalObject == null) + { + status = ODataAPIResponseStatus.NotFound; + } + + } + catch (Exception ex) + { + status = ODataAPIResponseStatus.Failure; + errorMessage = ex.Message; + } + + return status; + } + + public override IODataAPIHandler GetNestedHandler(NewFriend parent, string navigationPropertyName) + { + return null; + } + + public override ODataAPIResponseStatus TryAddRelatedObject(NewFriend resource, out string errorMessage) + { + throw new NotImplementedException(); + } + } + + internal class OrderEFPatchHandler : ODataAPIHandler + { + Friend friend; + public OrderEFPatchHandler(Friend friend) + { + this.friend = friend; + } + + public override ODataAPIResponseStatus TryCreate(IDictionary keyValues, out Order createdObject, out string errorMessage) + { + createdObject = null; + errorMessage = string.Empty; + + try + { + createdObject = new Order(); + friend.Orders.Add(createdObject); + + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + + public override ODataAPIResponseStatus TryDelete(IDictionary keyValues, out string errorMessage) + { + errorMessage = string.Empty; + + try + { + var id = keyValues.First().Value.ToString(); + var order = friend.Orders.First(x => x.Id == Int32.Parse(id)); + + friend.Orders.Remove(order); + + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + + public override ODataAPIResponseStatus TryGet(IDictionary keyValues, out Order originalObject, out string errorMessage) + { + ODataAPIResponseStatus status = ODataAPIResponseStatus.Success; + errorMessage = string.Empty; + originalObject = null; + + try + { + var id = keyValues["Id"].ToString(); + originalObject = friend.Orders.First(x => x.Id == Int32.Parse(id)); + + if (originalObject == null) + { + status = ODataAPIResponseStatus.NotFound; + } + } + catch (Exception ex) + { + status = ODataAPIResponseStatus.Failure; + errorMessage = ex.Message; + } + + return status; + } + + public override IODataAPIHandler GetNestedHandler(Order parent, string navigationPropertyName) + { + throw new NotImplementedException(); + } + + public override ODataAPIResponseStatus TryAddRelatedObject(Order resource, out string errorMessage) + { + throw new NotImplementedException(); + } + } + } +} diff --git a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNetCore3x/BulkOperation/EFTests/BulkOperationTestEF.cs b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNetCore3x/BulkOperation/EFTests/BulkOperationTestEF.cs new file mode 100644 index 0000000000..12f2bb6e04 --- /dev/null +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNetCore3x/BulkOperation/EFTests/BulkOperationTestEF.cs @@ -0,0 +1,285 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNet.OData; +using Microsoft.AspNet.OData.Extensions; +using Microsoft.AspNet.OData.Routing; +using Microsoft.AspNet.OData.Routing.Conventions; +using Microsoft.Test.E2E.AspNet.OData.Common.Execution; +using Microsoft.Test.E2E.AspNet.OData.Common.Extensions; +using Newtonsoft.Json.Linq; +using Xunit; + +namespace Microsoft.Test.E2E.AspNet.OData.BulkOperation +{ + public class BulkOperationTestEF : WebHostTestBase + { + public BulkOperationTestEF(WebHostTestFixture fixture) + : base(fixture) + { + } + + protected override void UpdateConfiguration(WebRouteConfiguration configuration) + { + var controllers = new[] { typeof(EmployeesControllerEF), typeof(MetadataController) }; + configuration.AddControllers(controllers); + + configuration.Routes.Clear(); + configuration.Count().Filter().OrderBy().Expand().MaxTop(null).Select(); + configuration.MapODataServiceRoute("convention", "convention", BulkOperationEdmModel.GetConventionModel(configuration)); + configuration.MapODataServiceRoute("explicit", "explicit", BulkOperationEdmModel.GetExplicitModel(configuration), new DefaultODataPathHandler(), ODataRoutingConventions.CreateDefault()); + configuration.EnsureInitialized(); + + } + + + #region Update + + [Fact] + public async Task PatchEmployee_WithUpdates() + { + //Arrange + string requestUri = this.BaseAddress + "/convention/Employees(1)"; + + var content = @"{ + 'Name':'Sql' , + 'Friends@odata.delta':[{'Id':1,'Name':'Test2'},{'Id':2,'Name':'Test3'}] + }"; + + var requestForPost = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + + StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + requestForPost.Content = stringContent; + + //Act & Assert + using (HttpResponseMessage response = await this.Client.SendAsync(requestForPost)) + { + var json = await response.Content.ReadAsObject(); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Contains("Sql", json.ToString()); + } + } + + [Fact] + public async Task PatchEmployee_WithUpdates_WithEmployees() + { + //Arrange + + string requestUri = this.BaseAddress + "/convention/Employees(1)"; + + var content = @"{ + 'Name':'SqlFU' , + 'Friends':[{'Id':345,'Name':'Test2'},{'Id':400,'Name':'Test3'},{'Id':900,'Name':'Test93'}] + }"; + + var requestForPost = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + + StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + requestForPost.Content = stringContent; + + //Act & Assert + using (HttpResponseMessage response = await this.Client.SendAsync(requestForPost)) + { + var json = await response.Content.ReadAsObject(); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Contains("SqlFU", json.ToString()); + } + } + + [Fact] + public async Task PatchEmployee_WithUpdates_Employees() + { + //Arrange + + string requestUri = this.BaseAddress + "/convention/Employees"; + + var content = @"{'@odata.context':'" + this.BaseAddress + @"/convention/$metadata#Employees/$delta', + 'value':[{ '@odata.type': '#Microsoft.Test.E2E.AspNet.OData.BulkOperation.Employee', 'ID':1,'Name':'Employee1', + 'Friends@odata.delta':[{'Id':1,'Name':'Friend1', + 'Orders@odata.delta' :[{'Id':1,'Price': 10}, {'Id':2,'Price': 20} ] },{'Id':2,'Name':'Friend2'}] + }, + { '@odata.type': '#Microsoft.Test.E2E.AspNet.OData.BulkOperation.Employee', 'ID':2,'Name':'Employee2', + 'Friends@odata.delta':[{'Id':3,'Name':'Friend3', + 'Orders@odata.delta' :[{'Id':3,'Price': 30}, {'Id':4,'Price': 40} ]},{'Id':4,'Name':'Friend4'}] + }] + }"; + + var requestForPost = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + requestForPost.Headers.Add("OData-Version", "4.01"); + requestForPost.Headers.Add("OData-MaxVersion", "4.01"); + + StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + requestForPost.Content = stringContent; + + // Act & Assert + var expected = "$delta\",\"value\":[{\"ID\":1,\"Name\":\"Employee1\",\"SkillSet\":[],\"Gender\":\"0\",\"AccessLevel\":" + + "\"0\",\"FavoriteSports\":null,\"Friends@delta\":[{\"Id\":1,\"Name\":\"Friend1\",\"Age\":0,\"Orders@delta\":[{\"Id\":1,\"Price\":10},{\"Id\":2,\"Price\":20}]},{\"Id\":2,\"Name\":" + + "\"Friend2\",\"Age\":0}]},{\"ID\":2,\"Name\":\"Employee2\",\"SkillSet\":[],\"Gender\":\"0\",\"AccessLevel\":\"0\",\"FavoriteSports\":null,\"Friends@delta\":" + + "[{\"Id\":3,\"Name\":\"Friend3\",\"Age\":0,\"Orders@delta\":[{\"Id\":3,\"Price\":30},{\"Id\":4,\"Price\":40}]},{\"Id\":4,\"Name\":\"Friend4\",\"Age\":0}]}]}"; + + using (HttpResponseMessage response = await this.Client.SendAsync(requestForPost)) + { + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var json = response.Content.ReadAsStringAsync().Result; + Assert.Contains(expected, json.ToString()); + Assert.Contains("Employee1", json); + Assert.Contains("Employee2", json); + } + } + + [Fact] + public async Task PatchEmployee_WithUpdates_Employees_InV4() + { + //Arrange + string requestUri = this.BaseAddress + "/convention/Employees"; + + var content = @"{'@odata.context':'" + this.BaseAddress + @"/convention/$metadata#Employees/$delta', + 'value':[{ '@odata.type': '#Microsoft.Test.E2E.AspNet.OData.BulkOperation.Employee', 'ID':1,'Name':'Employee1', + 'Friends@odata.delta':[{'Id':1,'Name':'Friend1', + 'Orders@odata.delta' :[{'Id':1,'Price': 10}, {'Id':2,'Price': 20} ] },{'Id':2,'Name':'Friend2'}] + }, + { '@odata.type': '#Microsoft.Test.E2E.AspNet.OData.BulkOperation.Employee', 'ID':2,'Name':'Employee2', + 'Friends@odata.delta':[{'Id':3,'Name':'Friend3', + 'Orders@odata.delta' :[{'Id':3,'Price': 30}, {'Id':4,'Price': 40} ]},{'Id':4,'Name':'Friend4'}] + }] + }"; + + var requestForPatch = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + requestForPatch.Headers.Add("OData-Version", "4.0"); + requestForPatch.Headers.Add("OData-MaxVersion", "4.0"); + + StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + requestForPatch.Content = stringContent; + + // Act & Assert + var expected = "$delta\",\"value\":[{\"ID\":1,\"Name\":\"Employee1\",\"SkillSet\":[],\"Gender\":\"0\",\"AccessLevel\":" + + "\"0\",\"FavoriteSports\":null,\"Friends@delta\":[{\"Id\":1,\"Name\":\"Friend1\",\"Age\":0,\"Orders@delta\":[{\"Id\":1,\"Price\":10},{\"Id\":2,\"Price\":20}]},{\"Id\":2,\"Name\":" + + "\"Friend2\",\"Age\":0}]},{\"ID\":2,\"Name\":\"Employee2\",\"SkillSet\":[],\"Gender\":\"0\",\"AccessLevel\":\"0\",\"FavoriteSports\":null,\"Friends@delta\":" + + "[{\"Id\":3,\"Name\":\"Friend3\",\"Age\":0,\"Orders@delta\":[{\"Id\":3,\"Price\":30},{\"Id\":4,\"Price\":40}]},{\"Id\":4,\"Name\":\"Friend4\",\"Age\":0}]}]}"; + + using (HttpResponseMessage response = await this.Client.SendAsync(requestForPatch)) + { + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var json = response.Content.ReadAsStringAsync().Result; + Assert.Contains(expected, json.ToString()); + Assert.Contains("Employee1", json); + Assert.Contains("Employee2", json); + } + } + + [Fact] + public async Task PatchEmployee_WithDelete() + { + //Arrange + + string requestUri = this.BaseAddress + "/convention/Employees(1)"; + + var content = @"{ + 'Name':'Sql', + 'Friends@odata.delta':[{ '@odata.removed' : {'reason':'changed'}, 'Id':1}] + }"; + + var requestForPost = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + + StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + requestForPost.Content = stringContent; + + //Act & Assert + using (HttpResponseMessage response = await this.Client.SendAsync(requestForPost)) + { + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var json = response.Content.ReadAsStringAsync().Result; + Assert.Contains("Sql", json); + } + } + + [Fact] + public async Task PatchEmployee_WithAddUpdateAndDelete() + { + //Arrange + + string requestUri = this.BaseAddress + "/convention/Employees(1)"; + + var content = @"{ + 'Name':'SqlUD', + 'Friends@odata.delta':[{ '@odata.removed' : {'reason':'changed'}, 'Id':1},{'Id':2,'Name':'Test3'},{'Id':3,'Name':'Test4'}] + }"; + + var requestForPost = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + + StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + requestForPost.Content = stringContent; + + //Act & Assert + using (HttpResponseMessage response = await this.Client.SendAsync(requestForPost)) + { + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var json = response.Content.ReadAsStringAsync().Result; + Assert.Contains("SqlUD", json); + } + } + + [Fact] + public async Task PatchEmployee_WithMultipleFriendUpdatesAndOneDelete() + { + //Arrange + + string requestUri = this.BaseAddress + "/convention/Employees(1)"; + + var content = @"{ + 'Name':'SqlMU' , + 'Friends@odata.delta':[{ '@odata.removed' : {'reason':'changed'}, 'Id':1},{'Id':1,'Name':'Test_1'},{'Id':2,'Name':'Test3'},{'Id':3,'Name':'Test4'}] + }"; + + var requestForPost = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + + StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + requestForPost.Content = stringContent; + + //Act & Assert + using (HttpResponseMessage response = await this.Client.SendAsync(requestForPost)) + { + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var json = response.Content.ReadAsStringAsync().Result; + Assert.Contains("SqlMU", json); + } + } + + [Fact] + public async Task PatchEmployee_WithMultipleFriendUpdatesAndMultipleDelete() + { + //Arrange + + string requestUri = this.BaseAddress + "/convention/Employees(1)"; + + var content = @"{ + 'Name':'SqlMU1' , + 'Friends@odata.delta':[{ '@odata.removed' : {'reason':'changed'}, 'Id':1},{'Id':1,'Name':'Test_1'},{'Id':2,'Name':'Test3'},{'Id':3,'Name':'Test4'},{ '@odata.removed' : {'reason':'changed'}, 'Id':1}] + }"; + + var requestForPost = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + + StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + requestForPost.Content = stringContent; + + //Act & Assert + using (HttpResponseMessage response = await this.Client.SendAsync(requestForPost)) + { + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var json = response.Content.ReadAsStringAsync().Result; + Assert.Contains("SqlMU1", json); + } + } + + #endregion + } +} \ No newline at end of file diff --git a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNetCore3x/Microsoft.Test.E2E.AspNetCore3x.OData.csproj b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNetCore3x/Microsoft.Test.E2E.AspNetCore3x.OData.csproj index 81b6c9853a..3004804f9c 100644 --- a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNetCore3x/Microsoft.Test.E2E.AspNetCore3x.OData.csproj +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNetCore3x/Microsoft.Test.E2E.AspNetCore3x.OData.csproj @@ -20,6 +20,7 @@ + @@ -1664,6 +1665,21 @@ Validation\DeltaOfTValidationTests.cs + + BulkOperation\BulkOperationTest.cs + + + BulkOperation\BulkOperationDataModel.cs + + + BulkOperation\BulkOperationEdmModel.cs + + + BulkOperation\BulkOperationController.cs + + + BulkOperation\BulkOperationPatchHandlers.cs + diff --git a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkOperationController.cs b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkOperationController.cs new file mode 100644 index 0000000000..4867df1938 --- /dev/null +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkOperationController.cs @@ -0,0 +1,537 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNet.OData; +using Microsoft.AspNet.OData.Extensions; +using Microsoft.AspNet.OData.Routing; +using Microsoft.OData.Edm; +using Microsoft.Test.E2E.AspNet.OData.Common.Controllers; +using Xunit; + +namespace Microsoft.Test.E2E.AspNet.OData.BulkOperation +{ + public class EmployeesController : TestODataController + { + public EmployeesController() + { + if (null == Employees) + { + InitEmployees(); + } + } + + /// + /// static so that the data is shared among requests. + /// + public static IList Employees = null; + + public static IList EmployeesTypeless = null; + + private List Friends = null; + + private void InitEmployees() + { + Friends = new List { new Friend { Id = 1, Name = "Test0", Age = 33 }, new Friend { Id = 2, Name = "Test1", Orders = new List() { new Order { Id = 1, Price = 2 } } }, new Friend { Id = 3, Name = "Test3" }, new Friend { Id = 4, Name = "Test4" } }; + + Employees = new List + { + new Employee() + { + ID=1, + Name="Name1", + SkillSet=new List{Skill.CSharp,Skill.Sql}, + Gender=Gender.Female, + AccessLevel=AccessLevel.Execute, + FavoriteSports = new FavoriteSports{Sport ="Football"}, + NewFriends = new List(){new NewFriend {Id =1, Name ="NewFriendTest1", Age=33, NewOrders= new List() { new NewOrder {Id=1, Price =101 } } } }, + Friends = this.Friends.Where(x=>x.Id ==1 || x.Id==2).ToList() + }, + new Employee() + { + ID=2,Name="Name2", + SkillSet=new List(), + Gender=Gender.Female, + AccessLevel=AccessLevel.Read, + NewFriends = new List(){ new MyNewFriend { Id = 2, MyNewOrders = new List() { new MyNewOrder { Id = 2, Price = 444 , Quantity=2 } } } }, + Friends = this.Friends.Where(x=>x.Id ==3 || x.Id==4).ToList() + }, + new Employee(){ + ID=3,Name="Name3", + SkillSet=new List{Skill.Web,Skill.Sql}, + Gender=Gender.Female, + AccessLevel=AccessLevel.Read|AccessLevel.Write + }, + }; + } + + private void InitTypeLessEmployees(IEdmEntityType entityType) + { + EmployeesTypeless = new List(); + var emp1 = new EdmEntityObject(entityType); + emp1.TrySetPropertyValue("ID", 1); + emp1.TrySetPropertyValue("Name", "Test1"); + + var friendType = entityType.DeclaredNavigationProperties().First().Type.Definition.AsElementType() as IEdmEntityType; + + var friends = new List(); + var friend1 = new EdmEntityObject(friendType); + friend1.TrySetPropertyValue("Id", 1); + friend1.TrySetPropertyValue("Age", 33); + friend1.TrySetPropertyValue("Name", "Test1"); + + var friend2 = new EdmEntityObject(friendType); + friend2.TrySetPropertyValue("Id", 2); + friend2.TrySetPropertyValue("Name", "Test2"); + + friends.Add(friend1); + friends.Add(friend2); + + emp1.TrySetPropertyValue("UnTypedFriends", friends); + + var emp2 = new EdmEntityObject(entityType); + emp2.TrySetPropertyValue("ID", 2); + emp2.TrySetPropertyValue("Name", "Test2"); + + var friends2 = new List(); + var friend3 = new EdmEntityObject(friendType); + friend3.TrySetPropertyValue("Id", 3); + friend3.TrySetPropertyValue("Name", "Test3"); + + var friend4 = new EdmEntityObject(friendType); + friend4.TrySetPropertyValue("Id", 4); + friend4.TrySetPropertyValue("Name", "Test4"); + + friends2.Add(friend3); + friends2.Add(friend4); + + emp2.TrySetPropertyValue("UnTypedFriends", friends2); + + var emp3 = new EdmEntityObject(entityType); + emp3.TrySetPropertyValue("ID", 3); + emp3.TrySetPropertyValue("Name", "Test3"); + + var friends35 = new List(); + var friend5 = new EdmEntityObject(friendType); + friend5.TrySetPropertyValue("Id", 5); + friend5.TrySetPropertyValue("Name", "Test5"); + + friends35.Add(friend5); + + emp3.TrySetPropertyValue("UnTypedFriends", friends35); + + EmployeesTypeless.Add(emp1); + EmployeesTypeless.Add(emp2); + EmployeesTypeless.Add(emp3); + } + + public DeltaSet PatchWithUsersMethod(DeltaSet friendColl, Employee employee) + { + var changedObjColl = friendColl.Patch(new NewFriendAPIHandler(employee), new APIHandlerFactory(Request.GetModel())); + + return changedObjColl; + } + public EdmChangedObjectCollection PatchWithUsersMethodTypeLess(int key, EdmChangedObjectCollection friendColl) + { + var entity = Request.GetModel().FindDeclaredType("Microsoft.Test.E2E.AspNet.OData.BulkOperation.UnTypedEmployee") as IEdmEntityType; + InitTypeLessEmployees(entity); + + var entity1 = Request.GetModel().FindDeclaredType("Microsoft.Test.E2E.AspNet.OData.BulkOperation.UnTypedFriend") as IEdmEntityType; + + var changedObjColl = friendColl.Patch(new FriendTypelessAPIHandler(EmployeesTypeless[key - 1], entity), new TypelessAPIHandlerFactory(Request.GetModel(), entity)); + + return changedObjColl; + } + + public EdmChangedObjectCollection EmployeePatchMethodTypeLess(EdmChangedObjectCollection empColl) + { + var entity = Request.GetModel().FindDeclaredType("Microsoft.Test.E2E.AspNet.OData.BulkOperation.UnTypedEmployee") as IEdmEntityType; + InitTypeLessEmployees(entity); + + var changedObjColl = empColl.Patch(new EmployeeEdmAPIHandler(entity), new TypelessAPIHandlerFactory(Request.GetModel(), entity)); + ValidateSuccessfulTypeless(); + + return changedObjColl; + } + + private void ValidateSuccessfulTypeless() + { + object obj; + Assert.True(EmployeesTypeless.First().TryGetPropertyValue("UnTypedFriends", out obj)); + + var friends = obj as ICollection; + Assert.NotNull(friends); + + object obj1; + + friends.First().TryGetPropertyValue("Name", out obj1); + + object name; + + if (EmployeesTypeless.First().TryGetPropertyValue("Name", out name) && name.ToString() == "Employeeabcd") + { + Assert.Equal("abcd", obj1.ToString()); + + object age; + friends.First().TryGetPropertyValue("Age", out age); + + Assert.Equal(33, (int)age); + } + else + { + Assert.Equal("Friend1", obj1.ToString()); + } + } + + [EnableQuery(PageSize = 10, MaxExpansionDepth = 5)] + public ITestActionResult Get() + { + return Ok(Employees.AsQueryable()); + } + + [EnableQuery] + public ITestActionResult Get(int key) + { + var emp = Employees.SingleOrDefault(e => e.ID == key); + + return Ok(emp); + } + + [ODataRoute("Employees({key})/Friends")] + public ITestActionResult GetFriends(int key) + { + var emp = Employees.SingleOrDefault(e => e.ID == key); + + return Ok(emp.Friends); + } + + [ODataRoute("Employees({key})/UnTypedFriends")] + public ITestActionResult GetUnTypedFriends(int key) + { + var entity = Request.GetModel().FindDeclaredType("Microsoft.Test.E2E.AspNet.OData.BulkOperation.UnTypedEmployee") as IEdmEntityType; + InitTypeLessEmployees(entity); + + foreach (var emp in EmployeesTypeless) + { + object obj; + emp.TryGetPropertyValue("ID", out obj); + + if (Equals(key, obj)) + { + object friends; + emp.TryGetPropertyValue("UntypedFriends", out friends); + return Ok(friends); + } + } + + return Ok(); + } + + [ODataRoute("Employees")] + [HttpPatch] + [EnableQuery] + public ITestActionResult PatchEmployees([FromBody] DeltaSet coll) + { + InitEmployees(); + + Assert.NotNull(coll); + + var returncoll = coll.Patch(new EmployeeAPIHandler(), new APIHandlerFactory(Request.GetModel())); + + return Ok(returncoll); + } + + [ODataRoute("Employees")] + [HttpPost] + public ITestActionResult Post([FromBody] Employee employee) + { + InitEmployees(); + + var handler = new EmployeeAPIHandler(); + + handler.UpdateLinkedObjects(employee, Request.GetModel()); + + return Ok(employee); + } + + [ODataRoute("Employees({key})/Friends")] + [HttpPatch] + public ITestActionResult PatchFriends(int key, [FromBody] DeltaSet friendColl) + { + InitEmployees(); + + Employee originalEmployee = Employees.SingleOrDefault(c => c.ID == key); + Assert.NotNull(originalEmployee); + + var changedObjColl = friendColl.Patch(originalEmployee.Friends); + + return Ok(changedObjColl); + } + + [ODataRoute("Employees({key})/NewFriends")] + [HttpPatch] + public ITestActionResult PatchNewFriends(int key, [FromBody] DeltaSet friendColl) + { + InitEmployees(); + + if (key == 1) + { + var deltaSet = PatchWithUsersMethod(friendColl, Employees.First(x => x.ID == key)); + + return Ok(deltaSet); + } + { + Employee originalEmployee = Employees.SingleOrDefault(c => c.ID == key); + Assert.NotNull(originalEmployee); + + var friendCollection = new FriendColl() { new NewFriend { Id = 2, Age = 15 } }; + + var changedObjColl = friendColl.Patch(friendCollection); + + return Ok(changedObjColl); + } + } + + [ODataRoute("Employees({key})/UnTypedFriends")] + [HttpPatch] + public ITestActionResult PatchUnTypedFriends(int key, [FromBody] EdmChangedObjectCollection friendColl) + { + if (key == 1) + { + var changedObjColl = PatchWithUsersMethodTypeLess(key, friendColl); + + var emp = EmployeesTypeless[key - 1]; + object obj; + emp.TryGetPropertyValue("UnTypedFriends", out obj); + var lst = obj as List; + + if (lst != null && lst.Count > 1) + { + object obj1; + if (lst[1].TryGetPropertyValue("Name", out obj1) && Equals("Friend007", obj1)) + { + lst[1].TryGetPropertyValue("Address", out obj1); + Assert.NotNull(obj1); + object obj2; + (obj1 as EdmStructuredObject).TryGetPropertyValue("Street", out obj2); + + Assert.Equal("Abc 123", obj2); + } + } + + return Ok(changedObjColl); + } + else if (key == 2) + { + var entitytype = Request.GetModel().FindDeclaredType("Microsoft.Test.E2E.AspNet.OData.BulkOperation.UnTypedEmployee") as IEdmEntityType; + var entity = new EdmEntityObject(friendColl[0].GetEdmType().AsEntity()); + entity.TrySetPropertyValue("Id", 2); + + var friendCollection = new FriendColl() { entity }; + + var changedObjColl = PatchWithUsersMethodTypeLess(key, friendColl); + + object obj; + Assert.Single(changedObjColl); + + changedObjColl.First().TryGetPropertyValue("Age", out obj); + Assert.Equal(35, obj); + + return Ok(changedObjColl); + } + else + { + var changedObjColl = PatchWithUsersMethodTypeLess(key, friendColl); + + return Ok(changedObjColl); + } + } + + [ODataRoute("UnTypedEmployees")] + [HttpPatch] + public ITestActionResult PatchUnTypedEmployees([FromBody] EdmChangedObjectCollection empColl) + { + var changedObjColl = EmployeePatchMethodTypeLess(empColl); + + return Ok(changedObjColl); + } + + [ODataRoute("Employees({key})")] + [EnableQuery] + public ITestActionResult Patch(int key, [FromBody] Delta delta) + { + InitEmployees(); + + delta.TrySetPropertyValue("ID", key); // It is the key property, and should not be updated. + + Employee employee = Employees.FirstOrDefault(e => e.ID == key); + + if (employee == null) + { + employee = new Employee(); + delta.Patch(employee, new EmployeeAPIHandler(), new APIHandlerFactory(Request.GetModel())); + + return Created(employee); + } + + try + { + // todo: put APIHandlerFactory in request context + delta.Patch(employee, new EmployeeAPIHandler(), new APIHandlerFactory(Request.GetModel())); + + if (employee.Name == "Bind1") + { + Assert.NotNull(employee.Friends.Single(x => x.Id == 3)); + } + } + catch (ArgumentException ae) + { + return BadRequest(ae.Message); + } + + return Ok(employee); + } + } + + public class CompanyController : TestODataController + { + public static IList Companies = null; + public static IList OverdueOrders = null; + public static IList MyOverdueOrders = null; + + public CompanyController() + { + if (null == Companies) + { + InitCompanies(); + } + } + + private void InitCompanies() + { + OverdueOrders = new List() { new NewOrder { Id = 1, Price = 10, Quantity = 1 }, new NewOrder { Id = 2, Price = 20, Quantity = 2 }, new NewOrder { Id = 3, Price = 30 }, new NewOrder { Id = 4, Price = 40 } }; + MyOverdueOrders = new List() { new MyNewOrder { Id = 1, Price = 10, Quantity = 1 }, new MyNewOrder { Id = 2, Price = 20, Quantity = 2 }, new MyNewOrder { Id = 3, Price = 30 }, new MyNewOrder { Id = 4, Price = 40 } }; + + Companies = new List() { new Company { Id = 1, Name = "Company1", OverdueOrders = OverdueOrders.Where(x => x.Id == 2).ToList(), MyOverdueOrders = MyOverdueOrders.Where(x => x.Id == 2).ToList() } , + new Company { Id = 2, Name = "Company2", OverdueOrders = OverdueOrders.Where(x => x.Id == 3 || x.Id == 4).ToList() } }; + } + + [ODataRoute("Companies")] + [HttpPatch] + public ITestActionResult PatchCompanies([FromBody] DeltaSet coll) + { + InitCompanies(); + InitEmployees(); + + Assert.NotNull(coll); + + var returncoll = coll.Patch(new CompanyAPIHandler(), new APIHandlerFactory(Request.GetModel())); + + var comp = coll.First() as Delta; + //object val; + + //if (comp.TryGetPropertyValue("Name", out val)) + //{ + // if (val.ToString() == "Company02") + // { + // ValidateOverdueOrders2(1, 2, 9); + // } + // else + // { + // ValidateOverdueOrders1(1, 1, 9); + // } + //} + + return Ok(returncoll); + } + + [ODataRoute("Companies")] + [HttpPost] + public ITestActionResult Post([FromBody] Company company) + { + + InitCompanies(); + InitEmployees(); + + if (company.Id == 4) + { + AddNewOrder(company); + } + + var handler = new CompanyAPIHandler(); + + handler.UpdateLinkedObjects(company, Request.GetModel()); + + Companies.Add(company); + + if (company.Id == 4) + { + ValidateOverdueOrders1(4, 4, 0, 30); + } + else + { + ValidateOverdueOrders1(3, 1); + } + + return Ok(company); + } + + [ODataRoute("Companies({key})/MyOverdueOrders")] + public ITestActionResult GetMyOverdueOrders(int key) + { + var emp = Companies.SingleOrDefault(e => e.Id == key); + + return Ok(emp.MyOverdueOrders); + } + + [ODataRoute("Companies({key})/OverdueOrders")] + public ITestActionResult GetOverdueOrders(int key) + { + var emp = Companies.SingleOrDefault(e => e.Id == key); + + return Ok(emp.OverdueOrders); + } + + private static void AddNewOrder(Company company) + { + var newOrder = new NewOrder { Id = 4, Price = company.OverdueOrders[1].Price, Quantity = company.OverdueOrders[1].Quantity }; + OverdueOrders.Add(newOrder); + company.OverdueOrders[1] = newOrder; + } + + private void InitEmployees() + { + var cntrl = new EmployeesController(); + } + + private void ValidateOverdueOrders1(int companyId, int orderId, int quantity = 0, int price = 101) + { + var comp = Companies.FirstOrDefault(x => x.Id == companyId); + Assert.NotNull(comp); + + NewOrder order = comp.OverdueOrders.FirstOrDefault(x => x.Id == orderId); + Assert.NotNull(order); + Assert.Equal(orderId, order.Id); + Assert.Equal(price, order.Price); + Assert.Equal(quantity, order.Quantity); + } + + private void ValidateOverdueOrders2(int companyId, int orderId, int quantity = 0) + { + var comp = Companies.FirstOrDefault(x => x.Id == companyId); + Assert.NotNull(comp); + + MyNewOrder order = comp.MyOverdueOrders.FirstOrDefault(x => x.Id == orderId); + Assert.NotNull(order); + Assert.Equal(orderId, order.Id); + Assert.Equal(444, order.Price); + Assert.Equal(quantity, order.Quantity); + } + } +} \ No newline at end of file diff --git a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkOperationDataModel.cs b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkOperationDataModel.cs new file mode 100644 index 0000000000..352ec6bba1 --- /dev/null +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkOperationDataModel.cs @@ -0,0 +1,205 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using Microsoft.AspNet.OData; +using Microsoft.AspNet.OData.Builder; + +namespace Microsoft.Test.E2E.AspNet.OData.BulkOperation +{ + public class Employee + { + [Key] + public int ID { get; set; } + public String Name { get; set; } + public List SkillSet { get; set; } + public Gender Gender { get; set; } + public AccessLevel AccessLevel { get; set; } + public List Friends { get; set; } + public List NewFriends { get; set; } + public List UnTypedFriends { get; set; } + public FavoriteSports FavoriteSports { get; set; } + public IODataInstanceAnnotationContainer InstanceAnnotations { get; set; } + } + + [Flags] + public enum AccessLevel + { + Read = 1, + Write = 2, + Execute = 4 + } + + public enum Gender + { + Male = 1, + Female = 2 + } + + public enum Skill + { + CSharp, + Sql, + Web, + } + + public enum Sport + { + Pingpong, + Basketball + } + + public class FavoriteSports + { + public string Sport { get; set; } + } + + public class Friend + { + [Key] + public int Id { get; set; } + public string Name { get; set; } + public int Age { get; set; } + public List Orders { get; set; } + } + + + public class Order + { + [Key] + public int Id { get; set; } + public int Price { get; set; } + } + + public class NewFriend + { + [Key] + public int Id { get; set; } + public string Name { get; set; } + public int Age { get; set; } + public IODataInstanceAnnotationContainer InstanceAnnotations { get; set; } + + [Contained] + public List NewOrders { get; set; } + + } + + public class MyNewFriend: NewFriend + { + public string MyName { get; set; } + + [Contained] + public List MyNewOrders { get; set; } + } + + public class MyNewOrder + { + [Key] + public int Id { get; set; } + public int Price { get; set; } + public int Quantity { get; set; } + public ODataIdContainer Container { get; set; } + } + + public class NewOrder + { + [Key] + public int Id { get; set; } + public int Price { get; set; } + public int Quantity { get; set; } + public ODataIdContainer Container {get;set;} + } + + + public class Company + { + [Key] + public int Id { get; set; } + public string Name { get; set; } + public List OverdueOrders { get; set; } + public List MyOverdueOrders { get; set; } + } + + public class UnTypedEmployee + { + [Key] + public int ID { get; set; } + public String Name { get; set; } + public List UnTypedFriends { get; set; } + } + + public class UnTypedFriend + { + [Key] + public int Id { get; set; } + public string Name { get; set; } + public int Age { get; set; } + public UnTypedAddress Address { get; set; } + public IODataInstanceAnnotationContainer InstanceAnnotations { get; set; } + } + + public class UnTypedAddress + { + [Key] + public int Id { get; set; } + public string Street { get; set; } + } + + public class FriendColl : ICollection + { + public FriendColl() { _items = new List(); } + private IList _items; + public int Count => _items.Count; + public bool IsReadOnly => _items.IsReadOnly; + + public void Add(T item) + { + var _item = item as NewFriend; + if (_item != null && _item.Age < 10) + { + throw new NotImplementedException(); + } + + _items.Add(item); + } + + public void Clear() + { + _items.Clear(); + } + + public bool Contains(T item) + { + return _items.Contains(item); + } + + public void CopyTo(T[] array, int arrayIndex) + { + _items.CopyTo(array, arrayIndex); + } + + public IEnumerator GetEnumerator() + { + return _items.GetEnumerator(); + } + + public bool Remove(T item) + { + throw new NotImplementedException(); + + //return _items.Remove(item); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)_items).GetEnumerator(); + } + } +} \ No newline at end of file diff --git a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkOperationEdmModel.cs b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkOperationEdmModel.cs new file mode 100644 index 0000000000..0ccc5aef7e --- /dev/null +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkOperationEdmModel.cs @@ -0,0 +1,113 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +using Microsoft.AspNet.OData.Builder; +using Microsoft.OData.Edm; +using Microsoft.Test.E2E.AspNet.OData.Common.Execution; + +namespace Microsoft.Test.E2E.AspNet.OData.BulkOperation +{ + internal class BulkOperationEdmModel + { + public static IEdmModel GetExplicitModel(WebRouteConfiguration configuration) + { + ODataModelBuilder builder = new ODataModelBuilder(); + var employee = builder.EntityType(); + employee.HasKey(c => c.ID); + employee.Property(c => c.Name); + employee.CollectionProperty(c => c.SkillSet); + employee.EnumProperty(c => c.Gender); + employee.EnumProperty(c => c.AccessLevel); + + employee.CollectionProperty(c => c.Friends); + employee.CollectionProperty(c => c.NewFriends); + employee.CollectionProperty(c => c.UnTypedFriends); + + + var skill = builder.EnumType(); + skill.Member(Skill.CSharp); + skill.Member(Skill.Sql); + skill.Member(Skill.Web); + + var gender = builder.EnumType(); + gender.Member(Gender.Female); + gender.Member(Gender.Male); + + var accessLevel = builder.EnumType(); + accessLevel.Member(AccessLevel.Execute); + accessLevel.Member(AccessLevel.Read); + accessLevel.Member(AccessLevel.Write); + + var sport = builder.EnumType(); + sport.Member(Sport.Basketball); + sport.Member(Sport.Pingpong); + + AddBoundActionsAndFunctions(employee); + AddUnboundActionsAndFunctions(builder); + + EntitySetConfiguration employees = builder.EntitySet("Employees"); + builder.Namespace = typeof(Employee).Namespace; + return builder.GetEdmModel(); + } + + public static IEdmModel GetConventionModel(WebRouteConfiguration configuration) + { + ODataConventionModelBuilder builder = configuration.CreateConventionModelBuilder(); + EntitySetConfiguration employees = builder.EntitySet("Employees"); + EntityTypeConfiguration employee = employees.EntityType; + + EntitySetConfiguration friends = builder.EntitySet("Friends"); + EntitySetConfiguration orders = builder.EntitySet("Orders"); + EntitySetConfiguration fnewriends = builder.EntitySet("NewFriends"); + EntitySetConfiguration funtypenewriends = builder.EntitySet("UnTypedFriends"); + EntitySetConfiguration addresses = builder.EntitySet("Address"); + + EntitySetConfiguration unemployees = builder.EntitySet("UnTypedEmployees"); + EntityTypeConfiguration unemployee = unemployees.EntityType; + + EntitySetConfiguration companies = builder.EntitySet("Companies"); + EntitySetConfiguration overdueorders = builder.EntitySet("OverdueOrders"); + EntitySetConfiguration myoverdueorders = builder.EntitySet("MyOverdueOrders"); + EntitySetConfiguration myNewOrders = builder.EntitySet("MyNewOrders"); + + // maybe following lines are not required once bug #1587 is fixed. + // 1587: It's better to support automatically adding actions and functions in ODataConventionModelBuilder. + AddBoundActionsAndFunctions(employee); + AddUnboundActionsAndFunctions(builder); + + builder.Namespace = typeof(Employee).Namespace; + builder.MaxDataServiceVersion = EdmConstants.EdmVersion401; + builder.DataServiceVersion = EdmConstants.EdmVersion401; + + var edmModel = builder.GetEdmModel(); + return edmModel; + } + + private static void AddBoundActionsAndFunctions(EntityTypeConfiguration employee) + { + var actionConfiguration = employee.Action("AddSkill"); + actionConfiguration.Parameter("skill"); + actionConfiguration.ReturnsCollection(); + + var functionConfiguration = employee.Function("GetAccessLevel"); + functionConfiguration.Returns(); + } + + private static void AddUnboundActionsAndFunctions(ODataModelBuilder odataModelBuilder) + { + var actionConfiguration = odataModelBuilder.Action("SetAccessLevel"); + actionConfiguration.Parameter("ID"); + actionConfiguration.Parameter("accessLevel"); + actionConfiguration.Returns(); + + var functionConfiguration = odataModelBuilder.Function("HasAccessLevel"); + functionConfiguration.Parameter("ID"); + functionConfiguration.Parameter("AccessLevel"); + functionConfiguration.Returns(); + } + } +} \ No newline at end of file diff --git a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkOperationPatchHandlers.cs b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkOperationPatchHandlers.cs new file mode 100644 index 0000000000..7bf808599a --- /dev/null +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkOperationPatchHandlers.cs @@ -0,0 +1,1295 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNet.OData; +using Microsoft.AspNet.OData.Common; +using Microsoft.AspNet.OData.Extensions; +using Microsoft.OData.Edm; +using Microsoft.OData.UriParser; +using Microsoft.Test.E2E.AspNet.OData.BulkOperation; + +namespace Microsoft.Test.E2E.AspNet.OData.BulkOperation +{ + internal class APIHandlerFactory : ODataAPIHandlerFactory + { + public APIHandlerFactory(IEdmModel model) : base(model) + { + } + + public override IODataAPIHandler GetHandler(ODataPath odataPath) + { + if (odataPath != null) + { + int currentPosition = 0; + + if (odataPath.Count == 1) + { + GetHandlerInternal(odataPath.FirstSegment.Identifier, currentPosition); + } + + List pathSegments = odataPath.GetSegments(); + + ODataPathSegment currentPathSegment = pathSegments[currentPosition]; + + if (currentPathSegment is EntitySetSegment || currentPathSegment is NavigationPropertySegment || currentPathSegment is SingletonSegment) + { + int keySegmentPosition = ODataPathHelper.GetNextKeySegmentPosition(pathSegments, currentPosition); + KeySegment keySegment = (KeySegment)pathSegments[keySegmentPosition]; + + currentPosition = keySegmentPosition; + + return GetHandlerInternal( + currentPathSegment.Identifier, + currentPosition, + ODataPathHelper.KeySegmentAsDictionary(keySegment), + pathSegments); + } + } + + return null; + } + + private IODataAPIHandler GetHandlerInternal( + string pathName, + int currentPosition, + Dictionary keys = null, + List pathSegments = null) + { + switch (pathName) + { + case "Employees": + Employee employee; + string msg; + if ((new EmployeeAPIHandler().TryGet(keys, out employee, out msg)) == ODataAPIResponseStatus.Success) + { + return GetNestedHandlerForEmployee(pathSegments, currentPosition, employee); + } + return null; + case "Companies": + return new CompanyAPIHandler(); + + default: + return null; + } + } + + private static IODataAPIHandler GetNestedHandlerForEmployee(List pathSegments, int currentPosition, Employee employee) + { + ++currentPosition; + + if (pathSegments.Count <= currentPosition) + { + return null; + } + + ODataPathSegment currentPathSegment = pathSegments[currentPosition]; + + if (currentPathSegment is NavigationPropertySegment) + { + int keySegmentPosition = ODataPathHelper.GetNextKeySegmentPosition(pathSegments, currentPosition); + KeySegment keySegment = (KeySegment)pathSegments[keySegmentPosition]; + Dictionary keys = ODataPathHelper.KeySegmentAsDictionary(keySegment); + + currentPosition = keySegmentPosition; + + switch (currentPathSegment.Identifier) + { + case "NewFriends": + ODataPathSegment nextPathSegment = pathSegments[++currentPosition]; + + if (nextPathSegment is TypeSegment) + { + currentPosition++; + TypeSegment typeSegment = nextPathSegment as TypeSegment; + + if (typeSegment.Identifier == "Microsoft.Test.E2E.AspNet.OData.BulkOperation.MyNewFriend") + { + MyNewFriend friend = employee.NewFriends.FirstOrDefault(x => x.Id == (int)keys["Id"]) as MyNewFriend; + + if (friend != null) + { + switch (pathSegments[++currentPosition].Identifier) + { + case "MyNewOrders": + return new MyNewOrderAPIHandler(friend); + + default: + return null; + + } + } + } + } + else + { + NewFriend friend = employee.NewFriends.FirstOrDefault(x => x.Id == (int)keys["Id"]); + + if (friend != null) + { + switch (pathSegments[++currentPosition].Identifier) + { + case "NewOrders": + return new NewOrderAPIHandler(friend); + + default: + return null; + } + } + } + return null; + + case "Friends": + return new FriendAPIHandler(employee); + + default: + return null; + } + } + return null; + } + } + + internal class TypelessAPIHandlerFactory : EdmODataAPIHandlerFactory + { + IEdmEntityType entityType; + + public TypelessAPIHandlerFactory(IEdmModel model, IEdmEntityType entityType) : base(model) + { + this.entityType = entityType; + } + + public override EdmODataAPIHandler GetHandler(ODataPath odataPath) + { + if (odataPath != null) + { + string pathName = odataPath.GetLastNonTypeNonKeySegment().Identifier; + + switch (pathName) + { + case "UnTypedEmployees": + return new EmployeeEdmAPIHandler(entityType); + + default: + return null; + } + } + + return null; + } + } + + internal class CompanyAPIHandler : ODataAPIHandler + { + public override ODataAPIResponseStatus TryCreate(IDictionary keyValues, out Company createdObject, out string errorMessage) + { + createdObject = null; + errorMessage = string.Empty; + + try + { + createdObject = new Company(); + CompanyController.Companies.Add(createdObject); + + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + + public override ODataAPIResponseStatus TryDelete(IDictionary keyValues, out string errorMessage) + { + errorMessage = string.Empty; + + try + { + var id = keyValues.First().Value.ToString(); + var company = CompanyController.Companies.First(x => x.Id == Int32.Parse(id)); + + CompanyController.Companies.Remove(company); + + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + + public override ODataAPIResponseStatus TryGet(IDictionary keyValues, out Company originalObject, out string errorMessage) + { + ODataAPIResponseStatus status = ODataAPIResponseStatus.Success; + errorMessage = string.Empty; + originalObject = null; + + try + { + var id = keyValues["Id"].ToString(); + originalObject = CompanyController.Companies.First(x => x.Id == Int32.Parse(id)); + + if (originalObject == null) + { + status = ODataAPIResponseStatus.NotFound; + } + } + catch (Exception ex) + { + status = ODataAPIResponseStatus.Failure; + errorMessage = ex.Message; + } + + return status; + } + + public override IODataAPIHandler GetNestedHandler(Company parent, string navigationPropertyName) + { + switch (navigationPropertyName) + { + case "OverdueOrders": + return new OverdueOrderAPIHandler(parent); + case "MyOverdueOrders": + return new MyOverdueOrderAPIHandler(parent); + default: + return null; + } + } + + public override ODataAPIResponseStatus TryAddRelatedObject(Company resource, out string errorMessage) + { + throw new NotImplementedException(); + } + } + + internal class OverdueOrderAPIHandler : ODataAPIHandler + { + Company parent; + + public OverdueOrderAPIHandler(Company parent) + { + this.parent = parent; + } + + public override ODataAPIResponseStatus TryCreate(IDictionary keyValues, out NewOrder createdObject, out string errorMessage) + { + createdObject = null; + errorMessage = string.Empty; + + try + { + createdObject = new NewOrder(); + parent.OverdueOrders.Add(createdObject); + + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + + public override ODataAPIResponseStatus TryDelete(IDictionary keyValues, out string errorMessage) + { + errorMessage = string.Empty; + + try + { + var id = keyValues.First().Value.ToString(); + var newOrders = CompanyController.OverdueOrders.First(x => x.Id == Int32.Parse(id)); + + parent.OverdueOrders.Remove(newOrders); + + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + + public override ODataAPIResponseStatus TryGet(IDictionary keyValues, out NewOrder originalObject, out string errorMessage) + { + ODataAPIResponseStatus status = ODataAPIResponseStatus.Success; + errorMessage = string.Empty; + originalObject = null; + + try + { + var id = keyValues["Id"].ToString(); + originalObject = parent.OverdueOrders.FirstOrDefault(x => x.Id == Int32.Parse(id)); + + if (originalObject == null) + { + status = ODataAPIResponseStatus.NotFound; + } + } + catch (Exception ex) + { + status = ODataAPIResponseStatus.Failure; + errorMessage = ex.Message; + } + + return status; + } + + public override IODataAPIHandler GetNestedHandler(NewOrder parent, string navigationPropertyName) + { + switch (navigationPropertyName) + { + default: + return null; + } + } + + public override ODataAPIResponseStatus TryAddRelatedObject(NewOrder resource, out string errorMessage) + { + errorMessage = string.Empty; + + try + { + parent.OverdueOrders.Add(resource); + + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + } + + internal class MyOverdueOrderAPIHandler : ODataAPIHandler + { + Company parent; + + public MyOverdueOrderAPIHandler(Company parent) + { + this.parent = parent; + } + + public override ODataAPIResponseStatus TryCreate(IDictionary keyValues, out MyNewOrder createdObject, out string errorMessage) + { + createdObject = null; + errorMessage = string.Empty; + + try + { + createdObject = new MyNewOrder(); + parent.MyOverdueOrders.Add(createdObject); + + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + + public override ODataAPIResponseStatus TryDelete(IDictionary keyValues, out string errorMessage) + { + errorMessage = string.Empty; + + try + { + var id = keyValues.First().Value.ToString(); + var newOrders = CompanyController.MyOverdueOrders.First(x => x.Id == Int32.Parse(id)); + + parent.MyOverdueOrders.Remove(newOrders); + + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + + public override ODataAPIResponseStatus TryGet(IDictionary keyValues, out MyNewOrder originalObject, out string errorMessage) + { + ODataAPIResponseStatus status = ODataAPIResponseStatus.Success; + errorMessage = string.Empty; + originalObject = null; + + try + { + var id = keyValues["Id"].ToString(); + originalObject = parent.MyOverdueOrders.FirstOrDefault(x => x.Id == Int32.Parse(id)); + + + if (originalObject == null) + { + status = ODataAPIResponseStatus.NotFound; + } + } + catch (Exception ex) + { + status = ODataAPIResponseStatus.Failure; + errorMessage = ex.Message; + } + + return status; + } + + public override IODataAPIHandler GetNestedHandler(MyNewOrder parent, string navigationPropertyName) + { + switch (navigationPropertyName) + { + + default: + return null; + } + } + + public override ODataAPIResponseStatus TryAddRelatedObject(MyNewOrder resource, out string errorMessage) + { + errorMessage = string.Empty; + + try + { + parent.MyOverdueOrders.Add(resource); + + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + } + + internal class EmployeeAPIHandler : ODataAPIHandler + { + public override ODataAPIResponseStatus TryCreate(IDictionary keyValues, out Employee createdObject, out string errorMessage) + { + createdObject = null; + errorMessage = null; + + try + { + createdObject = new Employee(); + EmployeesController.Employees.Add(createdObject); + + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + + public override ODataAPIResponseStatus TryDelete(IDictionary keyValues, out string errorMessage) + { + errorMessage = null; + + try + { + var id = keyValues.First().Value.ToString(); + var employee = EmployeesController.Employees.First(x => x.ID == Int32.Parse(id)); + + EmployeesController.Employees.Remove(employee); + + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + + public override ODataAPIResponseStatus TryGet(IDictionary keyValues, out Employee originalObject, out string errorMessage) + { + ODataAPIResponseStatus status = ODataAPIResponseStatus.Success; + errorMessage = null; + originalObject = null; + + try + { + var id = keyValues["ID"].ToString(); + originalObject = EmployeesController.Employees.First(x => x.ID == Int32.Parse(id)); + + if (originalObject == null) + { + status = ODataAPIResponseStatus.NotFound; + } + } + catch (Exception ex) + { + status = ODataAPIResponseStatus.Failure; + errorMessage = ex.Message; + } + + return status; + } + + public override IODataAPIHandler GetNestedHandler(Employee parent, string navigationPropertyName) + { + switch (navigationPropertyName) + { + case "Friends": + return new FriendAPIHandler(parent); + case "NewFriends": + return new NewFriendAPIHandler(parent); + default: + return null; + } + } + + public override ODataAPIResponseStatus TryAddRelatedObject(Employee resource, out string errorMessage) + { + throw new NotImplementedException(); + } + } + + internal class FriendAPIHandler : ODataAPIHandler + { + Employee employee; + public FriendAPIHandler(Employee employee) + { + this.employee = employee; + } + + public override ODataAPIResponseStatus TryCreate(IDictionary keyValues, out Friend createdObject, out string errorMessage) + { + createdObject = null; + errorMessage = string.Empty; + + try + { + createdObject = new Friend(); + employee.Friends.Add(createdObject); + + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + + public override ODataAPIResponseStatus TryDelete(IDictionary keyValues, out string errorMessage) + { + errorMessage = string.Empty; + + try + { + var id = keyValues.First().Value.ToString(); + var friend = employee.Friends.FirstOrDefault(x => x.Id == Int32.Parse(id)); + + employee.Friends.Remove(friend); + + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + + public override ODataAPIResponseStatus TryGet(IDictionary keyValues, out Friend originalObject, out string errorMessage) + { + ODataAPIResponseStatus status = ODataAPIResponseStatus.Success; + errorMessage = string.Empty; + originalObject = null; + + try + { + var id = keyValues["Id"].ToString(); + originalObject = employee.Friends.FirstOrDefault(x => x.Id == Int32.Parse(id)); + + + if (originalObject == null) + { + status = ODataAPIResponseStatus.NotFound; + } + } + catch (Exception ex) + { + status = ODataAPIResponseStatus.Failure; + errorMessage = ex.Message; + } + + return status; + } + + public override IODataAPIHandler GetNestedHandler(Friend parent, string navigationPropertyName) + { + switch (navigationPropertyName) + { + case "Orders": + return new OrderAPIHandler(parent); + default: + return null; + } + } + + public override ODataAPIResponseStatus TryAddRelatedObject(Friend resource, out string errorMessage) + { + throw new NotImplementedException(); + } + } + + internal class NewOrderAPIHandler : ODataAPIHandler + { + NewFriend friend; + public NewOrderAPIHandler(NewFriend friend) + { + this.friend = friend; + } + + public override ODataAPIResponseStatus TryCreate(IDictionary keyValues, out NewOrder createdObject, out string errorMessage) + { + createdObject = null; + errorMessage = string.Empty; + + try + { + createdObject = new NewOrder(); + + if (friend.NewOrders == null) + { + friend.NewOrders = new List(); + } + + friend.NewOrders.Add(createdObject); + + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + + public override ODataAPIResponseStatus TryDelete(IDictionary keyValues, out string errorMessage) + { + errorMessage = string.Empty; + + try + { + var id = keyValues.First().Value.ToString(); + var friend = this.friend.NewOrders.FirstOrDefault(x => x.Id == int.Parse(id)); + + this.friend.NewOrders.Remove(friend); + + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + + public override ODataAPIResponseStatus TryGet(IDictionary keyValues, out NewOrder originalObject, out string errorMessage) + { + ODataAPIResponseStatus status = ODataAPIResponseStatus.Success; + errorMessage = string.Empty; + originalObject = null; + + try + { + if (friend.NewOrders != null) + { + var id = keyValues["Id"].ToString(); + originalObject = friend.NewOrders.FirstOrDefault(x => x.Id == Int32.Parse(id)); + } + + if (originalObject == null) + { + status = ODataAPIResponseStatus.NotFound; + } + } + catch (Exception ex) + { + status = ODataAPIResponseStatus.Failure; + errorMessage = ex.Message; + } + + return status; + } + + public override IODataAPIHandler GetNestedHandler(NewOrder parent, string navigationPropertyName) + { + throw new NotImplementedException(); + } + + public override ODataAPIResponseStatus TryAddRelatedObject(NewOrder resource, out string errorMessage) + { + throw new NotImplementedException(); + } + } + + internal class MyNewOrderAPIHandler : ODataAPIHandler + { + MyNewFriend friend; + public MyNewOrderAPIHandler(MyNewFriend friend) + { + this.friend = friend; + } + + public override ODataAPIResponseStatus TryCreate(IDictionary keyValues, out MyNewOrder createdObject, out string errorMessage) + { + createdObject = null; + errorMessage = string.Empty; + + try + { + createdObject = new MyNewOrder(); + + if (friend.MyNewOrders == null) + { + friend.MyNewOrders = new List(); + } + + friend.MyNewOrders.Add(createdObject); + + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + + public override ODataAPIResponseStatus TryDelete(IDictionary keyValues, out string errorMessage) + { + errorMessage = string.Empty; + + try + { + var id = keyValues.First().Value.ToString(); + var friend = this.friend.MyNewOrders.FirstOrDefault(x => x.Id == int.Parse(id)); + + this.friend.MyNewOrders.Remove(friend); + + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + + public override ODataAPIResponseStatus TryGet(IDictionary keyValues, out MyNewOrder originalObject, out string errorMessage) + { + ODataAPIResponseStatus status = ODataAPIResponseStatus.Success; + errorMessage = string.Empty; + originalObject = null; + + try + { + if (friend.MyNewOrders != null) + { + var id = keyValues["Id"].ToString(); + originalObject = friend.MyNewOrders.FirstOrDefault(x => x.Id == Int32.Parse(id)); + } + + if (originalObject == null) + { + status = ODataAPIResponseStatus.NotFound; + } + } + catch (Exception ex) + { + status = ODataAPIResponseStatus.Failure; + errorMessage = ex.Message; + } + + return status; + } + + public override IODataAPIHandler GetNestedHandler(MyNewOrder parent, string navigationPropertyName) + { + throw new NotImplementedException(); + } + + public override ODataAPIResponseStatus TryAddRelatedObject(MyNewOrder resource, out string errorMessage) + { + throw new NotImplementedException(); + } + } + + internal class OrderAPIHandler : ODataAPIHandler + { + Friend friend; + public OrderAPIHandler(Friend friend) + { + this.friend = friend; + } + + public override ODataAPIResponseStatus TryCreate(IDictionary keyValues, out Order createdObject, out string errorMessage) + { + createdObject = null; + errorMessage = string.Empty; + + try + { + createdObject = new Order(); + + if (friend.Orders == null) + { + friend.Orders = new List(); + } + + friend.Orders.Add(createdObject); + + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + + public override ODataAPIResponseStatus TryDelete(IDictionary keyValues, out string errorMessage) + { + errorMessage = string.Empty; + + try + { + var id = keyValues.First().Value.ToString(); + var friend = this.friend.Orders.FirstOrDefault(x => x.Id == int.Parse(id)); + + this.friend.Orders.Remove(friend); + + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + + public override ODataAPIResponseStatus TryGet(IDictionary keyValues, out Order originalObject, out string errorMessage) + { + ODataAPIResponseStatus status = ODataAPIResponseStatus.Success; + errorMessage = string.Empty; + originalObject = null; + + try + { + if (friend.Orders != null) + { + var id = keyValues["Id"].ToString(); + originalObject = friend.Orders.FirstOrDefault(x => x.Id == Int32.Parse(id)); + } + + if (originalObject == null) + { + status = ODataAPIResponseStatus.NotFound; + } + } + catch (Exception ex) + { + status = ODataAPIResponseStatus.Failure; + errorMessage = ex.Message; + } + + return status; + } + + public override IODataAPIHandler GetNestedHandler(Order parent, string navigationPropertyName) + { + throw new NotImplementedException(); + } + + public override ODataAPIResponseStatus TryAddRelatedObject(Order resource, out string errorMessage) + { + throw new NotImplementedException(); + } + } + + internal class NewFriendAPIHandler : ODataAPIHandler + { + Employee employee; + public NewFriendAPIHandler(Employee employee) + { + this.employee = employee; + } + + public override ODataAPIResponseStatus TryCreate(IDictionary keyValues, out NewFriend createdObject, out string errorMessage) + { + createdObject = null; + errorMessage = string.Empty; + + try + { + createdObject = new NewFriend(); + + if (employee.NewFriends == null) + { + employee.NewFriends = new List(); + } + + employee.NewFriends.Add(createdObject); + + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + + public override ODataAPIResponseStatus TryDelete(IDictionary keyValues, out string errorMessage) + { + errorMessage = string.Empty; + + try + { + var id = keyValues.First().Value.ToString(); + var friend = employee.NewFriends.First(x => x.Id == Int32.Parse(id)); + + employee.NewFriends.Remove(friend); + + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + + public override ODataAPIResponseStatus TryGet(IDictionary keyValues, out NewFriend originalObject, out string errorMessage) + { + ODataAPIResponseStatus status = ODataAPIResponseStatus.Success; + errorMessage = string.Empty; + originalObject = null; + + try + { + var id = keyValues["Id"].ToString(); + + if (employee.NewFriends == null) + { + return ODataAPIResponseStatus.NotFound; + } + + originalObject = employee.NewFriends.FirstOrDefault(x => x.Id == Int32.Parse(id)); + + if (originalObject == null) + { + status = ODataAPIResponseStatus.NotFound; + } + } + catch (Exception ex) + { + status = ODataAPIResponseStatus.Failure; + errorMessage = ex.Message; + } + + return status; + } + + public override IODataAPIHandler GetNestedHandler(NewFriend parent, string navigationPropertyName) + { + throw new NotImplementedException(); + } + + public override ODataAPIResponseStatus TryAddRelatedObject(NewFriend resource, out string errorMessage) + { + throw new NotImplementedException(); + } + } + + internal class EmployeeEdmAPIHandler : EdmODataAPIHandler + { + IEdmEntityType entityType; + public EmployeeEdmAPIHandler(IEdmEntityType entityType) + { + this.entityType = entityType; + } + + public override ODataAPIResponseStatus TryCreate(IDictionary keyValues, out IEdmStructuredObject createdObject, out string errorMessage) + { + createdObject = null; + errorMessage = string.Empty; + + try + { + createdObject = new EdmEntityObject(entityType); + EmployeesController.EmployeesTypeless.Add(createdObject as EdmStructuredObject); + + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + + public override ODataAPIResponseStatus TryDelete(IDictionary keyValues, out string errorMessage) + { + errorMessage = string.Empty; + + try + { + var id = keyValues.First().Value.ToString(); + foreach (var emp in EmployeesController.EmployeesTypeless) + { + object id1; + emp.TryGetPropertyValue("ID", out id1); + + if (id == id1.ToString()) + { + EmployeesController.EmployeesTypeless.Remove(emp); + break; + } + } + + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + + public override ODataAPIResponseStatus TryGet(IDictionary keyValues, out IEdmStructuredObject originalObject, out string errorMessage) + { + ODataAPIResponseStatus status = ODataAPIResponseStatus.Success; + errorMessage = string.Empty; + originalObject = null; + + try + { + var id = keyValues["ID"].ToString(); + foreach (var emp in EmployeesController.EmployeesTypeless) + { + object id1; + emp.TryGetPropertyValue("ID", out id1); + + if (id == id1.ToString()) + { + originalObject = emp; + break; + } + } + + if (originalObject == null) + { + status = ODataAPIResponseStatus.NotFound; + } + } + catch (Exception ex) + { + status = ODataAPIResponseStatus.Failure; + errorMessage = ex.Message; + } + + return status; + } + + public override EdmODataAPIHandler GetNestedHandler(IEdmStructuredObject parent, string navigationPropertyName) + { + switch (navigationPropertyName) + { + case "UnTypedFriends": + return new FriendTypelessAPIHandler(parent, entityType.DeclaredNavigationProperties().First().Type.Definition.AsElementType() as IEdmEntityType); + + default: + return null; + } + } + + public override ODataAPIResponseStatus TryAddRelatedObject(IEdmStructuredObject resource, out string errorMessage) + { + throw new NotImplementedException(); + } + } + + internal class FriendTypelessAPIHandler : EdmODataAPIHandler + { + IEdmEntityType entityType; + EdmStructuredObject employee; + + public FriendTypelessAPIHandler(IEdmStructuredObject employee, IEdmEntityType entityType) + { + this.employee = employee as EdmStructuredObject; + this.entityType = entityType; + } + + public override ODataAPIResponseStatus TryCreate(IDictionary keyValues, out IEdmStructuredObject createdObject, out string errorMessage) + { + createdObject = null; + errorMessage = string.Empty; + + try + { + object empid; + if (employee.TryGetPropertyValue("ID", out empid) && empid as int? == 3) + { + throw new Exception("Testing Error"); + } + + createdObject = new EdmEntityObject(entityType); + object obj; + employee.TryGetPropertyValue("UnTypedFriends", out obj); + + var friends = obj as ICollection; + + if (friends == null) + { + friends = new List(); + } + + friends.Add(createdObject); + + employee.TrySetPropertyValue("UnTypedFriends", friends); + + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + + public override ODataAPIResponseStatus TryDelete(IDictionary keyValues, out string errorMessage) + { + errorMessage = string.Empty; + + try + { + var id = keyValues.First().Value.ToString(); + if (id == "5") + { + throw new Exception("Testing Error"); + } + foreach (var emp in EmployeesController.EmployeesTypeless) + { + object id1; + emp.TryGetPropertyValue("ID", out id1); + + if (id == id1.ToString()) + { + object obj; + employee.TryGetPropertyValue("UnTypedFriends", out obj); + + var friends = obj as IList; + + friends.Remove(emp); + + employee.TrySetPropertyValue("UnTypedFriends", friends); + break; + } + } + + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + + public override ODataAPIResponseStatus TryGet(IDictionary keyValues, out IEdmStructuredObject originalObject, out string errorMessage) + { + ODataAPIResponseStatus status = ODataAPIResponseStatus.Success; + errorMessage = string.Empty; + originalObject = null; + + try + { + var id = keyValues["Id"].ToString(); + object obj; + employee.TryGetPropertyValue("UnTypedFriends", out obj); + + var friends = obj as IList; + + if (friends == null) + { + return ODataAPIResponseStatus.NotFound; + } + + foreach (var friend in friends) + { + object id1; + friend.TryGetPropertyValue("Id", out id1); + + if (id == id1.ToString()) + { + originalObject = friend; + break; + } + } + + if (originalObject == null) + { + status = ODataAPIResponseStatus.NotFound; + } + } + catch (Exception ex) + { + status = ODataAPIResponseStatus.Failure; + errorMessage = ex.Message; + } + + return status; + } + + public override EdmODataAPIHandler GetNestedHandler(IEdmStructuredObject parent, string navigationPropertyName) + { + return null; + } + + public override ODataAPIResponseStatus TryAddRelatedObject(IEdmStructuredObject resource, out string errorMessage) + { + throw new NotImplementedException(); + } + } +} diff --git a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkOperationTest.cs b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkOperationTest.cs new file mode 100644 index 0000000000..1be4f2828e --- /dev/null +++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkOperationTest.cs @@ -0,0 +1,1199 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNet.OData; +using Microsoft.AspNet.OData.Extensions; +using Microsoft.AspNet.OData.Routing; +using Microsoft.AspNet.OData.Routing.Conventions; +using Microsoft.Test.E2E.AspNet.OData.Common.Execution; +using Microsoft.Test.E2E.AspNet.OData.Common.Extensions; +using Newtonsoft.Json.Linq; +using Xunit; + +namespace Microsoft.Test.E2E.AspNet.OData.BulkOperation +{ + public class BulkOperationTest : WebHostTestBase + { + public BulkOperationTest(WebHostTestFixture fixture) + :base(fixture) + { + } + + protected override void UpdateConfiguration(WebRouteConfiguration configuration) + { + var controllers = new[] { typeof(EmployeesController), typeof(CompanyController), typeof(MetadataController) }; + configuration.AddControllers(controllers); + + configuration.Routes.Clear(); + configuration.Count().Filter().OrderBy().Expand().MaxTop(null).Select(); + configuration.MapODataServiceRoute("convention", "convention", BulkOperationEdmModel.GetConventionModel(configuration)); + configuration.MapODataServiceRoute("explicit", "explicit", BulkOperationEdmModel.GetExplicitModel(configuration), new DefaultODataPathHandler(), ODataRoutingConventions.CreateDefault()); + configuration.EnsureInitialized(); + } + + #region Update + + [Fact] + public async Task PatchEmployee_WithUpdates() + { + //Arrange + string requestUri = this.BaseAddress + "/convention/Employees(1)"; + + var content = @"{ + 'Name':'Sql' , 'FavoriteSports' :{'Sport': 'Cricket'}, + 'Friends@odata.delta':[{'Id':1,'Name':'Test2'},{'Id':2,'Name':'Test3'}] + }"; + + var requestForPatch = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + + StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + requestForPatch.Content = stringContent; + + //Act & Assert + using (HttpResponseMessage response = await this.Client.SendAsync(requestForPatch)) + { + var json = response.Content.ReadAsStringAsync().Result; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + //Act & Assert + requestUri = this.BaseAddress + "/convention/Employees(1)/Friends"; + using (HttpResponseMessage response = await this.Client.GetAsync(requestUri)) + { + response.EnsureSuccessStatusCode(); + + var json = await response.Content.ReadAsObject(); + var result = json.GetValue("value") as JArray; + + Assert.Equal(2, result.Count); + Assert.Contains("Test2", result.ToString()); + Assert.Contains("Test3", result.ToString()); + } + } + + [Fact] + public async Task PatchEmployee_WithUpdates_WithEmployees() + { + //Arrange + string requestUri = this.BaseAddress + "/convention/Employees(1)"; + + var content = @"{ + 'Name':'Sql' , + 'Friends':[{'Id':345,'Name':'Test2'},{'Id':400,'Name':'Test3'},{'Id':900,'Name':'Test93'}] + }"; + + var requestForPatch = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + + StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + requestForPatch.Content = stringContent; + + //Act & Assert + using (HttpResponseMessage response = await this.Client.SendAsync(requestForPatch)) + { + var json = response.Content.ReadAsStringAsync().Result; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + //Act & Assert + requestUri = this.BaseAddress + "/convention/Employees(1)/Friends"; + using (HttpResponseMessage response = await this.Client.GetAsync(requestUri)) + { + response.EnsureSuccessStatusCode(); + + var json = await response.Content.ReadAsObject(); + var result = json.GetValue("value") as JArray; + + Assert.Equal(3, result.Count); + Assert.Contains("345", result.ToString()); + Assert.Contains("400", result.ToString()); + Assert.Contains("900", result.ToString()); + } + } + + [Fact] + public async Task PatchEmployee_WithUpdates_Friends() + { + //Arrange + string requestUri = this.BaseAddress + "/convention/Employees(1)/Friends"; + + var content = @"{'@odata.type': '#Microsoft.Test.E2E.AspNet.OData.BulkOperation.Friend', + '@odata.context':'" + this.BaseAddress + @"/convention/$metadata#Employees(1)/Friends/$delta', + 'value':[{ 'Id':1,'Name':'Friend1'}, { 'Id':2,'Name':'Friend2'}] + }"; + + var requestForPatch = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + requestForPatch.Headers.Add("OData-Version", "4.01"); + + StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + requestForPatch.Content = stringContent; + Client.DefaultRequestHeaders.Add("Prefer", @"odata.include-annotations=""*"""); + + //Act & Assert + var expected = "$delta\",\"value\":[{\"Id\":1,\"Name\":\"Friend1\",\"Age\":0}," + + "{\"Id\":2,\"Name\":\"Friend2\",\"Age\":0}]}"; + + using (HttpResponseMessage response = await this.Client.SendAsync(requestForPatch)) + { + var json = response.Content.ReadAsStringAsync().Result; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Contains(expected, json.ToString()); + } + + //Act & Assert + requestUri = this.BaseAddress + "/convention/Employees(1)/Friends"; + using (HttpResponseMessage response = await this.Client.GetAsync(requestUri)) + { + response.EnsureSuccessStatusCode(); + + var json = await response.Content.ReadAsObject(); + var result = json.GetValue("value") as JArray; + + Assert.Equal(2, result.Count); + Assert.Contains("Friend1", result.ToString()); + Assert.Contains("Friend2", result.ToString()); + } + } + + [Fact] + public async Task PatchEmployee_WithDeletes_Friends() + { + //Arrange + string requestUri = this.BaseAddress + "/convention/Employees(1)/Friends"; + + var content = @"{'@odata.context':'" + this.BaseAddress + @"/convention/$metadata#Employees(1)/Friends/$delta', + 'value':[{ '@odata.removed' : {'reason':'changed'}, 'Id':1},{ 'Id':2,'Name':'Friend2'}] + }"; + + var requestForPatch = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + requestForPatch.Headers.Add("OData-Version", "4.01"); + + StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + requestForPatch.Content = stringContent; + + //Act & Assert + var expected = "$delta\",\"value\":[{\"@removed\":{\"reason\":\"changed\"}," + + "\"@id\":\""+this.BaseAddress+"/convention/Friends(1)\",\"Id\":1,\"Name\":null,\"Age\":0},{\"Id\":2,\"Name\":\"Friend2\",\"Age\":0}]}"; + + using (HttpResponseMessage response = await this.Client.SendAsync(requestForPatch)) + { + var json = response.Content.ReadAsStringAsync().Result; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Contains(expected.ToLower(), json.ToString().ToLower()); + } + + //Act & Assert + requestUri = this.BaseAddress + "/convention/Employees(1)/Friends"; + using (HttpResponseMessage response = await this.Client.GetAsync(requestUri)) + { + response.EnsureSuccessStatusCode(); + + var json = await response.Content.ReadAsObject(); + var result = json.GetValue("value") as JArray; + + Assert.Single(result); + Assert.Contains("Friend2", result.ToString()); + } + } + + [Fact] + public async Task PatchEmployee_WithDeletes_Friends_WithNestedTypes() + { + //Arrange + string requestUri = this.BaseAddress + "/convention/Employees(1)/Friends"; + + var content = @"{'@odata.context':'" + this.BaseAddress + @"/convention/$metadata#Employees(1)/Friends/$delta', '@odata.type': '#Microsoft.Test.E2E.AspNet.OData.BulkOperation.Friend', + 'value':[{ '@odata.removed' : {'reason':'changed'}, 'Id':1, 'Orders@odata.delta' :[{'Id':1,'Price': 10}, {'Id':2,'Price': 20} ] },{ 'Id':2,'Name':'Friend2'}] + }"; + + var requestForPatch = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + requestForPatch.Headers.Add("OData-Version", "4.01"); + + StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + requestForPatch.Content = stringContent; + + //Act & Assert + var expected = "$delta\",\"value\":[{\"@removed\":{\"reason\":\"changed\"}," + + "\"@id\":\""+this.BaseAddress+"/convention/Friends(1)\",\"Id\":1,\"Name\":null,\"Age\":0},{\"Id\":2,\"Name\":\"Friend2\",\"Age\":0}]}"; + + using (HttpResponseMessage response = await this.Client.SendAsync(requestForPatch)) + { + var json = response.Content.ReadAsStringAsync().Result; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Contains(expected.ToLower(), json.ToString().ToLower()); + } + + //Act & Assert + requestUri = this.BaseAddress + "/convention/Employees(1)/Friends"; + using (HttpResponseMessage response = await this.Client.GetAsync(requestUri)) + { + response.EnsureSuccessStatusCode(); + + var json = await response.Content.ReadAsObject(); + var result = json.GetValue("value") as JArray; + + Assert.Single(result); + Assert.Contains("Friend2", result.ToString()); + } + } + + [Fact] + public async Task PatchEmployee_WithDeletes_Friends_WithNestedDeletes() + { + //Arrange + string requestUri = this.BaseAddress + "/convention/Employees(1)/Friends"; + + var content = @"{'@odata.context':'" + this.BaseAddress + @"/convention/$metadata#Employees(1)/Friends/$delta', '@odata.type': '#Microsoft.Test.E2E.AspNet.OData.BulkOperation.Friend', + 'value':[{ '@odata.removed' : {'reason':'changed'}, 'Id':1, 'Orders@odata.delta' :[{'@odata.removed' : {'reason':'changed'}, 'Id':1,'Price': 10}, {'Id':2,'Price': 20} ] },{ 'Id':2,'Name':'Friend2'}] + }"; + + var requestForPatch = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + requestForPatch.Headers.Add("OData-Version", "4.01"); + + StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + requestForPatch.Content = stringContent; + + //Act & Assert + var expected = "$delta\",\"value\":[{\"@removed\":{\"reason\":\"changed\"}," + + "\"@id\":\""+ this.BaseAddress +"/convention/Friends(1)\",\"Id\":1,\"Name\":null,\"Age\":0},{\"Id\":2,\"Name\":\"Friend2\",\"Age\":0}]}"; + + using (HttpResponseMessage response = await this.Client.SendAsync(requestForPatch)) + { + var json = response.Content.ReadAsStringAsync().Result; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Contains(expected.ToLower(), json.ToString().ToLower()); + } + + requestUri = this.BaseAddress + "/convention/Employees(1)/Friends"; + using (HttpResponseMessage response = await this.Client.GetAsync(requestUri)) + { + response.EnsureSuccessStatusCode(); + + var json = await response.Content.ReadAsObject(); + var result = json.GetValue("value") as JArray; + + Assert.Contains("Friend2", result.ToString()); + } + } + + [Fact] + public async Task PatchEmployee_WithAdds_Friends_WithAnnotations() + { + //Arrange + string requestUri = this.BaseAddress + "/convention/Employees(1)/NewFriends"; + var content = @"{'@odata.context':'" + this.BaseAddress + @"/convention/$metadata#Employees(1)/NewFriends/$delta', + 'value':[{ 'Id':3, 'Age':35, '@NS.Test':1}] + }"; + + var requestForPatch = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + requestForPatch.Headers.Add("OData-Version", "4.01"); + requestForPatch.Content = new StringContent(content); + + requestForPatch.Content.Headers.ContentType= MediaTypeWithQualityHeaderValue.Parse("application/json"); + + Client.DefaultRequestHeaders.Add("Prefer", @"odata.include-annotations=""*"""); + + //Act & Assert + var expected = "$delta\",\"value\":[{\"@NS.Test\":1,\"Id\":3,\"Name\":null,\"Age\":35}]}"; + + using (HttpResponseMessage response = await this.Client.SendAsync(requestForPatch)) + { + var json = response.Content.ReadAsStringAsync().Result; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Contains(expected, json.ToString()); + } + } + + [Fact] + public async Task PatchEmployee_WithFailedAdds_Friends() + { + //Arrange + string requestUri = this.BaseAddress + "/convention/Employees(1)/NewFriends"; + var content = @"{'@odata.context':'" + this.BaseAddress + @"/convention/$metadata#Employees(1)/NewFriends/$delta', + 'value':[{ 'Id':3, 'Age':3, '@NS.Test':1}] + }"; + + var requestForPatch = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + requestForPatch.Headers.Add("OData-Version", "4.01"); + + StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + requestForPatch.Content = stringContent; + Client.DefaultRequestHeaders.Add("Prefer", @"odata.include-annotations=""*"""); + + //Act & Assert + var expected = "$delta\",\"value\":[{\"@NS.Test\":1,\"Id\":3,\"Name\":null,\"Age\":3}]}"; + + using (HttpResponseMessage response = await this.Client.SendAsync(requestForPatch)) + { + var json = response.Content.ReadAsStringAsync().Result; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Contains(expected, json.ToString()); + } + } + + [Fact] + public async Task PatchEmployee_WithFailedDeletes_Friends() + { + //Arrange + string requestUri = this.BaseAddress + "/convention/Employees(2)/NewFriends"; + var content = @"{'@odata.context':'" + this.BaseAddress + @"/convention/$metadata#Employees(1)/NewFriends/$delta', + 'value':[{ '@odata.removed' : {'reason':'changed'}, 'Id':2, '@NS.Test':1}] + }"; + + var requestForPatch = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + requestForPatch.Headers.Add("OData-Version", "4.01"); + + StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + requestForPatch.Content = stringContent; + Client.DefaultRequestHeaders.Add("Prefer", @"odata.include-annotations=""*"""); + + //Act & Assert + var expected = "$delta\",\"value\":[{\"@NS.Test\":1,\"@Core.DataModificationException\":" + + "{\"@type\":\"#Org.OData.Core.V1.DataModificationExceptionType\"},\"Id\":2,\"Name\":null,\"Age\":15}]}"; + + using (HttpResponseMessage response = await this.Client.SendAsync(requestForPatch)) + { + var json = response.Content.ReadAsStringAsync().Result; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Contains("$delta", json); + Assert.Contains(expected, json.ToString()); + } + } + + [Fact] + public async Task PatchEmployee_WithFailedOperation_WithAnnotations() + { + //Arrange + string requestUri = this.BaseAddress + "/convention/Employees(2)/NewFriends"; + var content = @"{'@odata.context':'" + this.BaseAddress + @"/convention/$metadata#Employees(2)/NewFriends/$delta', + 'value':[{ '@odata.removed' : {'reason':'changed'}, 'Id':2, '@Core.ContentID':3, '@NS.Test2':'testing'}] + }"; + + var requestForPatch = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + requestForPatch.Headers.Add("OData-Version", "4.01"); + + StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + requestForPatch.Content = stringContent; + Client.DefaultRequestHeaders.Add("Prefer", @"odata.include-annotations=""*"""); + + //Act & Assert + var expected = "/convention/$metadata#NewFriends/$delta\",\"value\":[{\"@NS.Test2\":\"testing\",\"@Core.ContentID\":3," + + "\"@Core.DataModificationException\":{\"@type\":\"#Org.OData.Core.V1.DataModificationExceptionType\"},\"Id\":2,\"Name\":null,\"Age\":15}]}"; + + using (HttpResponseMessage response = await this.Client.SendAsync(requestForPatch)) + { + var json = response.Content.ReadAsStringAsync().Result; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var str = json.ToString(); + Assert.Contains("$delta",str); + Assert.Contains("NS.Test2", str); + Assert.Contains("Core.DataModificationException", str); + Assert.Contains(expected, str); + } + } + + [Fact] + public async Task PatchUntypedEmployee_WithAdds_Friends_Untyped() + { + //Arrange + string requestUri = this.BaseAddress + "/convention/UnTypedEmployees"; + + string content = @"{'@odata.context':'" + this.BaseAddress + @"/convention/$metadata#UnTypedEmployees/$delta', + 'value':[{ 'ID':1,'Name':'Employee1', + 'UnTypedFriends@odata.delta':[{'Id':1,'Name':'Friend1'},{'Id':2,'Name':'Friend2'}] + }, + { 'ID':2,'Name':'Employee2', + 'UnTypedFriends@odata.delta':[{'Id':3,'Name':'Friend3'},{'Id':4,'Name':'Friend4'}] + }] + }"; + + var requestForPatch = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + requestForPatch.Headers.Add("OData-Version", "4.01"); + + StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + requestForPatch.Content = stringContent; + Client.DefaultRequestHeaders.Add("Prefer", @"odata.include-annotations=""*"""); + + //Act & Assert + var expected = "/convention/$metadata#UnTypedEmployees/$delta\",\"value\":[{\"ID\":1,\"Name\":\"Employee1\",\"UnTypedFriends@delta\":" + + "[{\"Id\":1,\"Name\":\"Friend1\",\"Age\":0},{\"Id\":2,\"Name\":\"Friend2\",\"Age\":0}]},{\"ID\":2,\"Name\":\"Employee2\",\"UnTypedFriends@delta\":" + + "[{\"Id\":3,\"Name\":\"Friend3\",\"Age\":0},{\"Id\":4,\"Name\":\"Friend4\",\"Age\":0}]}]}"; + + using (HttpResponseMessage response = await this.Client.SendAsync(requestForPatch)) + { + var json = response.Content.ReadAsStringAsync().Result; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Contains(expected, json.ToString()); + } + } + + [Fact] + public async Task PatchEmployee_WithAdds_Friends_WithNested_Untyped() + { + //Arrange + string requestUri = this.BaseAddress + "/convention/Employees(1)/UnTypedFriends"; + var content = @"{'@odata.context':'" + this.BaseAddress + @"/convention/$metadata#Employees(1)/UnTypedFriends/$delta', + 'value':[{ 'Id':2, 'Name': 'Friend007', 'Age':35,'Address@odata.delta':{'Id':1, 'Street' : 'Abc 123'}, '@NS.Test':1}] + }"; + + var requestForPatch = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + + StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + requestForPatch.Content = stringContent; + Client.DefaultRequestHeaders.Add("Prefer", @"odata.include-annotations=""*"""); + + //Act & Assert + using (HttpResponseMessage response = await this.Client.SendAsync(requestForPatch)) + { + var json = response.Content.ReadAsStringAsync().Result; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + json.ToString().Contains("$delta"); + json.ToString().Contains("@NS.Test"); + } + } + + [Fact] + public async Task PatchEmployee_WithAdds_Friends_WithAnnotations_Untyped() + { + //Arrange + string requestUri = this.BaseAddress + "/convention/Employees(2)/UnTypedFriends"; + var content = @"{'@odata.context':'" + this.BaseAddress + @"/convention/$metadata#Employees(2)/UnTypedFriends/$delta', + 'value':[{ 'Id':2, 'Age':35, '@NS.Test':1}] + }"; + + var requestForPatch = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + + StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + requestForPatch.Content = stringContent; + Client.DefaultRequestHeaders.Add("Prefer", @"odata.include-annotations=""*"""); + + //Act & Assert + using (HttpResponseMessage response = await this.Client.SendAsync(requestForPatch)) + { + var json = response.Content.ReadAsStringAsync().Result; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + json.ToString().Contains("$delta"); + json.ToString().Contains("@NS.Test"); + } + } + + [Fact] + public async Task PatchEmployee_WithFailedAdds_Friends_Untyped() + { + //Arrange + string requestUri = this.BaseAddress + "/convention/Employees(3)/UnTypedFriends"; + var content = @"{'@odata.context':'" + this.BaseAddress + @"/convention/$metadata#Employees(3)/UnTypedFriends/$delta', + 'value':[{ 'Id':3, 'Age':3, '@NS.Test':1}] + }"; + + var requestForPatch = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + + StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + requestForPatch.Content = stringContent; + Client.DefaultRequestHeaders.Add("Prefer", @"odata.include-annotations=""*"""); + + //Act & Assert + using (HttpResponseMessage response = await this.Client.SendAsync(requestForPatch)) + { + var json = response.Content.ReadAsStringAsync().Result; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + json.ToString().Contains("$deletedEntity"); + } + } + + [Fact] + public async Task PatchEmployee_WithFailedDeletes_Friends_Untyped() + { + //Arrange + string requestUri = this.BaseAddress + "/convention/Employees(3)/UnTypedFriends"; + var content = @"{'@odata.context':'" + this.BaseAddress + @"/convention/$metadata#Employees(3)/UnTypedFriends/$delta', + 'value':[{ '@odata.removed' : {'reason':'changed'}, 'Id':5, '@NS.Test':1}] + }"; + + var requestForPatch = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + + StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + requestForPatch.Content = stringContent; + Client.DefaultRequestHeaders.Add("Prefer", @"odata.include-annotations=""*"""); + + //Act & Assert + using (HttpResponseMessage response = await this.Client.SendAsync(requestForPatch)) + { + var json = response.Content.ReadAsStringAsync().Result; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Contains("@Core.DataModificationException", json.ToString()); + Assert.Contains("@NS.Test", json.ToString()); + } + } + + [Fact] + public async Task PatchEmployee_WithFailedOperation_WithAnnotations_Untyped() + { + //Arrange + string requestUri = this.BaseAddress + "/convention/Employees(3)/UnTypedFriends"; + var content = @"{'@odata.context':'" + this.BaseAddress + @"/convention/$metadata#Employees(3)/UnTypedFriends/$delta', + 'value':[{ '@odata.removed' : {'reason':'changed'}, 'Id':5, '@Core.ContentID':3, '@NS.Test2':'testing'}] + }"; + + var requestForPatch = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + + StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + requestForPatch.Content = stringContent; + Client.DefaultRequestHeaders.Add("Prefer", @"odata.include-annotations=""*"""); + + //Act & Assert + using (HttpResponseMessage response = await this.Client.SendAsync(requestForPatch)) + { + var json = response.Content.ReadAsStringAsync().Result; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var str = json.ToString(); + Assert.Contains("$delta", str); + Assert.Contains("NS.Test2", str); + Assert.Contains("Core.DataModificationException", str); + Assert.Contains("Core.ContentID", str); + } + } + + [Fact] + public async Task PatchEmployee_WithUnchanged_Employee() + { + //Arrange + string requestUri = this.BaseAddress + "/convention/Employees"; + + var content = @"{'@odata.context':'" + this.BaseAddress + @"/convention/$metadata#Employees/$delta', + 'value':[{ '@odata.type': '#Microsoft.Test.E2E.AspNet.OData.BulkOperation.Employee', 'ID':1,'Name':'Name1', + 'Friends@odata.delta':[{'Id':1,'Name':'Test0','Age':33},{'Id':2,'Name':'Test1'}] + }] + }"; + + var requestForPatch = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + requestForPatch.Headers.Add("OData-Version", "4.01"); + + StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + requestForPatch.Content = stringContent; + + //Act & Assert + var expected = "\"value\":[{\"ID\":1,\"Name\":\"Name1\",\"SkillSet\":[],\"Gender\":\"0\",\"AccessLevel\":\"0\",\"FavoriteSports\":null," + + "\"Friends@delta\":[{\"Id\":1,\"Name\":\"Test0\",\"Age\":33}," + + "{\"Id\":2,\"Name\":\"Test1\",\"Age\":0}]}]}"; + + using (HttpResponseMessage response = await this.Client.SendAsync(requestForPatch)) + { + var json = response.Content.ReadAsStringAsync().Result; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Contains(expected, json.ToString()); + + // Navigation properties absent from the payload are not serialized in the response. + Assert.DoesNotContain("NewFriends@delta", json.ToString()); + Assert.DoesNotContain("UntypedFriends@delta", json.ToString()); + } + } + + [Fact] + public async Task PatchEmployee_WithUpdates_Employees() + { + //Arrange + string requestUri = this.BaseAddress + "/convention/Employees"; + + var content = @"{'@odata.context':'"+ this.BaseAddress + @"/convention/$metadata#Employees/$delta', + 'value':[{ '@odata.type': '#Microsoft.Test.E2E.AspNet.OData.BulkOperation.Employee', 'ID':1,'Name':'Employee1', + 'Friends@odata.delta':[{'Id':1,'Name':'Friend1', + 'Orders@odata.delta' :[{'Id':1,'Price': 10}, {'Id':2,'Price': 20} ] },{'Id':2,'Name':'Friend2'}] + }, + { '@odata.type': '#Microsoft.Test.E2E.AspNet.OData.BulkOperation.Employee', 'ID':2,'Name':'Employee2', + 'Friends@odata.delta':[{'Id':3,'Name':'Friend3', + 'Orders@odata.delta' :[{'Id':3,'Price': 30}, {'Id':4,'Price': 40} ]},{'Id':4,'Name':'Friend4'}] + }] + }"; + + var requestForPatch = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + requestForPatch.Headers.Add("OData-Version", "4.01"); + + StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + requestForPatch.Content = stringContent; + + //Act & Assert + var expected = "\"value\":[{\"ID\":1,\"Name\":\"Employee1\",\"SkillSet\":[],\"Gender\":\"0\",\"AccessLevel\":\"0\",\"FavoriteSports\":null," + + "\"Friends@delta\":[{\"Id\":1,\"Name\":\"Friend1\",\"Age\":0,\"Orders@delta\":[{\"Id\":1,\"Price\":10},{\"Id\":2,\"Price\":20}]}," + + "{\"Id\":2,\"Name\":\"Friend2\",\"Age\":0}]},{\"ID\":2,\"Name\":\"Employee2\",\"SkillSet\":[],\"Gender\":\"0\",\"AccessLevel\":\"0\",\"FavoriteSports\":null," + + "\"Friends@delta\":[{\"Id\":3,\"Name\":\"Friend3\",\"Age\":0,\"Orders@delta\":[{\"Id\":3,\"Price\":30},{\"Id\":4,\"Price\":40}]},{\"Id\":4,\"Name\":\"Friend4\",\"Age\":0}]}]}"; + + using (HttpResponseMessage response = await this.Client.SendAsync(requestForPatch)) + { + var json = response.Content.ReadAsStringAsync().Result; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Contains(expected, json.ToString()); + Assert.DoesNotContain("NewFriends@delta", json.ToString()); + Assert.DoesNotContain("UntypedFriends@delta", json.ToString()); + } + + //Act & Assert + requestUri = this.BaseAddress + "/convention/Employees(1)/Friends"; + using (HttpResponseMessage response = await this.Client.GetAsync(requestUri)) + { + response.EnsureSuccessStatusCode(); + + var json = response.Content.ReadAsStringAsync().Result; + + Assert.Contains("Friend1", json.ToString()); + Assert.Contains("Friend2", json.ToString()); + } + + //Act & Assert + requestUri = this.BaseAddress + "/convention/Employees(2)?$expand=Friends"; + using (HttpResponseMessage response = await this.Client.GetAsync(requestUri)) + { + response.EnsureSuccessStatusCode(); + + var json = response.Content.ReadAsStringAsync().Result; + + Assert.Contains("Friend3", json.ToString()); + Assert.Contains("Friend4", json.ToString()); + } + } + + [Fact] + public async Task PatchEmployee_WithDelete() + { + //Arrange + string requestUri = this.BaseAddress + "/convention/Employees(1)"; + + var content = @"{ + 'Name':'Sql' , + 'Friends@odata.delta':[{ '@odata.removed' : {'reason':'changed'}, 'Id':1}] + }"; + + var requestForPatch = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + + StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + requestForPatch.Content = stringContent; + + //Act & Assert + var expected = "/convention/$metadata#Employees/$entity\",\"ID\":1,\"Name\":\"Sql\"," + + "\"SkillSet\":[\"CSharp\",\"Sql\"],\"Gender\":\"Female\",\"AccessLevel\":\"Execute\",\"FavoriteSports\":{\"Sport\":\"Football\"}}"; + + using (HttpResponseMessage response = await this.Client.SendAsync(requestForPatch)) + { + var json = response.Content.ReadAsStringAsync().Result; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Contains(expected, json.ToString()); + } + + //Act & Assert + requestUri = this.BaseAddress + "/convention/Employees(1)/Friends"; + using (HttpResponseMessage response = await this.Client.GetAsync(requestUri)) + { + response.EnsureSuccessStatusCode(); + + var json = await response.Content.ReadAsObject(); + var result = json.GetValue("value") as JArray; + + Assert.Single(result); + Assert.DoesNotContain("Test0", result.ToString()); + } + } + + [Fact] + public async Task PatchEmployee_WithODataBind() + { + //Arrange + string requestUri = this.BaseAddress + "/convention/Employees(1)?$expand=Friends"; + + var content = @"{ + 'Name':'Bind1' , + 'Friends@odata.bind':['Friends(3)'] + }"; + + var requestForPatch = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + + StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + requestForPatch.Content = stringContent; + + //Act & Assert + var expected = "/convention/$metadata#Employees(Friends())/$entity\",\"ID\":1,\"Name\":\"Bind1\"," + + "\"SkillSet\":[\"CSharp\",\"Sql\"],\"Gender\":\"Female\",\"AccessLevel\":\"Execute\",\"FavoriteSports\":{\"Sport\":\"Football\"},\"Friends\":[{\"Id\":3,\"Name\":null,\"Age\":0}]}"; + + using (HttpResponseMessage response = await this.Client.SendAsync(requestForPatch)) + { + var json = response.Content.ReadAsStringAsync().Result; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Contains(expected, json.ToString()); + } + } + + [Fact] + public async Task PatchEmployee_WithAddUpdateAndDelete() + { + //Arrange + string requestUri = this.BaseAddress + "/convention/Employees(1)"; + + var content = @"{ + 'Name':'Sql' , + 'Friends@odata.delta':[{ '@odata.removed' : {'reason':'changed'}, 'Id':1},{'Id':2,'Name':'Test3'},{'Id':3,'Name':'Test4'}] + }"; + + var requestForPatch = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + + StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + requestForPatch.Content = stringContent; + + //Act & Assert + using (HttpResponseMessage response = await this.Client.SendAsync(requestForPatch)) + { + var json = response.Content.ReadAsStringAsync().Result; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + //Act & Assert + requestUri = this.BaseAddress + "/convention/Employees(1)/Friends"; + using (HttpResponseMessage response = await this.Client.GetAsync(requestUri)) + { + response.EnsureSuccessStatusCode(); + + var json = await response.Content.ReadAsObject(); + var result = json.GetValue("value") as JArray; + + Assert.Equal(2, result.Count); + Assert.DoesNotContain("Test0", result.ToString()); + Assert.Contains("Test3", result.ToString()); + Assert.Contains("Test4", result.ToString()); + } + } + + [Fact] + public async Task PatchEmployee_WithMultipleFriendUpdatesAndOneDelete() + { + //Arrange + string requestUri = this.BaseAddress + "/convention/Employees(1)"; + + var content = @"{ + 'Name':'Sql' , + 'Friends@odata.delta':[{ '@odata.removed' : {'reason':'changed'}, 'Id':1},{'Id':1,'Name':'Test_1'},{'Id':2,'Name':'Test3'},{'Id':3,'Name':'Test4'}] + }"; + + var requestForPatch = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + + StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + requestForPatch.Content = stringContent; + + //Act & Assert + using (HttpResponseMessage response = await this.Client.SendAsync(requestForPatch)) + { + var json = response.Content.ReadAsStringAsync().Result; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + //Act & Assert + requestUri = this.BaseAddress + "/convention/Employees(1)/Friends"; + using (HttpResponseMessage response = await this.Client.GetAsync(requestUri)) + { + response.EnsureSuccessStatusCode(); + + var json = await response.Content.ReadAsObject(); + var result = json.GetValue("value") as JArray; + + Assert.Equal(3, result.Count); + Assert.DoesNotContain("Test0", result.ToString()); + Assert.Contains("Test_1", result.ToString()); + Assert.Contains("Test3", result.ToString()); + Assert.Contains("Test4", result.ToString()); + } + } + + [Fact] + public async Task PatchEmployee_WithMultipleFriendUpdatesAndMultipleDelete() + { + //Arrange + string requestUri = this.BaseAddress + "/convention/Employees(1)"; + + var content = @"{ + 'Name':'Sql' , + 'Friends@odata.delta':[{ '@odata.removed' : {'reason':'changed'}, 'Id':1},{'Id':1,'Name':'Test_1'},{'Id':2,'Name':'Test3'},{'Id':3,'Name':'Test4'},{ '@odata.removed' : {'reason':'changed'}, 'Id':1}] + }"; + + var requestForPatch = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + + StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + requestForPatch.Content = stringContent; + + //Act & Assert + using (HttpResponseMessage response = await this.Client.SendAsync(requestForPatch)) + { + var json = response.Content.ReadAsStringAsync().Result; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + //Act & Assert + requestUri = this.BaseAddress + "/convention/Employees(1)/Friends"; + using (HttpResponseMessage response = await this.Client.GetAsync(requestUri)) + { + response.EnsureSuccessStatusCode(); + + var json = await response.Content.ReadAsObject(); + var result = json.GetValue("value") as JArray; + + Assert.Equal(2, result.Count); + Assert.DoesNotContain("Test0", result.ToString()); + Assert.DoesNotContain("Test_1", result.ToString()); + Assert.Contains("Test3", result.ToString()); + Assert.Contains("Test4", result.ToString()); + } + } + + [Fact] + public async Task PatchCompanies_WithUpdates_ODataId() + { + //Arrange + string requestUri = this.BaseAddress + "/convention/Companies"; + + var content = @"{'@odata.context':'" + this.BaseAddress + @"/convention/$metadata#Companies/$delta', + 'value':[{ '@odata.type': '#Microsoft.Test.E2E.AspNet.OData.BulkOperation.Company', 'Id':1,'Name':'Company01', + 'OverdueOrders@odata.delta':[{'@odata.id':'Employees(1)/NewFriends(1)/NewOrders(1)', 'Quantity': 9}] + }] + }"; + + var requestForPatch = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + requestForPatch.Headers.Add("OData-Version", "4.01"); + + StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + requestForPatch.Content = stringContent; + + //Act & Assert + var expected = "/convention/$metadata#Companies/$delta\",\"value\":[{\"Id\":1,\"Name\":\"Company01\",\"OverdueOrders@delta\":" + + "[{\"Id\":1,\"Price\":101,\"Quantity\":9}]}]}"; + + using (HttpResponseMessage response = await this.Client.SendAsync(requestForPatch)) + { + var json = response.Content.ReadAsStringAsync().Result; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Contains(expected, json.ToString()); + } + + requestUri = this.BaseAddress + "/convention/Companies(1)/OverdueOrders"; + using (HttpResponseMessage response = await this.Client.GetAsync(requestUri)) + { + var json = response.Content.ReadAsStringAsync().Result; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + //Assert.Contains(expected, json.ToString()); + } + } + + [Fact] + public async Task PatchCompanies_WithUpdates_ODataId_WithCast() + { + //Arrange + string requestUri = this.BaseAddress + "/convention/Companies"; + + var content = @"{'@odata.context':'" + this.BaseAddress + @"/convention/$metadata#Companies/$delta', + 'value':[{ '@odata.type': '#Microsoft.Test.E2E.AspNet.OData.BulkOperation.Company', 'Id':2,'Name':'Company03', + 'MyOverdueOrders@odata.delta':[{'@odata.id':'Employees(2)/NewFriends(2)/Microsoft.Test.E2E.AspNet.OData.BulkOperation.MyNewFriend/MyNewOrders(2)', 'Quantity': 9}] + }] + }"; + + var requestForPatch = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + requestForPatch.Headers.Add("OData-Version", "4.01"); + + StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + requestForPatch.Content = stringContent; + + //Act & Assert + var expected = "$delta\",\"value\":[{\"Id\":2,\"Name\":\"Company03\",\"MyOverdueOrders@delta\":" + + "[{\"Id\":2,\"Price\":444,\"Quantity\":9}]}]}"; + + using (HttpResponseMessage response = await this.Client.SendAsync(requestForPatch)) + { + var json = response.Content.ReadAsStringAsync().Result; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Contains(expected, json.ToString()); + } + } + + [Fact] + public async Task PatchCompanies_WithMultipleUpdates_ODataId_WithCast() + { + //Arrange + string requestUri = this.BaseAddress + "/convention/Companies"; + + var content = @"{'@odata.context':'" + this.BaseAddress + @"/convention/$metadata#Companies/$delta', + 'value':[{ '@odata.type': '#Microsoft.Test.E2E.AspNet.OData.BulkOperation.Company', 'Id':2,'Name':'Company03', + 'MyOverdueOrders@odata.delta':[{'Id': 1, 'Price': 10, 'Quantity': 5}, {'@odata.id':'Employees(2)/NewFriends(2)/Microsoft.Test.E2E.AspNet.OData.BulkOperation.MyNewFriend/MyNewOrders(2)', 'Quantity': 9}] + }] + }"; + + var requestForPatch = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + requestForPatch.Headers.Add("OData-Version", "4.01"); + + StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + requestForPatch.Content = stringContent; + + //Act & Assert + var expected = "\"value\":[{\"Id\":2,\"Name\":\"Company03\",\"MyOverdueOrders@delta\":" + + "[{\"Id\":1,\"Price\":10,\"Quantity\":5},{\"Id\":2,\"Price\":444,\"Quantity\":9}]}]}"; + + using (HttpResponseMessage response = await this.Client.SendAsync(requestForPatch)) + { + var json = response.Content.ReadAsStringAsync().Result; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Contains(expected, json.ToString()); + } + } + + [Fact] + public async Task PatchCompanies_WithMultipleReplace() + { + //Arrange + string requestUri = this.BaseAddress + "/convention/Companies"; + + var content = @"{'@odata.context':'" + this.BaseAddress + @"/convention/$metadata#Companies/$delta', + 'value':[{ '@odata.type': '#Microsoft.Test.E2E.AspNet.OData.BulkOperation.Company', 'Id':2,'Name':'Company03', + 'MyOverdueOrders':[{'Id': 1, 'Price': 10, 'Quantity': 5},{'Id': 2, 'Price': 20, 'Quantity': 10}] + }] + }"; + + var requestForPatch = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + requestForPatch.Headers.Add("OData-Version", "4.01"); + + StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + requestForPatch.Content = stringContent; + + using (HttpResponseMessage response = await this.Client.SendAsync(requestForPatch)) + { + var json = response.Content.ReadAsStringAsync().Result; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + //Act & Assert + var expected = "\"value\":[{\"Id\":1,\"Price\":10,\"Quantity\":5},{\"Id\":2,\"Price\":20,\"Quantity\":10}]}"; + + requestUri = this.BaseAddress + "/convention/Companies(2)/MyOverdueOrders"; + using (HttpResponseMessage response = await this.Client.GetAsync(requestUri)) + { + var json = response.Content.ReadAsStringAsync().Result; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Contains(expected, json.ToString()); + } + } + + [Fact] + public async Task PatchUntypedEmployee_WithOdataId() + { + //Arrange + string requestUri = this.BaseAddress + "/convention/UnTypedEmployees"; + + var content = @"{'@odata.context':'" + this.BaseAddress + @"/convention/$metadata#UnTypedEmployees/$delta', + 'value':[{ 'ID':1,'Name':'Employeeabcd', + 'UnTypedFriends@odata.delta':[{'@odata.id':'UnTypedEmployees(1)/UnTypedFriends(1)', 'Name':'abcd'}] + }] + }"; + + var requestForPatch = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + requestForPatch.Headers.Add("OData-Version", "4.01"); + + StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + requestForPatch.Content = stringContent; + Client.DefaultRequestHeaders.Add("Prefer", @"odata.include-annotations=""*"""); + + //Act & Assert + var expected = "/convention/$metadata#UnTypedEmployees/$delta\",\"value\":[{\"ID\":1,\"Name\":\"Employeeabcd\"," + + "\"UnTypedFriends@delta\":[{\"Id\":0,\"Name\":\"abcd\",\"Age\":0}]}]}"; + + using (HttpResponseMessage response = await this.Client.SendAsync(requestForPatch)) + { + var json = response.Content.ReadAsStringAsync().Result; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Contains(expected, json.ToString()); + } + } + + #endregion + + //#region Post + // Commented until we redesign the Bulk Insert + //[Fact] + //public async Task PostCompany_WithODataId() + //{ + // //Arrange + + // string requestUri = this.BaseAddress + "/convention/Companies"; + + // var content = @"{'Id':3,'Name':'Company03', + // 'OverdueOrders':[{'@odata.id':'Employees(1)/NewFriends(1)/NewOrders(1)'}] + // }"; + + // var requestForPost = new HttpRequestMessage(new HttpMethod("POST"), requestUri); + + // StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + // requestForPost.Content = stringContent; + + // //Act & Assert + // using (HttpResponseMessage response = await this.Client.SendAsync(requestForPost)) + // { + // var json = response.Content.ReadAsStringAsync().Result; + // Assert.Equal(HttpStatusCode.OK, response.StatusCode); + // } + //} + + //[Fact] + //public async Task PostCompany_WithODataId_AndWithout() + //{ + // //Arrange + + // string requestUri = this.BaseAddress + "/convention/Companies"; + + // var content = @"{'Id':4,'Name':'Company04', + // 'OverdueOrders':[{'@odata.id':'Employees(1)/NewFriends(1)/NewOrders(1)'},{Price:30}] + // }"; + + // var requestForPost = new HttpRequestMessage(new HttpMethod("POST"), requestUri); + + // StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + // requestForPost.Content = stringContent; + + // //Act & Assert + // using (HttpResponseMessage response = await this.Client.SendAsync(requestForPost)) + // { + // var json = response.Content.ReadAsStringAsync().Result; + // Assert.Equal(HttpStatusCode.OK, response.StatusCode); + // } + //} + + //[Fact] + //public async Task PostEmployee_WithCreateFriends() + //{ + // //Arrange + + // string requestUri = this.BaseAddress + "/convention/Employees"; + + // var content = @"{ + // 'Name':'SqlUD', + // 'Friends':[{ 'Id':1001, 'Name' : 'Friend 1001', 'Age': 31},{ 'Id':1002, 'Name' : 'Friend 1002', 'Age': 32},{ 'Id':1003, 'Name' : 'Friend 1003', 'Age': 33}] + // }"; + + // var requestForPost = new HttpRequestMessage(new HttpMethod("POST"), requestUri); + + // StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + // requestForPost.Content = stringContent; + + // var expected = "Friends\":[{\"Id\":1001,\"Name\":\"Friend 1001\",\"Age\":31},{\"Id\":1002,\"Name\":\"Friend 1002\",\"Age\":32},{\"Id\":1003,\"Name\":\"Friend 1003\",\"Age\":33}]"; + + // //Act & Assert + // using (HttpResponseMessage response = await this.Client.SendAsync(requestForPost)) + // { + // Assert.Equal(HttpStatusCode.OK, response.StatusCode); + // var json = response.Content.ReadAsStringAsync().Result; + // Assert.Contains("SqlUD", json); + // Assert.Contains(expected, json); + // } + //} + + //[Fact] + //public async Task PostEmployee_WithCreateFriendsFullMetadata() + //{ + // //Arrange + + // string requestUri = this.BaseAddress + "/convention/Employees?$format=application/json;odata.metadata=full"; + + // string content = @"{ + // 'Name':'SqlUD', + // 'Friends':[{ 'Id':1001, 'Name' : 'Friend 1001', 'Age': 31},{ 'Id':1002, 'Name' : 'Friend 1002', 'Age': 32},{ 'Id':1003, 'Name' : 'Friend 1003', 'Age': 33}] + // }"; + + // var requestForPost = new HttpRequestMessage(new HttpMethod("POST"), requestUri); + + // StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + // requestForPost.Content = stringContent; + + // string friendsNavigationLink = "Friends@odata.navigationLink"; + // string newFriendsNavigationLink = "NewFriends@odata.navigationLink"; + // string untypedFriendsNavigationLink = "UnTypedFriends@odata.navigationLink"; + + // string expected = "Friends\":[{\"@odata.type\":\"#Microsoft.Test.E2E.AspNet.OData.BulkOperation.Friend\""; + + // //Act & Assert + // using (HttpResponseMessage response = await this.Client.SendAsync(requestForPost)) + // { + // Assert.Equal(HttpStatusCode.OK, response.StatusCode); + // var json = response.Content.ReadAsStringAsync().Result; + // Assert.Contains("SqlUD", json); + // Assert.Contains(expected, json); + // Assert.Contains(friendsNavigationLink, json); + // Assert.Contains(newFriendsNavigationLink, json); + // Assert.Contains(untypedFriendsNavigationLink, json); + // } + //} + + //[Fact] + //public async Task PostEmployee_WithFullMetadata() + //{ + // //Arrange + + // string requestUri = this.BaseAddress + "/convention/Employees?$format=application/json;odata.metadata=full"; + + // var content = @"{ + // 'Name':'SqlUD' + // }"; + + // var requestForPost = new HttpRequestMessage(new HttpMethod("POST"), requestUri); + + // StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + // requestForPost.Content = stringContent; + + // string friendsNavigationLink = "Friends@odata.navigationLink"; + // string newFriendsNavigationLink = "NewFriends@odata.navigationLink"; + // string untypedFriendsNavigationLink = "UnTypedFriends@odata.navigationLink"; + + // //Act & Assert + // using (HttpResponseMessage response = await this.Client.SendAsync(requestForPost)) + // { + // Assert.Equal(HttpStatusCode.OK, response.StatusCode); + // var json = response.Content.ReadAsStringAsync().Result; + // Assert.Contains("SqlUD", json); + // Assert.Contains(friendsNavigationLink, json); + // Assert.Contains(newFriendsNavigationLink, json); + // Assert.Contains(untypedFriendsNavigationLink, json); + // } + //} + + //#endregion + + #region Full Metadata + + [Fact] + public async Task GetEmployee_WithFullMetadata() + { + //Arrange + + string requestUri = this.BaseAddress + "/convention/Employees(1)?$format=application/json;odata.metadata=full"; + + var requestForPatch = new HttpRequestMessage(new HttpMethod("GET"), requestUri); + + string friendsNavigationLink = "Friends@odata.navigationLink"; + string newFriendsNavigationLink = "NewFriends@odata.navigationLink"; + string untypedFriendsNavigationLink = "UnTypedFriends@odata.navigationLink"; + + string notexpected = "Friends\":[{\"@odata.type\":\"#Microsoft.Test.E2E.AspNet.OData.BulkOperation.Friend\""; + + //Act & Assert + using (HttpResponseMessage response = await this.Client.SendAsync(requestForPatch)) + { + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var json = response.Content.ReadAsStringAsync().Result; + Assert.DoesNotContain(notexpected, json); + Assert.Contains(friendsNavigationLink, json); + Assert.Contains(newFriendsNavigationLink, json); + Assert.Contains(untypedFriendsNavigationLink, json); + } + } + + #endregion + } +} \ No newline at end of file diff --git a/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/DeltaSetOfTTest.cs b/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/DeltaSetOfTTest.cs new file mode 100644 index 0000000000..58380742ae --- /dev/null +++ b/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/DeltaSetOfTTest.cs @@ -0,0 +1,428 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNet.OData.Builder; +using Microsoft.AspNet.OData.Test.Abstraction; +using Microsoft.OData.Edm; +using Microsoft.OData.UriParser; +using Xunit; + +namespace Microsoft.AspNet.OData.Test +{ + public class DeltaSetOfTTest + { + public static List friends; + + [Fact] + public void DeltaSet_Patch() + { + //Arrange + var lstId = new List(); + lstId.Add("Id"); + var deltaSet = new DeltaSet(lstId); + + var edmChangedObj1 = new Delta(); + edmChangedObj1.TrySetPropertyValue("Id", 1); + edmChangedObj1.TrySetPropertyValue("Name", "Friend1"); + + var edmChangedObj2 = new Delta(); + edmChangedObj2.TrySetPropertyValue("Id", 2); + edmChangedObj2.TrySetPropertyValue("Name", "Friend2"); + + ODataConventionModelBuilder builder = ODataConventionModelBuilderFactory.Create(); + var friendsSet = builder.EntitySet("Friends"); + var model = builder.GetEdmModel(); + + var keys = new[] { new KeyValuePair("Id", 1) }; + var lst1 = new List(); + lst1.Add(new EntitySetSegment(model.EntityContainer.FindEntitySet("Friends")) { Identifier = "NewFriends" }); + lst1.Add(new KeySegment(keys, null, null)); + + var keys2 = new[] { new KeyValuePair("Id", 2) }; + var lst2 = new List(); + lst2.Add(new EntitySetSegment(model.EntityContainer.FindEntitySet("Friends")) { Identifier = "NewFriends" }); + lst2.Add(new KeySegment(keys2, null, null)); + + edmChangedObj1.ODataPath = new ODataPath(lst1); + edmChangedObj2.ODataPath = new ODataPath(lst2); + + deltaSet.Add(edmChangedObj1); + deltaSet.Add(edmChangedObj2); + + var friends = new List(); + friends.Add(new Friend { Id = 1, Name = "Test1" }); + friends.Add(new Friend { Id = 2, Name = "Test2" }); + + //Act + deltaSet.Patch(friends); + + //Assert + Assert.Equal(2, friends.Count); + Assert.Equal("Friend1", friends[0].Name); + Assert.Equal("Friend2", friends[1].Name); + } + + [Fact] + public void DeltaSet_Add_WrongItem_ThrowsError() + { + //Assign + + var edmChangedObjectcollection = new DeltaSet(new List() { "Id" }); + + var edmChangedObj1 = new Delta(); + edmChangedObj1.TrySetPropertyValue("Id", 1); + edmChangedObj1.TrySetPropertyValue("Name", "Friend1"); + + //Act & Assert + Assert.Throws(() => edmChangedObjectcollection.Add(edmChangedObj1)); + } + + [Fact] + public void DeltaSet_Patch_WithDeletes() + { + //Arrange + var deltaSet = new DeltaSet(new List() { "Id" }); + + ODataConventionModelBuilder builder = ODataConventionModelBuilderFactory.Create(); + var friendsSet = builder.EntitySet("Friends"); + var model = builder.GetEdmModel(); + + var edmChangedObj1 = new Delta(); + edmChangedObj1.TrySetPropertyValue("Id", 1); + edmChangedObj1.TrySetPropertyValue("Name", "Friend1"); + + var edmChangedObj2 = new DeltaDeletedEntityObject(); + edmChangedObj2.TrySetPropertyValue("Id", 2); + + var keys = new[] { new KeyValuePair("Id", 1) }; + var lst1 = new List(); + lst1.Add(new EntitySetSegment(model.EntityContainer.FindEntitySet("Friends")) { Identifier = "NewFriends" }); + lst1.Add(new KeySegment(keys, null, null)); + + var keys2 = new[] { new KeyValuePair("Id", 2) }; + var lst2 = new List(); + lst2.Add(new EntitySetSegment(model.EntityContainer.FindEntitySet("Friends")) { Identifier = "NewFriends" }); + lst2.Add(new KeySegment(keys2, null, null)); + + edmChangedObj1.ODataPath = new ODataPath(lst1); + edmChangedObj2.ODataPath = new ODataPath(lst2); + + deltaSet.Add(edmChangedObj1); + deltaSet.Add(edmChangedObj2); + + friends = new List(); + friends.Add(new Friend { Id = 1, Name = "Test1" }); + friends.Add(new Friend { Id = 2, Name = "Test2" }); + + //Act + deltaSet.Patch(new FriendPatchHandler(), new APIHandlerFactory(model)); + + //Assert + Assert.Single(friends); + Assert.Equal("Friend1", friends[0].Name); + } + + [Fact] + public void DeltaSet_Patch_WithInstanceAnnotations() + { + //Arrange + + var deltaSet = new DeltaSet((new List() { "Id" })); + ODataConventionModelBuilder builder = ODataConventionModelBuilderFactory.Create(); + var friendsSet = builder.EntitySet("Friends"); + var model = builder.GetEdmModel(); + + var edmChangedObj1 = new Delta(); + edmChangedObj1.TrySetPropertyValue("Id", 1); + edmChangedObj1.TrySetPropertyValue("Name", "Friend1"); + + var annotation = new ODataInstanceAnnotationContainer(); + annotation.AddResourceAnnotation("NS.Test1", 1); + edmChangedObj1.TrySetPropertyValue("InstanceAnnotations", annotation); + + var edmChangedObj2 = new DeltaDeletedEntityObject(); + edmChangedObj2.TrySetPropertyValue("Id", 2); + + edmChangedObj2.TransientInstanceAnnotationContainer = new ODataInstanceAnnotationContainer(); + edmChangedObj2.TransientInstanceAnnotationContainer.AddResourceAnnotation("Core.ContentID", 3); + + var keys = new[] { new KeyValuePair("Id", 1) }; + var lst1 = new List(); + lst1.Add(new EntitySetSegment(model.EntityContainer.FindEntitySet("Friends")) { Identifier = "NewFriends" }); + lst1.Add(new KeySegment(keys, null, null)); + + var keys2 = new[] { new KeyValuePair("Id", 2) }; + var lst2 = new List(); + lst2.Add(new EntitySetSegment(model.EntityContainer.FindEntitySet("Friends")) { Identifier = "NewFriends" }); + lst2.Add(new KeySegment(keys2, null, null)); + + edmChangedObj1.ODataPath = new ODataPath(lst1); + edmChangedObj2.ODataPath = new ODataPath(lst2); + + deltaSet.Add(edmChangedObj1); + deltaSet.Add(edmChangedObj2); + + friends = new List(); + friends.Add(new Friend { Id = 1, Name = "Test1" }); + friends.Add(new Friend { Id = 2, Name = "Test2" }); + + //Act + var coll = deltaSet.Patch(new FriendPatchHandler(), new APIHandlerFactory(model)).ToArray(); + + //Assert + Assert.Single(friends); + Assert.Equal("Friend1", friends[0].Name); + var changedObj = coll[0] as Delta; + Assert.NotNull(changedObj); + + object obj; + changedObj.TryGetPropertyValue("InstanceAnnotations",out obj); + var annotations = (obj as IODataInstanceAnnotationContainer).GetResourceAnnotations(); + Assert.Equal("NS.Test1", annotations.First().Key); + Assert.Equal(1, annotations.First().Value); + + DeltaDeletedEntityObject changedObj1 = coll[1] as DeltaDeletedEntityObject; + Assert.NotNull(changedObj1); + + annotations = changedObj1.TransientInstanceAnnotationContainer.GetResourceAnnotations(); + Assert.Equal("Core.ContentID", annotations.First().Key); + Assert.Equal(3, annotations.First().Value); + } + + [Fact] + public void DeltaSet_Patch_WithNestedDelta() + { + //Arrange + ODataConventionModelBuilder builder = ODataConventionModelBuilderFactory.Create(); + var friendsSet = builder.EntitySet("Friends"); + var model = builder.GetEdmModel(); + + var lstId = new List(); + lstId.Add("Id"); + + var deltaSet = new DeltaSet(lstId); + + var deltaSet1 = new DeltaSet(lstId); + + var keys = new[] { new KeyValuePair("Id", 1) }; + var lst1 = new List(); + lst1.Add(new EntitySetSegment(model.EntityContainer.FindEntitySet("Friends")) { Identifier = "NewFriends" }); + lst1.Add(new KeySegment(keys, null, null)); + + var keys2 = new[] { new KeyValuePair("Id", 2) }; + var lst2 = new List(); + lst2.Add(new EntitySetSegment(model.EntityContainer.FindEntitySet("Friends")) { Identifier = "NewFriends" }); + lst2.Add(new KeySegment(keys2, null, null)); + + var edmNewObj1 = new Delta(); + edmNewObj1.TrySetPropertyValue("Id", 1); + edmNewObj1.TrySetPropertyValue("Name", "NewFriend1"); + + var edmNewObj2 = new Delta(); + edmNewObj2.TrySetPropertyValue("Id", 2); + edmNewObj2.TrySetPropertyValue("Name", "NewFriend2"); + + edmNewObj1.ODataPath = new ODataPath(lst1); + edmNewObj2.ODataPath = new ODataPath(lst2); + + deltaSet1.Add(edmNewObj1); + deltaSet1.Add(edmNewObj2); + + var deltaSet2 = new DeltaSet(lstId); + + var edmNewObj21 = new Delta(); + edmNewObj21.TrySetPropertyValue("Id", 3); + edmNewObj21.TrySetPropertyValue("Name", "NewFriend3"); + + var edmNewObj22 = new Delta(); + edmNewObj22.TrySetPropertyValue("Id", 4); + edmNewObj22.TrySetPropertyValue("Name", "NewFriend4"); + + var keys3 = new[] { new KeyValuePair("Id", 3) }; + var lst3 = new List(); + lst3.Add(new EntitySetSegment(model.EntityContainer.FindEntitySet("Friends")) { Identifier = "NewFriends" }); + lst3.Add(new KeySegment(keys3, null, null)); + + var keys4 = new[] { new KeyValuePair("Id", 4) }; + var lst4 = new List(); + lst4.Add(new EntitySetSegment(model.EntityContainer.FindEntitySet("Friends")) { Identifier = "NewFriends" }); + lst4.Add(new KeySegment(keys4, null, null)); + + edmNewObj21.ODataPath = new ODataPath(lst3); + edmNewObj22.ODataPath = new ODataPath(lst4); + + var edmChangedObj1 = new Delta(); + edmChangedObj1.TrySetPropertyValue("Id", 1); + edmChangedObj1.TrySetPropertyValue("Name", "Friend1"); + edmChangedObj1.TrySetPropertyValue("NewFriends", deltaSet1); + + var edmChangedObj2 = new Delta(); + edmChangedObj2.TrySetPropertyValue("Id", 2); + edmChangedObj2.TrySetPropertyValue("Name", "Friend2"); + edmChangedObj2.TrySetPropertyValue("NewFriends", deltaSet2); + + deltaSet2.Add(edmNewObj21); + deltaSet2.Add(edmNewObj22); + + var keys1 = new[] { new KeyValuePair("Id", 2) }; + var lst = new List(); + lst.Add(new EntitySetSegment(model.EntityContainer.FindEntitySet("Friends")) { Identifier = "Friends" }); + lst.Add(new KeySegment(keys1, null, null)); + + edmChangedObj1.ODataPath = new ODataPath(lst1); + edmChangedObj2.ODataPath = new ODataPath(lst2); + + deltaSet.Add(edmChangedObj1); + deltaSet.Add(edmChangedObj2); + + friends = new List(); + friends.Add(new Friend { Id = 1, Name = "Test1" }); + friends.Add(new Friend { Id = 2, Name = "Test2", NewFriends= new List() { new NewFriend {Id=3, Name="Test33" }, new NewFriend { Id = 4, Name = "Test44" } } }); + + //Act + deltaSet.Patch(new FriendPatchHandler(), new APIHandlerFactory(model)); + + //Assert + Assert.Equal(2, friends.Count); + Assert.Equal("Friend1", friends[0].Name); + Assert.Equal("Friend2", friends[1].Name); + + Assert.Equal(2, friends[0].NewFriends.Count); + Assert.Equal(2, friends[1].NewFriends.Count); + + Assert.Equal("NewFriend1", friends[0].NewFriends[0].Name); + Assert.Equal("NewFriend2", friends[0].NewFriends[1].Name); + Assert.Equal("NewFriend3", friends[1].NewFriends[0].Name); + Assert.Equal("NewFriend4", friends[1].NewFriends[1].Name); + } + } + + internal class APIHandlerFactory : ODataAPIHandlerFactory + { + public APIHandlerFactory(IEdmModel model): base(model) + { + + } + + public override IODataAPIHandler GetHandler(ODataPath path) + { + if (path != null) + { + switch (path.LastSegment.Identifier) + { + case "Friend": + return new FriendPatchHandler(); + + default: + return null; + } + } + + return null; + } + } + internal class FriendPatchHandler : ODataAPIHandler + { + public override IODataAPIHandler GetNestedHandler(Friend parent, string navigationPropertyName) + { + return new NewFriendPatchHandler(parent); + } + + public override ODataAPIResponseStatus TryCreate(IDictionary keyValues, out Friend createdObject, out string errorMessage) + { + createdObject = new Friend(); + DeltaSetOfTTest.friends.Add(createdObject); + errorMessage = string.Empty; + return ODataAPIResponseStatus.Success; + } + + public override ODataAPIResponseStatus TryDelete(IDictionary keyValues, out string errorMessage) + { + int id = Int32.Parse( keyValues.First().Value.ToString()); + + DeltaSetOfTTest.friends.Remove(DeltaSetOfTTest.friends.First(x => x.Id == id)); + errorMessage = string.Empty; + + return ODataAPIResponseStatus.Success; + } + + public override ODataAPIResponseStatus TryGet(IDictionary keyValues, out Friend originalObject, out string errorMessage) + { + int id = Int32.Parse(keyValues.First().Value.ToString()); + originalObject = DeltaSetOfTTest.friends.First(x => x.Id == id); + errorMessage = string.Empty; + + return ODataAPIResponseStatus.Success; + } + + public override ODataAPIResponseStatus TryAddRelatedObject(Friend resource, out string errorMessage) + { + throw new NotImplementedException(); + } + } + + public class NewFriendPatchHandler : ODataAPIHandler + { + Friend parent; + public NewFriendPatchHandler(Friend parent) + { + this.parent = parent; + } + + public override IODataAPIHandler GetNestedHandler(NewFriend parent, string navigationPropertyName) + { + throw new NotImplementedException(); + } + + public override ODataAPIResponseStatus TryCreate(IDictionary keyValues, out NewFriend createdObject, out string errorMessage) + { + createdObject = new NewFriend(); + if(parent.NewFriends == null) + { + parent.NewFriends = new List(); + } + + parent.NewFriends.Add(createdObject); + errorMessage = string.Empty; + return ODataAPIResponseStatus.Success; + } + + public override ODataAPIResponseStatus TryDelete(IDictionary keyValues, out string errorMessage) + { + int id = Int32.Parse(keyValues.First().Value.ToString()); + + parent.NewFriends.Remove(parent.NewFriends.First(x => x.Id == id)); + errorMessage = string.Empty; + + return ODataAPIResponseStatus.Success; + } + + public override ODataAPIResponseStatus TryGet(IDictionary keyValues, out NewFriend originalObject, out string errorMessage) + { + errorMessage = string.Empty; + originalObject = null; + + if(parent.NewFriends == null) + { + return ODataAPIResponseStatus.NotFound; + } + + int id = Int32.Parse(keyValues.First().Value.ToString()); + originalObject = parent.NewFriends.FirstOrDefault(x => x.Id == id); + errorMessage = string.Empty; + + return originalObject!=null? ODataAPIResponseStatus.Success : ODataAPIResponseStatus.NotFound; + } + + public override ODataAPIResponseStatus TryAddRelatedObject(NewFriend resource, out string errorMessage) + { + throw new NotImplementedException(); + } + } +} diff --git a/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/EdmChangedObjectCollectionTest.cs b/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/EdmChangedObjectCollectionTest.cs index 511934dfcb..01a94b2645 100644 --- a/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/EdmChangedObjectCollectionTest.cs +++ b/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/EdmChangedObjectCollectionTest.cs @@ -5,13 +5,61 @@ // //------------------------------------------------------------------------------ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using Microsoft.AspNet.OData.Builder; +using Microsoft.AspNet.OData.Test.Abstraction; using Microsoft.AspNet.OData.Test.Common; using Microsoft.OData.Edm; +using Microsoft.OData.UriParser; using Moq; using Xunit; namespace Microsoft.AspNet.OData.Test { + internal class TypelessAPIHandlerFactory : EdmODataAPIHandlerFactory + { + IEdmEntityType entityType; + IEdmStructuredObject employee; + + protected TypelessAPIHandlerFactory(IEdmModel model): base(model) + { + + } + + public TypelessAPIHandlerFactory(IEdmEntityType entityType, IEdmModel model): base(model) + { + this.entityType = entityType; + } + + public TypelessAPIHandlerFactory(IEdmEntityType entityType, IEdmStructuredObject employee, IEdmModel model) : base(model) + { + this.entityType = entityType; + this.employee = employee; + } + + public override EdmODataAPIHandler GetHandler(ODataPath path) + { + if (path != null) + { + switch (path.LastSegment.Identifier) + { + case "UnTypedFriend": + case "Friend": + return new FriendTypelessPatchHandler(entityType); + + default: + return null; + } + } + + return null; + } + + } + public class EdmChangedObjectCollectionTest { [Fact] @@ -36,5 +84,478 @@ public void GetEdmType_Returns_EdmTypeInitializedByCtor() Assert.Same(_entityType, collectionTypeReference.ElementType().Definition); } - } + + public static List friends = new List(); + + internal void InitFriends() + { + friends = new List(); + EdmEntityType _entityType = new EdmEntityType("Microsoft.AspNet.OData.Test", "Friend"); + _entityType.AddKeys(_entityType.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32)); + _entityType.AddStructuralProperty("Name", EdmPrimitiveTypeKind.String); + + EdmEntityType _entityType1 = new EdmEntityType("Microsoft.AspNet.OData.Test", "NewFriend"); + _entityType1.AddKeys(_entityType.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32)); + _entityType1.AddStructuralProperty("Name", EdmPrimitiveTypeKind.String); + + var friend1 = new EdmEntityObject(_entityType); + friend1.TrySetPropertyValue("Id", 1); + friend1.TrySetPropertyValue("Name", "Test1"); + + var friend2 = new EdmEntityObject(_entityType); + friend2.TrySetPropertyValue("Id", 2); + friend2.TrySetPropertyValue("Name", "Test2"); + + var nfriend1 = new EdmEntityObject(_entityType1); + nfriend1.TrySetPropertyValue("Id", 1); + nfriend1.TrySetPropertyValue("Name", "Test1"); + + var nfriend2 = new EdmEntityObject(_entityType1); + nfriend2.TrySetPropertyValue("Id", 2); + nfriend2.TrySetPropertyValue("Name", "Test2"); + + var nfriends = new List(); + nfriends.Add(nfriend1); + nfriends.Add(nfriend2); + + friend1.TrySetPropertyValue("NewFriends", nfriends); + + friends.Add(friend1); + friends.Add(friend2); + } + + + [Fact] + public void EdmChangedObjectCollection_Patch() + { + //Assign + InitFriends(); + EdmEntityType _entityType = new EdmEntityType("Microsoft.AspNet.OData.Test", "Friend"); + _entityType.AddKeys(_entityType.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32)); + _entityType.AddStructuralProperty("Name", EdmPrimitiveTypeKind.String); + + var lstId = new List(); + lstId.Add("Id"); + var deltaSet = new EdmChangedObjectCollection(_entityType); + + var edmChangedObj1 = new EdmDeltaEntityObject(_entityType); + edmChangedObj1.TrySetPropertyValue("Id", 1); + edmChangedObj1.TrySetPropertyValue("Name", "Friend1"); + + var edmChangedObj2 = new EdmDeltaEntityObject(_entityType); + edmChangedObj2.TrySetPropertyValue("Id", 2); + edmChangedObj2.TrySetPropertyValue("Name", "Friend2"); + + ODataConventionModelBuilder builder = ODataConventionModelBuilderFactory.Create(); + var friendsSet = builder.EntitySet("Friends"); + var model = builder.GetEdmModel(); + + var keys = new[] { new KeyValuePair("Id", 1) }; + var lst1 = new List(); + lst1.Add(new EntitySetSegment(model.EntityContainer.FindEntitySet("Friends")) { Identifier = "NewFriends" }); + lst1.Add(new KeySegment(keys, null, null)); + + var keys2 = new[] { new KeyValuePair("Id", 2) }; + var lst2 = new List(); + lst2.Add(new EntitySetSegment(model.EntityContainer.FindEntitySet("Friends")) { Identifier = "NewFriends" }); + lst2.Add(new KeySegment(keys2, null, null)); + + edmChangedObj1.ODataPath = new ODataPath(lst1); + edmChangedObj2.ODataPath = new ODataPath(lst2); + + deltaSet.Add(edmChangedObj1); + deltaSet.Add(edmChangedObj2); + + //Act + deltaSet.Patch(new FriendTypelessPatchHandler(_entityType), new TypelessAPIHandlerFactory(_entityType, null)); + + //Assert + Assert.Equal(2, friends.Count); + object obj; + friends[0].TryGetPropertyValue("Name", out obj); + Assert.Equal("Friend1", obj ); + friends[1].TryGetPropertyValue("Name", out obj); + Assert.Equal("Friend2", obj); + + } + + + [Fact] + public void EdmChangedObjectCollection_Patch_WithDeletes() + { + //Assign + InitFriends(); + EdmEntityType _entityType = new EdmEntityType("Microsoft.AspNet.OData.Test", "Friend"); + _entityType.AddKeys(_entityType.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32)); + _entityType.AddStructuralProperty("Name", EdmPrimitiveTypeKind.String); + + var changedObjCollection = new EdmChangedObjectCollection(_entityType); + + var edmChangedObj1 = new EdmDeltaEntityObject(_entityType); + edmChangedObj1.TrySetPropertyValue("Id", 1); + edmChangedObj1.TrySetPropertyValue("Name", "Friend1"); + + var edmChangedObj2 = new EdmDeltaDeletedEntityObject(_entityType); + edmChangedObj2.TrySetPropertyValue("Id", 2); + edmChangedObj2.TrySetPropertyValue("Name", "Friend2"); + + ODataConventionModelBuilder builder = ODataConventionModelBuilderFactory.Create(); + var friendsSet = builder.EntitySet("Friends"); + var model = builder.GetEdmModel(); + + var keys = new[] { new KeyValuePair("Id", 1) }; + var lst1 = new List(); + lst1.Add(new EntitySetSegment(model.EntityContainer.FindEntitySet("Friends")) { Identifier = "NewFriends" }); + lst1.Add(new KeySegment(keys, null, null)); + + var keys2 = new[] { new KeyValuePair("Id", 2) }; + var lst2 = new List(); + lst2.Add(new EntitySetSegment(model.EntityContainer.FindEntitySet("Friends")) { Identifier = "NewFriends" }); + lst2.Add(new KeySegment(keys2, null, null)); + + edmChangedObj1.ODataPath = new ODataPath(lst1); + edmChangedObj2.ODataPath = new ODataPath(lst2); + + changedObjCollection.Add(edmChangedObj1); + changedObjCollection.Add(edmChangedObj2); + + //Act + changedObjCollection.Patch(new FriendTypelessPatchHandler(_entityType), new TypelessAPIHandlerFactory(_entityType, null)); + + //Assert + Assert.Single(friends); + object obj; + friends[0].TryGetPropertyValue("Name", out obj); + Assert.Equal("Friend1", obj); + + } + + [Fact] + public void EdmChangedObjectCollection_Patch_WithInstanceAnnotations() + { + //Assign + InitFriends(); + EdmEntityType _entityType = new EdmEntityType("Microsoft.AspNet.OData.Test", "Friend"); + _entityType.AddKeys(_entityType.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32)); + _entityType.AddStructuralProperty("Name", EdmPrimitiveTypeKind.String); + + + + var changedObjCollection = new EdmChangedObjectCollection(_entityType); + + var edmChangedObj1 = new EdmDeltaEntityObject(_entityType); + edmChangedObj1.TrySetPropertyValue("Id", 1); + edmChangedObj1.TrySetPropertyValue("Name", "Friend1"); + edmChangedObj1.PersistentInstanceAnnotationsContainer = new ODataInstanceAnnotationContainer(); + edmChangedObj1.PersistentInstanceAnnotationsContainer.AddResourceAnnotation("NS.Test", 1); + + var edmChangedObj2 = new EdmDeltaEntityObject(_entityType); + edmChangedObj2.TrySetPropertyValue("Id", 2); + edmChangedObj2.TrySetPropertyValue("Name", "Friend2"); + + ODataConventionModelBuilder builder = ODataConventionModelBuilderFactory.Create(); + var friendsSet = builder.EntitySet("Friends"); + var model = builder.GetEdmModel(); + + var keys = new[] { new KeyValuePair("Id", 1) }; + var lst1 = new List(); + lst1.Add(new EntitySetSegment(model.EntityContainer.FindEntitySet("Friends")) { Identifier = "NewFriends" }); + lst1.Add(new KeySegment(keys, null, null)); + + var keys2 = new[] { new KeyValuePair("Id", 2) }; + var lst2 = new List(); + lst2.Add(new EntitySetSegment(model.EntityContainer.FindEntitySet("Friends")) { Identifier = "NewFriends" }); + lst2.Add(new KeySegment(keys2, null, null)); + + edmChangedObj1.ODataPath = new ODataPath(lst1); + edmChangedObj2.ODataPath = new ODataPath(lst2); + + changedObjCollection.Add(edmChangedObj1); + changedObjCollection.Add(edmChangedObj2); + + //Act + var coll= changedObjCollection.Patch(new FriendTypelessPatchHandler(_entityType), new TypelessAPIHandlerFactory(_entityType, null)); + + //Assert + Assert.Equal(2, friends.Count); + object obj; + friends[0].TryGetPropertyValue("Name", out obj); + Assert.Equal("Friend1", obj); + + var edmObj = coll[0] as EdmDeltaEntityObject; + + Assert.Equal("NS.Test", edmObj.PersistentInstanceAnnotationsContainer.GetResourceAnnotations().First().Key); + Assert.Equal(1, edmObj.PersistentInstanceAnnotationsContainer.GetResourceAnnotations().First().Value); + + friends[1].TryGetPropertyValue("Name", out obj); + Assert.Equal("Friend2", obj); + } + + } + + public class Friend + { + [Key] + public int Id { get; set; } + public string Name { get; set; } + public List NewFriends { get; set; } + + public IODataInstanceAnnotationContainer InstanceAnnotations { get; set; } + } + + public class NewFriend + { + public int Id { get; set; } + public string Name { get; set; } + } + + internal class FriendTypelessPatchHandler : EdmODataAPIHandler + { + IEdmEntityType entityType; + + public FriendTypelessPatchHandler(IEdmEntityType entityType) + { + this.entityType = entityType; + } + + public override ODataAPIResponseStatus TryCreate(IDictionary keyValues, out IEdmStructuredObject createdObject, out string errorMessage) + { + createdObject = null; + errorMessage = string.Empty; + + try + { + createdObject = new EdmEntityObject(entityType); + + EdmChangedObjectCollectionTest.friends.Add(createdObject); + + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + + public override ODataAPIResponseStatus TryDelete(IDictionary keyValues, out string errorMessage) + { + errorMessage = string.Empty; + + try + { + var id = keyValues.First().Value.ToString(); + + foreach (var emp in EdmChangedObjectCollectionTest.friends) + { + object id1; + emp.TryGetPropertyValue("Id", out id1); + + if (id == id1.ToString()) + { + EdmChangedObjectCollectionTest.friends.Remove(emp); + + break; + } + } + + + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + + public override ODataAPIResponseStatus TryGet(IDictionary keyValues, out IEdmStructuredObject originalObject, out string errorMessage) + { + ODataAPIResponseStatus status = ODataAPIResponseStatus.Success; + errorMessage = string.Empty; + originalObject = null; + + try + { + var id = keyValues["Id"].ToString(); + + foreach (var friend in EdmChangedObjectCollectionTest.friends) + { + object id1; + friend.TryGetPropertyValue("Id", out id1); + + if (id == id1.ToString()) + { + originalObject = friend; + break; + } + } + + + if (originalObject == null) + { + status = ODataAPIResponseStatus.NotFound; + } + + } + catch (Exception ex) + { + status = ODataAPIResponseStatus.Failure; + errorMessage = ex.Message; + } + + return status; + } + + public override EdmODataAPIHandler GetNestedHandler(IEdmStructuredObject parent, string navigationPropertyName) + { + switch (navigationPropertyName) + { + case "NewFriends": + return new NewFriendTypelessPatchHandler(parent, entityType.DeclaredNavigationProperties().First().Type.Definition.AsElementType() as IEdmEntityType); + default: + return null; + } + } + + public override ODataAPIResponseStatus TryAddRelatedObject(IEdmStructuredObject resource, out string errorMessage) + { + throw new NotImplementedException(); + } + } + + internal class NewFriendTypelessPatchHandler : EdmODataAPIHandler + { + IEdmEntityType entityType; + EdmStructuredObject friend; + + public NewFriendTypelessPatchHandler(IEdmStructuredObject friend, IEdmEntityType entityType) + { + this.entityType = entityType; + this.friend = friend as EdmStructuredObject; + } + + public override ODataAPIResponseStatus TryCreate(IDictionary keyValues, out IEdmStructuredObject createdObject, out string errorMessage) + { + createdObject = null; + errorMessage = string.Empty; + + try + { + createdObject = new EdmEntityObject(entityType); + + object obj; + friend.TryGetPropertyValue("NewFriends", out obj); + + var nfriends = obj as List; + + nfriends.Add(createdObject); + + friend.TrySetPropertyValue("NewFriends", nfriends); + + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + + public override ODataAPIResponseStatus TryDelete(IDictionary keyValues, out string errorMessage) + { + errorMessage = string.Empty; + + try + { + object obj; + friend.TryGetPropertyValue("NewFriends", out obj); + + var nfriends = obj as List; + + var id = keyValues.First().Value.ToString(); + + foreach (var frnd in nfriends) + { + object id1; + frnd.TryGetPropertyValue("Id", out id1); + + if (id == id1.ToString()) + { + nfriends.Remove(frnd); + + break; + } + } + + + return ODataAPIResponseStatus.Success; + } + catch (Exception ex) + { + errorMessage = ex.Message; + + return ODataAPIResponseStatus.Failure; + } + } + + public override ODataAPIResponseStatus TryGet(IDictionary keyValues, out IEdmStructuredObject originalObject, out string errorMessage) + { + ODataAPIResponseStatus status = ODataAPIResponseStatus.Success; + errorMessage = string.Empty; + originalObject = null; + + try + { + object obj; + friend.TryGetPropertyValue("NewFriends", out obj); + + var nfriends = obj as List; + + var id = keyValues.First().Value.ToString(); + + foreach (var frnd in nfriends) + { + object id1; + frnd.TryGetPropertyValue("Id", out id1); + + if (id == id1.ToString()) + { + originalObject = frnd; + + break; + } + } + + + + if (originalObject == null) + { + status = ODataAPIResponseStatus.NotFound; + } + + } + catch (Exception ex) + { + status = ODataAPIResponseStatus.Failure; + errorMessage = ex.Message; + } + + return status; + } + + public override EdmODataAPIHandler GetNestedHandler(IEdmStructuredObject parent, string navigationPropertyName) + { + return null; + } + + public override ODataAPIResponseStatus TryAddRelatedObject(IEdmStructuredObject resource, out string errorMessage) + { + throw new NotImplementedException(); + } + } + } diff --git a/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/Formatter/Deserialization/ODataResourceDeserializerTests.cs b/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/Formatter/Deserialization/ODataResourceDeserializerTests.cs index 32ab20c543..7b680f3dd6 100644 --- a/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/Formatter/Deserialization/ODataResourceDeserializerTests.cs +++ b/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/Formatter/Deserialization/ODataResourceDeserializerTests.cs @@ -421,7 +421,6 @@ public void ReadResource_CanReadDynamicPropertiesForOpenEntityType() TypeName = typeof(SimpleOpenCustomer).FullName }; - IEdmEntityType entityType1 = customerTypeReference.EntityDefinition(); EdmEntityContainer container = new EdmEntityContainer("NS", "Container"); IEdmNavigationSource navigationSource = new EdmEntitySet(container, "EntitySet", entityType1); @@ -443,11 +442,13 @@ public void ReadResource_CanReadDynamicPropertiesForOpenEntityType() IsCollection = true, Name = "CollectionProperty" }; + ODataNestedResourceInfoWrapper resourceInfoWrapper = new ODataNestedResourceInfoWrapper(resourceInfo); ODataResourceSetWrapper resourceSetWrapper = new ODataResourceSetWrapper(new ODataResourceSet { TypeName = String.Format("Collection({0})", typeof(SimpleOpenAddress).FullName) }); + foreach (var complexResource in complexResources) { resourceSetWrapper.Resources.Add(new ODataResourceWrapper(complexResource)); @@ -557,7 +558,6 @@ public void ReadResource_CanReadDynamicPropertiesForOpenEntityTypeAndAnnotations InstanceAnnotations = instAnn }; - IEdmEntityType entityType1 = customerTypeReference.EntityDefinition(); EdmEntityContainer container = new EdmEntityContainer("NS", "Container"); IEdmNavigationSource navigationSource = new EdmEntitySet(container, "EntitySet", entityType1); @@ -579,11 +579,13 @@ public void ReadResource_CanReadDynamicPropertiesForOpenEntityTypeAndAnnotations IsCollection = true, Name = "CollectionProperty" }; + ODataNestedResourceInfoWrapper resourceInfoWrapper = new ODataNestedResourceInfoWrapper(resourceInfo); ODataResourceSetWrapper resourceSetWrapper = new ODataResourceSetWrapper(new ODataResourceSet { TypeName = String.Format("Collection({0})", typeof(SimpleOpenAddress).FullName) }); + foreach (var complexResource in complexResources) { resourceSetWrapper.Resources.Add(new ODataResourceWrapper(complexResource)); @@ -633,7 +635,6 @@ public void ReadResource_CanReadDynamicPropertiesForOpenEntityTypeAndAnnotations IEdmEntityTypeReference customerTypeReference = model.GetEdmTypeReference(typeof(SimpleOpenCustomer)).AsEntity(); - var deserializer = new ODataResourceDeserializer(_deserializerProvider); ODataEnumValue enumValue = new ODataEnumValue("Third", typeof(SimpleEnum).FullName); @@ -739,11 +740,13 @@ public void ReadResource_CanReadDynamicPropertiesForOpenEntityTypeAndAnnotations IsCollection = true, Name = "CollectionProperty" }; + ODataNestedResourceInfoWrapper resourceInfoWrapper = new ODataNestedResourceInfoWrapper(resourceInfo); ODataResourceSetWrapper resourceSetWrapper = new ODataResourceSetWrapper(new ODataResourceSet { TypeName = String.Format("Collection({0})", typeof(SimpleOpenAddress).FullName) }); + foreach (var complexResource in complexResources) { resourceSetWrapper.Resources.Add(new ODataResourceWrapper(complexResource)); @@ -1006,6 +1009,7 @@ public void ReadResource_CanReadInstanceAnnotationforOpenType() Assert.Equal(1, customer.InstanceAnnotations.GetPropertyAnnotations("GuidProperty").Count); Assert.Equal(1, customer.InstanceAnnotations.GetPropertyAnnotations("CustomerId").Count); } + [Fact] public void CreateResourceInstance_ThrowsArgumentNull_ReadContext() { @@ -1068,6 +1072,7 @@ public void CreateResourceInstance_CreatesDeltaWith_ExpectedUpdatableProperties( Model = _readContext.Model, ResourceType = typeof(Delta) }; + var structuralProperties = _productEdmType.StructuralProperties().Select(p => p.Name).Union(_productEdmType.NavigationProperties().Select(p => p.Name)); // Act @@ -1319,7 +1324,7 @@ public void ApplyNestedProperty_UsesThePropertyAlias_ForResourceWrapper_WithWron Model = model.Model, Path = new ODataPath(new ODataPathSegment[1] { new KeySegment(keys, entityType1, navigationSource ) - }), + }) }; // Act @@ -1342,7 +1347,6 @@ public void ApplyNestedProperties_Preserves_ReadContextRequest() builder.AddService(ServiceLifetime.Singleton, prov => ((Mock)prov.GetService(typeof(Mock))).Object); }); - var originalContext = new ODataDeserializerContext { Model = _edmModel, @@ -1437,7 +1441,6 @@ public void ApplyStructuralProperty_ThrowsArgumentNull_StructuralProperty() "structuralProperty"); } - [Fact] public void ApplyStructuralPropertiesAndInstanceAnnotations_Calls_ApplyStructuralPropertyOnEachPropertyInResource() { @@ -1628,7 +1631,7 @@ public void ApplyIdToPath_CreatesODataPathWithNullKeySegment_IfKeyValueNotSet() ODataPath path = ODataResourceDeserializerHelpers.ApplyIdToPath(currentContext, resourceWrapper); string value = path.ToString(); - Assert.Equal("Products('Null')", value); + Assert.Equal("Products('')", value); } private static ODataMessageReader GetODataMessageReader(IODataRequestMessage oDataRequestMessage, IEdmModel edmModel) diff --git a/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/Microsoft.AspNet.OData.Test.Shared.projitems b/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/Microsoft.AspNet.OData.Test.Shared.projitems index 240d273bae..8759d27633 100644 --- a/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/Microsoft.AspNet.OData.Test.Shared.projitems +++ b/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/Microsoft.AspNet.OData.Test.Shared.projitems @@ -229,6 +229,9 @@ + + + @@ -335,6 +338,7 @@ + diff --git a/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/ODataPathExtensionsTest.cs b/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/ODataPathExtensionsTest.cs new file mode 100644 index 0000000000..09b722b044 --- /dev/null +++ b/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/ODataPathExtensionsTest.cs @@ -0,0 +1,208 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + + +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNet.OData.Extensions; +using Microsoft.AspNet.OData.Test.Common.Models; +using Microsoft.OData.UriParser; +using Xunit; + +namespace Microsoft.AspNet.OData.Test +{ + public class ODataPathExtensionsTest + { + SampleEdmModel model = new SampleEdmModel(); + KeyValuePair[] customerKey = new[] { new KeyValuePair("Id", "1") }; + KeyValuePair[] friendsKey = new[] { new KeyValuePair("Id", "1001") }; + + [Fact] + public void GetKeys_PathWithTypeSegmentReturnsKeysFromLastKeySegment() + { + // From this path Customers(1) + // GetKeys() should return { "Id": "1" } + + // Arrange + ODataPath path = new ODataPath(new ODataPathSegment[] + { + new EntitySetSegment(model.customerSet), + new KeySegment(customerKey, model.customerType, model.customerSet), + new TypeSegment(model.vipCustomerType, model.customerType, null) + }); + + // Act + Dictionary keys = path.GetKeys(); + + // Assert + Assert.Single(keys); + Assert.Equal("Id", keys.First().Key); + Assert.Equal("1", keys.First().Value); + } + + [Fact] + public void GetKeys_PathWithNoSegmentReturnsEmptyCollection() + { + // Arrange + ODataPath path = new ODataPath(new ODataPathSegment[] + { + }); + + // Act + Dictionary keys = path.GetKeys(); + + // Assert + Assert.Empty(keys); + } + + [Fact] + public void GetKeys_PathWithNoKeySegmentReturnsEmptyCollection() + { + // Arrange + ODataPath path = new ODataPath(new ODataPathSegment[] + { + new EntitySetSegment(model.customerSet), + new TypeSegment(model.vipCustomerType, model.customerType, null) + }); + + // Act + Dictionary keys = path.GetKeys(); + + // Assert + Assert.Empty(keys); + } + + [Fact] + public void GetKeys_PathWithNavPropReturnsKeysFromLastKeySegment() + { + // From this path Customers(1)/Friends(1001) + // GetKeys() should return { "Id": "1001" } + + // Arrange + ODataPath path = new ODataPath(new ODataPathSegment[] + { + new EntitySetSegment(model.customerSet), + new KeySegment(customerKey, model.customerType, model.customerSet), + new NavigationPropertySegment(model.friendsProperty, model.customerSet), + new KeySegment(friendsKey, model.personType, null) + }); + + // Act + Dictionary keys = path.GetKeys(); + + // Assert + Assert.Single(keys); + Assert.Equal("Id", keys.First().Key); + Assert.Equal("1001", keys.First().Value); + } + + [Fact] + public void GetLastNonTypeNonKeySegment_TypeSegmentAsLastSegmentReturnsCorrectSegment() + { + // If the path is Customers(1)/Friends(1001)/Ns.UniquePerson where Ns.UniquePerson is a type segment + // and 1001 is a KeySegment, + // GetLastNonTypeNonKeySegment() should return Friends NavigationPropertySegment. + + // Arrange + ODataPath path = new ODataPath(new ODataPathSegment[] + { + new EntitySetSegment(model.customerSet), + new KeySegment(customerKey, model.customerType, model.customerSet), + new NavigationPropertySegment(model.friendsProperty, model.customerSet), + new KeySegment(friendsKey, model.personType, null), + new TypeSegment(model.uniquePersonType, model.personType, null) + }); + + // Act + ODataPathSegment segment = path.GetLastNonTypeNonKeySegment(); + + // Assert + Assert.Equal("Friends", segment.Identifier); + Assert.True(segment is NavigationPropertySegment); + } + + [Fact] + public void GetLastNonTypeNonKeySegment_KeySegmentAsLastSegmentReturnsCorrectSegment() + { + // If the path is Customers(1)/Friends(1001) where1001 is a KeySegment, + // GetLastNonTypeNonKeySegment() should return Friends NavigationPropertySegment. + + // Arrange + ODataPath path = new ODataPath(new ODataPathSegment[] + { + new EntitySetSegment(model.customerSet), + new KeySegment(customerKey, model.customerType, model.customerSet), + new NavigationPropertySegment(model.friendsProperty, model.customerSet), + new KeySegment(friendsKey, model.personType, null) + }); + + // Act + ODataPathSegment segment = path.GetLastNonTypeNonKeySegment(); + + // Assert + Assert.Equal("Friends", segment.Identifier); + Assert.True(segment is NavigationPropertySegment); + } + + [Fact] + public void GetLastNonTypeNonKeySegment_SingleSegmentPathReturnsCorrectSegment() + { + // If the path is /Customers, + // GetLastNonTypeNonKeySegment() should return Customers EntitySetSegment. + + // Arrange + ODataPath path = new ODataPath(new ODataPathSegment[] + { + new EntitySetSegment(model.customerSet) + }); + + // Act + ODataPathSegment segment = path.GetLastNonTypeNonKeySegment(); + + // Assert + Assert.True(segment is EntitySetSegment); + } + + [Fact] + public void GetLastNonTypeNonKeySegment_SingleKeySegmentPathReturnsNull() + { + // If the path is /1, + // GetLastNonTypeNonKeySegment() should return null since this is a KeySegment. + + // Arrange + ODataPath path = new ODataPath(new ODataPathSegment[] + { + new KeySegment(customerKey, model.customerType, model.customerSet) + }); + + // Act + ODataPathSegment segment = path.GetLastNonTypeNonKeySegment(); + + // Assert + Assert.Null(segment); + } + + [Fact] + public void GetLastNonTypeNonKeySegment_SingleTypeSegmentPathReturnsNull() + { + // If the path is /Ns.UniquePerson, + // GetLastNonTypeNonKeySegment() should return null since this is a TypeSegment. + + // Arrange + ODataPath path = new ODataPath(new ODataPathSegment[] + { + new TypeSegment(model.uniquePersonType, model.personType, null) + }); + + // Act + ODataPathSegment segment = path.GetLastNonTypeNonKeySegment(); + + // Assert + Assert.Null(segment); + } + } +} diff --git a/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/ODataPathHelperTest.cs b/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/ODataPathHelperTest.cs new file mode 100644 index 0000000000..d54e622cb4 --- /dev/null +++ b/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/ODataPathHelperTest.cs @@ -0,0 +1,118 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNet.OData.Common; +using Microsoft.AspNet.OData.Test.Common; +using Microsoft.AspNet.OData.Test.Common.Models; +using Microsoft.OData.UriParser; +using Xunit; + +namespace Microsoft.AspNet.OData.Test +{ + public class ODataPathHelperTest + { + SampleEdmModel model = new SampleEdmModel(); + KeyValuePair[] customerKey = new[] { new KeyValuePair("Id", "1"), new KeyValuePair("AlternateId", "2") }; + KeyValuePair[] friendsKey = new[] { new KeyValuePair("Id", "1001") }; + + [Fact] + public void GetKeysFromKeySegment_ReturnsCorrectKeysDictionary() + { + // Arrange + KeySegment keySegment = new KeySegment(customerKey, model.customerType, model.customerSet); + + // Act + Dictionary keys = ODataPathHelper.KeySegmentAsDictionary(keySegment); + + // Assert + Assert.Equal(2, keys.Count); + Assert.Equal("Id", keys.First().Key); + Assert.Equal("1", keys.First().Value); + Assert.Equal("AlternateId", keys.Last().Key); + Assert.Equal("2", keys.Last().Value); + } + + [Fact] + public void GetKeysFromKeySegment_ThrowsExceptionForNullKeySegment() + { + KeySegment keySegment = null; + + ExceptionAssert.ThrowsArgumentNull( + () => ODataPathHelper.KeySegmentAsDictionary(keySegment), + nameof(keySegment)); + } + + [Fact] + public void GetNextKeySegmentPosition_ReturnsCorrectPosition() + { + // If the path is Customers(1)/Friends(1001)/Ns.UniqueFriend where Ns.UniqueFriend is a type segment + // and 1001 is a KeySegment, and the starting position is index 1, the next keysegment position is index 3. + + // Arrange + ODataPath path = new ODataPath(new ODataPathSegment[] + { + new EntitySetSegment(model.customerSet), + new KeySegment(customerKey, model.customerType, model.customerSet), + new NavigationPropertySegment(model.friendsProperty, model.customerSet), + new KeySegment(friendsKey, model.personType, null), + new TypeSegment(model.uniquePersonType, model.personType, null) + }); + + // Act + int position = ODataPathHelper.GetNextKeySegmentPosition(path.AsList(), 1); + + // Assert + Assert.Equal(3, position); + } + + [Fact] + public void GetNextKeySegmentPosition_ReturnsNegativeOneIfNoKeySegmentIsFound() + { + // If the path is Customers(1)/Friends(1001)/Ns.UniqueFriend where Ns.UniqueFriend is a type segment + // and 1001 is a KeySegment, and the starting position is index 1, the next keysegment position is index 3. + + // Arrange + ODataPath path = new ODataPath(new ODataPathSegment[] + { + new EntitySetSegment(model.customerSet), + new KeySegment(customerKey, model.customerType, model.customerSet), + new NavigationPropertySegment(model.friendsProperty, model.customerSet), + new TypeSegment(model.uniquePersonType, model.personType, null) + }); + + // Act + int position = ODataPathHelper.GetNextKeySegmentPosition(path.AsList(), 1); + + // Assert + Assert.Equal(-1, position); + } + + [Fact] + public void GetNextKeySegmentPosition_ReturnsNegativeOneIfInvalidPositionIsPassed() + { + // If the path is Customers(1)/Friends(1001)/Ns.UniqueFriend where Ns.UniqueFriend is a type segment + // and 1001 is a KeySegment, and the starting position is index 1, the next keysegment position is index 3. + + // Arrange + ODataPath path = new ODataPath(new ODataPathSegment[] + { + new EntitySetSegment(model.customerSet), + new KeySegment(customerKey, model.customerType, model.customerSet), + new NavigationPropertySegment(model.friendsProperty, model.customerSet), + new TypeSegment(model.uniquePersonType, model.personType, null) + }); + + // Act + int position = ODataPathHelper.GetNextKeySegmentPosition(path.AsList(), 10); + + // Assert + Assert.Equal(-1, position); + } + } +} diff --git a/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/TestCommon/Models/SampleEdmModel.cs b/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/TestCommon/Models/SampleEdmModel.cs new file mode 100644 index 0000000000..89e7db509b --- /dev/null +++ b/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/TestCommon/Models/SampleEdmModel.cs @@ -0,0 +1,60 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +using Microsoft.OData.Edm; + +namespace Microsoft.AspNet.OData.Test.Common.Models +{ + public class SampleEdmModel + { + public EdmNavigationProperty friendsProperty; + public EdmEntityType customerType; + public EdmEntityType personType; + public EdmEntityType uniquePersonType; + public EdmEntityType vipCustomerType; + public IEdmEntitySet customerSet; + public EdmModel model; + + public SampleEdmModel() + { + model = new EdmModel(); + EdmEntityContainer container = new EdmEntityContainer("NS", "Container"); + + personType = new EdmEntityType("NS", "Person"); + personType.AddKeys(personType.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32, isNullable: false)); + personType.AddStructuralProperty("Name", EdmPrimitiveTypeKind.String, isNullable: false); + + customerType = new EdmEntityType("NS", "Customer"); + customerType.AddKeys(customerType.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32, isNullable: false)); + customerType.AddStructuralProperty("Name", EdmPrimitiveTypeKind.String, isNullable: false); + friendsProperty = customerType.AddUnidirectionalNavigation( + new EdmNavigationPropertyInfo + { + ContainsTarget = true, + Name = "Friends", + Target = personType, + TargetMultiplicity = EdmMultiplicity.Many + }); + + vipCustomerType = new EdmEntityType("NS", "VipCustomer", customerType); + vipCustomerType.AddKeys(vipCustomerType.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32, isNullable: false)); + vipCustomerType.AddStructuralProperty("VipName", EdmPrimitiveTypeKind.String, isNullable: false); + + uniquePersonType = new EdmEntityType("NS", "UniquePerson", personType); + uniquePersonType.AddKeys(uniquePersonType.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32, isNullable: false)); + uniquePersonType.AddStructuralProperty("UniqueName", EdmPrimitiveTypeKind.String, isNullable: false); + + model.AddElement(customerType); + model.AddElement(personType); + model.AddElement(uniquePersonType); + model.AddElement(vipCustomerType); + model.AddElement(container); + + customerSet = container.AddEntitySet("Customers", customerType); + } + } +} \ No newline at end of file diff --git a/test/UnitTest/Microsoft.AspNet.OData.Test/PublicApi/Microsoft.AspNet.OData.PublicApi.bsl b/test/UnitTest/Microsoft.AspNet.OData.Test/PublicApi/Microsoft.AspNet.OData.PublicApi.bsl index 1059ca8987..41f992b577 100644 --- a/test/UnitTest/Microsoft.AspNet.OData.Test/PublicApi/Microsoft.AspNet.OData.PublicApi.bsl +++ b/test/UnitTest/Microsoft.AspNet.OData.Test/PublicApi/Microsoft.AspNet.OData.PublicApi.bsl @@ -16,6 +16,12 @@ public enum Microsoft.AspNet.OData.EdmDeltaEntityKind : int { Unknown = 4 } +public enum Microsoft.AspNet.OData.ODataAPIResponseStatus : int { + Failure = 1 + NotFound = 2 + Success = 0 +} + public interface Microsoft.AspNet.OData.IDelta { void Clear () System.Collections.Generic.IEnumerable`1[[System.String]] GetChangedPropertyNames () @@ -31,6 +37,9 @@ public interface Microsoft.AspNet.OData.IDeltaDeletedEntityObject { System.Nullable`1[[Microsoft.OData.DeltaDeletedEntryReason]] Reason { public abstract get; public abstract set; } } +public interface Microsoft.AspNet.OData.IDeltaSet { +} + public interface Microsoft.AspNet.OData.IDeltaSetItem { EdmDeltaEntityKind DeltaKind { public abstract get; } ODataIdContainer ODataIdContainer { public abstract get; public abstract set; } @@ -76,6 +85,9 @@ public interface Microsoft.AspNet.OData.IEdmStructuredObject : IEdmObject { bool TryGetPropertyValue (string propertyName, out System.Object& value) } +public interface Microsoft.AspNet.OData.IODataAPIHandler { +} + public interface Microsoft.AspNet.OData.IPerRouteContainer { System.Func`1[[Microsoft.OData.IContainerBuilder]] BuilderFactory { public abstract get; public abstract set; } @@ -102,6 +114,16 @@ public abstract class Microsoft.AspNet.OData.Delta : System.Dynamic.DynamicObjec public abstract bool TrySetPropertyValue (string name, object value) } +public abstract class Microsoft.AspNet.OData.EdmODataAPIHandler : IODataAPIHandler { + protected EdmODataAPIHandler () + + public abstract EdmODataAPIHandler GetNestedHandler (IEdmStructuredObject parent, string navigationPropertyName) + public abstract ODataAPIResponseStatus TryAddRelatedObject (IEdmStructuredObject resource, out System.String& errorMessage) + public abstract ODataAPIResponseStatus TryCreate (System.Collections.Generic.IDictionary`2[[System.String],[System.Object]] keyValues, out IEdmStructuredObject& createdObject, out System.String& errorMessage) + public abstract ODataAPIResponseStatus TryDelete (System.Collections.Generic.IDictionary`2[[System.String],[System.Object]] keyValues, out System.String& errorMessage) + public abstract ODataAPIResponseStatus TryGet (System.Collections.Generic.IDictionary`2[[System.String],[System.Object]] keyValues, out IEdmStructuredObject& originalObject, out System.String& errorMessage) +} + [ NonValidatingParameterBindingAttribute(), ] @@ -124,6 +146,25 @@ public abstract class Microsoft.AspNet.OData.EdmStructuredObject : Delta, IDynam public virtual bool TrySetPropertyValue (string name, object value) } +public abstract class Microsoft.AspNet.OData.ODataAPIHandler`1 : IODataAPIHandler { + protected ODataAPIHandler`1 () + + public abstract IODataAPIHandler GetNestedHandler (TStructuralType parent, string navigationPropertyName) + public abstract ODataAPIResponseStatus TryAddRelatedObject (TStructuralType resource, out System.String& errorMessage) + public abstract ODataAPIResponseStatus TryCreate (System.Collections.Generic.IDictionary`2[[System.String],[System.Object]] keyValues, out TStructuralType& createdObject, out System.String& errorMessage) + public abstract ODataAPIResponseStatus TryDelete (System.Collections.Generic.IDictionary`2[[System.String],[System.Object]] keyValues, out System.String& errorMessage) + public abstract ODataAPIResponseStatus TryGet (System.Collections.Generic.IDictionary`2[[System.String],[System.Object]] keyValues, out TStructuralType& originalObject, out System.String& errorMessage) + internal virtual void UpdateLinkedObjects (TStructuralType resource, Microsoft.OData.Edm.IEdmModel model) +} + +public abstract class Microsoft.AspNet.OData.ODataAPIHandlerFactory { + protected ODataAPIHandlerFactory (Microsoft.OData.Edm.IEdmModel model) + + Microsoft.OData.Edm.IEdmModel Model { public get; } + + public abstract IODataAPIHandler GetHandler (Microsoft.OData.UriParser.ODataPath odataPath) +} + [ ODataFormattingAttribute(), ODataRoutingAttribute(), @@ -288,6 +329,8 @@ public class Microsoft.AspNet.OData.Delta`1 : TypedDelta, IDynamicMetaObjectProv public TStructuralType GetInstance () public virtual System.Collections.Generic.IEnumerable`1[[System.String]] GetUnchangedPropertyNames () public TStructuralType Patch (TStructuralType original) + public void Patch (TStructuralType original, IODataAPIHandler apiHandler) + public void Patch (TStructuralType original, IODataAPIHandler apiHandler, ODataAPIHandlerFactory apiHandlerFactory) public void Put (TStructuralType original) public bool TryGetNestedPropertyValue (string name, out System.Object& value) public virtual bool TryGetPropertyType (string name, out System.Type& type) @@ -319,6 +362,9 @@ public class Microsoft.AspNet.OData.DeltaSet`1 : System.Collections.ObjectModel. public DeltaSet`1 (System.Collections.Generic.IList`1[[System.String]] keys) protected virtual void InsertItem (int index, IDeltaSetItem item) + public DeltaSet`1 Patch (ICollection`1 originalCollection) + public DeltaSet`1 Patch (ODataAPIHandler`1 apiHandlerOfT) + public DeltaSet`1 Patch (ODataAPIHandler`1 apiHandlerOfT, ODataAPIHandlerFactory apiHandlerFactory) } [ @@ -328,6 +374,8 @@ public class Microsoft.AspNet.OData.EdmChangedObjectCollection : System.Collecti public EdmChangedObjectCollection (Microsoft.OData.Edm.IEdmEntityType entityType) public EdmChangedObjectCollection (Microsoft.OData.Edm.IEdmEntityType entityType, System.Collections.Generic.IList`1[[Microsoft.AspNet.OData.IEdmChangedObject]] changedObjectList) + Microsoft.OData.Edm.IEdmEntityType EntityType { public get; } + public virtual Microsoft.OData.Edm.IEdmTypeReference GetEdmType () } diff --git a/test/UnitTest/Microsoft.AspNetCore.OData.Test/PublicApi/Microsoft.AspNetCore.OData.PublicApi.bsl b/test/UnitTest/Microsoft.AspNetCore.OData.Test/PublicApi/Microsoft.AspNetCore.OData.PublicApi.bsl index b54a1f418b..51de45943f 100644 --- a/test/UnitTest/Microsoft.AspNetCore.OData.Test/PublicApi/Microsoft.AspNetCore.OData.PublicApi.bsl +++ b/test/UnitTest/Microsoft.AspNetCore.OData.Test/PublicApi/Microsoft.AspNetCore.OData.PublicApi.bsl @@ -16,6 +16,12 @@ public enum Microsoft.AspNet.OData.EdmDeltaEntityKind : int { Unknown = 4 } +public enum Microsoft.AspNet.OData.ODataAPIResponseStatus : int { + Failure = 1 + NotFound = 2 + Success = 0 +} + public interface Microsoft.AspNet.OData.IDelta { void Clear () System.Collections.Generic.IEnumerable`1[[System.String]] GetChangedPropertyNames () @@ -31,6 +37,9 @@ public interface Microsoft.AspNet.OData.IDeltaDeletedEntityObject { System.Nullable`1[[Microsoft.OData.DeltaDeletedEntryReason]] Reason { public abstract get; public abstract set; } } +public interface Microsoft.AspNet.OData.IDeltaSet { +} + public interface Microsoft.AspNet.OData.IDeltaSetItem { EdmDeltaEntityKind DeltaKind { public abstract get; } ODataIdContainer ODataIdContainer { public abstract get; public abstract set; } @@ -76,6 +85,9 @@ public interface Microsoft.AspNet.OData.IEdmStructuredObject : IEdmObject { bool TryGetPropertyValue (string propertyName, out System.Object& value) } +public interface Microsoft.AspNet.OData.IODataAPIHandler { +} + public interface Microsoft.AspNet.OData.IPerRouteContainer { System.Func`1[[Microsoft.OData.IContainerBuilder]] BuilderFactory { public abstract get; public abstract set; } @@ -102,6 +114,16 @@ public abstract class Microsoft.AspNet.OData.Delta : System.Dynamic.DynamicObjec public abstract bool TrySetPropertyValue (string name, object value) } +public abstract class Microsoft.AspNet.OData.EdmODataAPIHandler : IODataAPIHandler { + protected EdmODataAPIHandler () + + public abstract EdmODataAPIHandler GetNestedHandler (IEdmStructuredObject parent, string navigationPropertyName) + public abstract ODataAPIResponseStatus TryAddRelatedObject (IEdmStructuredObject resource, out System.String& errorMessage) + public abstract ODataAPIResponseStatus TryCreate (System.Collections.Generic.IDictionary`2[[System.String],[System.Object]] keyValues, out IEdmStructuredObject& createdObject, out System.String& errorMessage) + public abstract ODataAPIResponseStatus TryDelete (System.Collections.Generic.IDictionary`2[[System.String],[System.Object]] keyValues, out System.String& errorMessage) + public abstract ODataAPIResponseStatus TryGet (System.Collections.Generic.IDictionary`2[[System.String],[System.Object]] keyValues, out IEdmStructuredObject& originalObject, out System.String& errorMessage) +} + [ NonValidatingParameterBindingAttribute(), ] @@ -124,6 +146,25 @@ public abstract class Microsoft.AspNet.OData.EdmStructuredObject : Delta, IDynam public virtual bool TrySetPropertyValue (string name, object value) } +public abstract class Microsoft.AspNet.OData.ODataAPIHandler`1 : IODataAPIHandler { + protected ODataAPIHandler`1 () + + public abstract IODataAPIHandler GetNestedHandler (TStructuralType parent, string navigationPropertyName) + public abstract ODataAPIResponseStatus TryAddRelatedObject (TStructuralType resource, out System.String& errorMessage) + public abstract ODataAPIResponseStatus TryCreate (System.Collections.Generic.IDictionary`2[[System.String],[System.Object]] keyValues, out TStructuralType& createdObject, out System.String& errorMessage) + public abstract ODataAPIResponseStatus TryDelete (System.Collections.Generic.IDictionary`2[[System.String],[System.Object]] keyValues, out System.String& errorMessage) + public abstract ODataAPIResponseStatus TryGet (System.Collections.Generic.IDictionary`2[[System.String],[System.Object]] keyValues, out TStructuralType& originalObject, out System.String& errorMessage) + internal virtual void UpdateLinkedObjects (TStructuralType resource, Microsoft.OData.Edm.IEdmModel model) +} + +public abstract class Microsoft.AspNet.OData.ODataAPIHandlerFactory { + protected ODataAPIHandlerFactory (Microsoft.OData.Edm.IEdmModel model) + + Microsoft.OData.Edm.IEdmModel Model { public get; } + + public abstract IODataAPIHandler GetHandler (Microsoft.OData.UriParser.ODataPath odataPath) +} + [ ODataFormattingAttribute(), ODataRoutingAttribute(), @@ -303,6 +344,8 @@ public class Microsoft.AspNet.OData.Delta`1 : TypedDelta, IDynamicMetaObjectProv public TStructuralType GetInstance () public virtual System.Collections.Generic.IEnumerable`1[[System.String]] GetUnchangedPropertyNames () public TStructuralType Patch (TStructuralType original) + public void Patch (TStructuralType original, IODataAPIHandler apiHandler) + public void Patch (TStructuralType original, IODataAPIHandler apiHandler, ODataAPIHandlerFactory apiHandlerFactory) public void Put (TStructuralType original) public bool TryGetNestedPropertyValue (string name, out System.Object& value) public virtual bool TryGetPropertyType (string name, out System.Type& type) @@ -334,6 +377,9 @@ public class Microsoft.AspNet.OData.DeltaSet`1 : System.Collections.ObjectModel. public DeltaSet`1 (System.Collections.Generic.IList`1[[System.String]] keys) protected virtual void InsertItem (int index, IDeltaSetItem item) + public DeltaSet`1 Patch (ICollection`1 originalCollection) + public DeltaSet`1 Patch (ODataAPIHandler`1 apiHandlerOfT) + public DeltaSet`1 Patch (ODataAPIHandler`1 apiHandlerOfT, ODataAPIHandlerFactory apiHandlerFactory) } [ @@ -343,6 +389,8 @@ public class Microsoft.AspNet.OData.EdmChangedObjectCollection : System.Collecti public EdmChangedObjectCollection (Microsoft.OData.Edm.IEdmEntityType entityType) public EdmChangedObjectCollection (Microsoft.OData.Edm.IEdmEntityType entityType, System.Collections.Generic.IList`1[[Microsoft.AspNet.OData.IEdmChangedObject]] changedObjectList) + Microsoft.OData.Edm.IEdmEntityType EntityType { public get; } + public virtual Microsoft.OData.Edm.IEdmTypeReference GetEdmType () } diff --git a/test/UnitTest/Microsoft.AspNetCore.OData.Test/PublicApi/Microsoft.AspNetCore3x.OData.PublicApi.bsl b/test/UnitTest/Microsoft.AspNetCore.OData.Test/PublicApi/Microsoft.AspNetCore3x.OData.PublicApi.bsl index 7b4e5c9219..665e176151 100644 --- a/test/UnitTest/Microsoft.AspNetCore.OData.Test/PublicApi/Microsoft.AspNetCore3x.OData.PublicApi.bsl +++ b/test/UnitTest/Microsoft.AspNetCore.OData.Test/PublicApi/Microsoft.AspNetCore3x.OData.PublicApi.bsl @@ -16,6 +16,12 @@ public enum Microsoft.AspNet.OData.EdmDeltaEntityKind : int { Unknown = 4 } +public enum Microsoft.AspNet.OData.ODataAPIResponseStatus : int { + Failure = 1 + NotFound = 2 + Success = 0 +} + public interface Microsoft.AspNet.OData.IDelta { void Clear () System.Collections.Generic.IEnumerable`1[[System.String]] GetChangedPropertyNames () @@ -31,6 +37,9 @@ public interface Microsoft.AspNet.OData.IDeltaDeletedEntityObject { System.Nullable`1[[Microsoft.OData.DeltaDeletedEntryReason]] Reason { public abstract get; public abstract set; } } +public interface Microsoft.AspNet.OData.IDeltaSet { +} + public interface Microsoft.AspNet.OData.IDeltaSetItem { EdmDeltaEntityKind DeltaKind { public abstract get; } ODataIdContainer ODataIdContainer { public abstract get; public abstract set; } @@ -76,6 +85,9 @@ public interface Microsoft.AspNet.OData.IEdmStructuredObject : IEdmObject { bool TryGetPropertyValue (string propertyName, out System.Object& value) } +public interface Microsoft.AspNet.OData.IODataAPIHandler { +} + public interface Microsoft.AspNet.OData.IPerRouteContainer { System.Func`1[[Microsoft.OData.IContainerBuilder]] BuilderFactory { public abstract get; public abstract set; } @@ -102,6 +114,16 @@ public abstract class Microsoft.AspNet.OData.Delta : System.Dynamic.DynamicObjec public abstract bool TrySetPropertyValue (string name, object value) } +public abstract class Microsoft.AspNet.OData.EdmODataAPIHandler : IODataAPIHandler { + protected EdmODataAPIHandler () + + public abstract EdmODataAPIHandler GetNestedHandler (IEdmStructuredObject parent, string navigationPropertyName) + public abstract ODataAPIResponseStatus TryAddRelatedObject (IEdmStructuredObject resource, out System.String& errorMessage) + public abstract ODataAPIResponseStatus TryCreate (System.Collections.Generic.IDictionary`2[[System.String],[System.Object]] keyValues, out IEdmStructuredObject& createdObject, out System.String& errorMessage) + public abstract ODataAPIResponseStatus TryDelete (System.Collections.Generic.IDictionary`2[[System.String],[System.Object]] keyValues, out System.String& errorMessage) + public abstract ODataAPIResponseStatus TryGet (System.Collections.Generic.IDictionary`2[[System.String],[System.Object]] keyValues, out IEdmStructuredObject& originalObject, out System.String& errorMessage) +} + [ NonValidatingParameterBindingAttribute(), ] @@ -124,6 +146,25 @@ public abstract class Microsoft.AspNet.OData.EdmStructuredObject : Delta, IDynam public virtual bool TrySetPropertyValue (string name, object value) } +public abstract class Microsoft.AspNet.OData.ODataAPIHandler`1 : IODataAPIHandler { + protected ODataAPIHandler`1 () + + public abstract IODataAPIHandler GetNestedHandler (TStructuralType parent, string navigationPropertyName) + public abstract ODataAPIResponseStatus TryAddRelatedObject (TStructuralType resource, out System.String& errorMessage) + public abstract ODataAPIResponseStatus TryCreate (System.Collections.Generic.IDictionary`2[[System.String],[System.Object]] keyValues, out TStructuralType& createdObject, out System.String& errorMessage) + public abstract ODataAPIResponseStatus TryDelete (System.Collections.Generic.IDictionary`2[[System.String],[System.Object]] keyValues, out System.String& errorMessage) + public abstract ODataAPIResponseStatus TryGet (System.Collections.Generic.IDictionary`2[[System.String],[System.Object]] keyValues, out TStructuralType& originalObject, out System.String& errorMessage) + internal virtual void UpdateLinkedObjects (TStructuralType resource, Microsoft.OData.Edm.IEdmModel model) +} + +public abstract class Microsoft.AspNet.OData.ODataAPIHandlerFactory { + protected ODataAPIHandlerFactory (Microsoft.OData.Edm.IEdmModel model) + + Microsoft.OData.Edm.IEdmModel Model { public get; } + + public abstract IODataAPIHandler GetHandler (Microsoft.OData.UriParser.ODataPath odataPath) +} + [ ODataFormattingAttribute(), ODataRoutingAttribute(), @@ -307,6 +348,8 @@ public class Microsoft.AspNet.OData.Delta`1 : TypedDelta, IDynamicMetaObjectProv public TStructuralType GetInstance () public virtual System.Collections.Generic.IEnumerable`1[[System.String]] GetUnchangedPropertyNames () public TStructuralType Patch (TStructuralType original) + public void Patch (TStructuralType original, IODataAPIHandler apiHandler) + public void Patch (TStructuralType original, IODataAPIHandler apiHandler, ODataAPIHandlerFactory apiHandlerFactory) public void Put (TStructuralType original) public bool TryGetNestedPropertyValue (string name, out System.Object& value) public virtual bool TryGetPropertyType (string name, out System.Type& type) @@ -338,6 +381,9 @@ public class Microsoft.AspNet.OData.DeltaSet`1 : System.Collections.ObjectModel. public DeltaSet`1 (System.Collections.Generic.IList`1[[System.String]] keys) protected virtual void InsertItem (int index, IDeltaSetItem item) + public DeltaSet`1 Patch (ICollection`1 originalCollection) + public DeltaSet`1 Patch (ODataAPIHandler`1 apiHandlerOfT) + public DeltaSet`1 Patch (ODataAPIHandler`1 apiHandlerOfT, ODataAPIHandlerFactory apiHandlerFactory) } [ @@ -347,6 +393,8 @@ public class Microsoft.AspNet.OData.EdmChangedObjectCollection : System.Collecti public EdmChangedObjectCollection (Microsoft.OData.Edm.IEdmEntityType entityType) public EdmChangedObjectCollection (Microsoft.OData.Edm.IEdmEntityType entityType, System.Collections.Generic.IList`1[[Microsoft.AspNet.OData.IEdmChangedObject]] changedObjectList) + Microsoft.OData.Edm.IEdmEntityType EntityType { public get; } + public virtual Microsoft.OData.Edm.IEdmTypeReference GetEdmType () } diff --git a/tools/GetNugetPackageMetadata.proj b/tools/GetNugetPackageMetadata.proj index 2d9f3abfa2..afa3aa8ba7 100644 --- a/tools/GetNugetPackageMetadata.proj +++ b/tools/GetNugetPackageMetadata.proj @@ -1,5 +1,8 @@ + + $([System.DateTime]::Now.ToString("yyyyMMddHHmm")) +