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