diff --git a/samples/AspNetODataSample.Web/AspNetODataSample.Web.csproj b/samples/AspNetODataSample.Web/AspNetODataSample.Web.csproj
index d602164144..d21a9e28eb 100644
--- a/samples/AspNetODataSample.Web/AspNetODataSample.Web.csproj
+++ b/samples/AspNetODataSample.Web/AspNetODataSample.Web.csproj
@@ -98,7 +98,6 @@
..\..\sln\packages\Microsoft.Spatial.7.12.2\lib\net45\Microsoft.Spatial.dll
- True
..\..\sln\packages\Newtonsoft.Json.13.0.1\lib\net45\Newtonsoft.Json.dll
diff --git a/src/Microsoft.AspNet.OData.Shared/Common/Error.cs b/src/Microsoft.AspNet.OData.Shared/Common/Error.cs
index 31c2285e9f..8d6e2f6aa1 100644
--- a/src/Microsoft.AspNet.OData.Shared/Common/Error.cs
+++ b/src/Microsoft.AspNet.OData.Shared/Common/Error.cs
@@ -103,6 +103,7 @@ internal static ArgumentNullException PropertyNull()
/// Creates an with a default message.
///
/// The logged .
+ [SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly")]
internal static ArgumentException PropertyNullOrWhiteSpace()
{
return new ArgumentException(CommonWebApiResources.PropertyNullOrWhiteSpace, "value");
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..1e4c27b23a
--- /dev/null
+++ b/src/Microsoft.AspNet.OData.Shared/Common/ODataPathHelper.cs
@@ -0,0 +1,70 @@
+//-----------------------------------------------------------------------------
+//
+// 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++)
+ {
+ ODataPathSegment currentSegment = pathSegments[i];
+
+ if (currentSegment is KeySegment)
+ {
+ return i;
+ }
+ }
+
+ return -1;
+ }
+ }
+}
diff --git a/src/Microsoft.AspNet.OData.Shared/Common/SRResources.Designer.cs b/src/Microsoft.AspNet.OData.Shared/Common/SRResources.Designer.cs
index de6a9e0d09..5d350ccf7c 100644
--- a/src/Microsoft.AspNet.OData.Shared/Common/SRResources.Designer.cs
+++ b/src/Microsoft.AspNet.OData.Shared/Common/SRResources.Designer.cs
@@ -498,6 +498,17 @@ internal static string CantFindEdmType
}
}
+ ///
+ /// Looks up a localized string similar to Cannot use Changed Object of type '{0}' on an entity of type '{1}'..
+ ///
+ internal static string ChangedObjectTypeMismatch
+ {
+ get
+ {
+ return ResourceManager.GetString("ChangedObjectTypeMismatch", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to The given model does not contain the type '{0}'..
///
@@ -564,6 +575,17 @@ internal static string CollectionShouldHaveClearMethod
}
}
+ ///
+ /// Looks up a localized string similar to ContentID.
+ ///
+ internal static string ContentID
+ {
+ get
+ {
+ return ResourceManager.GetString("ContentID", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Type {0} already added as derived type constraint.
///
@@ -597,6 +619,17 @@ internal static string CreateODataValueNotSupported
}
}
+ ///
+ /// Looks up a localized string similar to the error DataModificationException
+ ///
+ internal static string DataModificationException
+ {
+ get
+ {
+ return ResourceManager.GetString("DataModificationException", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to The actual entity type '{0}' is not assignable to the expected type '{1}'..
///
@@ -608,6 +641,17 @@ internal static string DeltaEntityTypeNotAssignable
}
}
+ ///
+ /// Looks up a localized string similar to The actual error DeltaLinkNotSupported
+ ///
+ internal static string DeltaLinkNotSupported
+ {
+ get
+ {
+ return ResourceManager.GetString("DeltaLinkNotSupported", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Cannot find nested resource name '{0}' in parent resource type '{1}'.
///
@@ -2721,6 +2765,28 @@ internal static string RequestUriTooShortForODataPath
}
}
+ ///
+ /// Looks up a localized string similar to Cannot find the resource type '{0}' in the model..
+ ///
+ internal static string ResourcesShouldbePresent
+ {
+ get
+ {
+ return ResourceManager.GetString("ResourcesShouldbePresent", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Cannot find the resource wrapper type '{0}' in the model..
+ ///
+ internal static string ResourceSetWrapperSupported
+ {
+ get
+ {
+ return ResourceManager.GetString("ResourceSetWrapperSupported", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Cannot find the resource type '{0}' in the model..
///
diff --git a/src/Microsoft.AspNet.OData.Shared/Common/SRResources.resx b/src/Microsoft.AspNet.OData.Shared/Common/SRResources.resx
index 0d7be880c7..f94864ec2e 100644
--- a/src/Microsoft.AspNet.OData.Shared/Common/SRResources.resx
+++ b/src/Microsoft.AspNet.OData.Shared/Common/SRResources.resx
@@ -988,4 +988,22 @@
A navigation property expand path should have navigation property in the path.
+
+ ResourceSetWrapper should have ResourceWrappers in it
+
+
+ Can only add ResourceWrapper to ResourceSetWrapper
+
+
+ Cannot use Changed Object of type '{0}' on an entity of type '{1}'.
+
+
+ Core.DataModificationException
+
+
+ Core.ContentID
+
+
+ DeltaLinks are not supported
+
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.OData.Shared/DataModificationExceptionType.cs b/src/Microsoft.AspNet.OData.Shared/DataModificationExceptionType.cs
new file mode 100644
index 0000000000..9c95d769bb
--- /dev/null
+++ b/src/Microsoft.AspNet.OData.Shared/DataModificationExceptionType.cs
@@ -0,0 +1,119 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (c) .NET Foundation and Contributors. All rights reserved.
+// See License.txt in the project root for license information.
+//
+//------------------------------------------------------------------------------
+
+using System;
+using Microsoft.AspNet.OData.Builder;
+
+namespace Org.OData.Core.V1
+{
+ ///
+ /// Represents a Message Type
+ ///
+ public class MessageType
+ {
+ ///
+ /// Code of message
+ ///
+ public string Code { get; set; }
+
+ ///
+ /// Actual message
+ ///
+ public string Message { get; set; }
+
+ ///
+ /// Severity of message
+ ///
+ public string Severity { get; set; }
+
+ ///
+ /// Target of message
+ ///
+ public string Target { get; set; }
+
+ ///
+ /// Details of message
+ ///
+ public string Details { get; set; }
+ }
+
+ ///
+ /// Represents an Exception Type
+ ///
+ public abstract class ExceptionType
+ {
+ ///
+ /// Represents a MessageType
+ ///
+ public MessageType MessageType { get; set; }
+ }
+
+ ///
+ /// Represents an Exception for Data modification Operation.
+ ///
+ public class DataModificationExceptionType : ExceptionType
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public DataModificationExceptionType(DataModificationOperationKind failedOperation)
+ {
+ this.FailedOperation = failedOperation;
+ }
+
+ ///
+ /// Represents kind of type of operation
+ ///
+ public DataModificationOperationKind FailedOperation { get; }
+
+ ///
+ /// Represents response code
+ ///
+ public Int16 ResponseCode { get; set; }
+ }
+
+ ///
+ /// Enumerates the DataModificationOperation for the operation kind
+ ///
+ public enum DataModificationOperationKind
+ {
+ ///
+ /// Insert new Instance
+ ///
+ Insert,
+
+ ///
+ /// Update existing Instance
+ ///
+ Update,
+
+ ///
+ /// Insert new instance or update it if it already exists
+ ///
+ Upsert,
+
+ ///
+ /// Delete existing instance
+ ///
+ Delete,
+
+ ///
+ /// Invoke action or function
+ ///
+ Invoke,
+
+ ///
+ /// Add link between entities
+ ///
+ Link,
+
+ ///
+ /// Remove link between entities
+ ///
+ Unlink
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.OData.Shared/DefaultEdmPatchMethodHandler.cs b/src/Microsoft.AspNet.OData.Shared/DefaultEdmPatchMethodHandler.cs
new file mode 100644
index 0000000000..9b13f3ca21
--- /dev/null
+++ b/src/Microsoft.AspNet.OData.Shared/DefaultEdmPatchMethodHandler.cs
@@ -0,0 +1,165 @@
+//-----------------------------------------------------------------------------
+//
+// 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.OData.Edm;
+
+namespace Microsoft.AspNet.OData
+{
+ ///
+ /// This is the default Patch Handler for non CLR type. This calss has default Get, Create and Update
+ /// and will do these actions. This will be used when the original collection to be Patched is provided.
+ ///
+ internal class DefaultEdmODataAPIHandler : EdmODataAPIHandler
+ {
+ IEdmEntityType entityType;
+ ICollection originalList;
+
+ public DefaultEdmODataAPIHandler(ICollection originalList, IEdmEntityType entityType)
+ {
+ Contract.Assert(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;
+
+ Contract.Assert(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(IEdmChangedObject changedObject, 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;
+ }
+ }
+
+ 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;
+ }
+
+
+ private EdmStructuredObject GetFilteredItem(IDictionary keyValues)
+ {
+ //This logic is for filtering the object based on the set of keys,
+ //There will only be very few key elements usually, mostly 1, so performance wont be impacted.
+
+ if(originalList == null)
+ {
+ 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..34cf87bbb8
--- /dev/null
+++ b/src/Microsoft.AspNet.OData.Shared/DefaultODataAPIHandler.cs
@@ -0,0 +1,157 @@
+//-----------------------------------------------------------------------------
+//
+// 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 type. This calss has default Get, Create and Update
+ /// and will do these actions. This will be used when the original collection to be Patched is provided.
+ ///
+ ///
+ internal class DefaultODataAPIHandler : ODataAPIHandler where TStructuralType :class
+ {
+ Type _clrType;
+ ICollection originalList;
+
+ 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(originalList != null)
+ {
+ originalList = new List();
+ }
+
+ createdObject = Activator.CreateInstance(_clrType) as TStructuralType;
+ 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
+ {
+ 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;
+ }
+
+
+ private TStructuralType GetFilteredItem(IDictionary keyValues)
+ {
+ //This logic is for filtering the object based on the set of keys,
+ //There will only be very few key elements usually, mostly 1, so performance wont be impacted.
+
+ if(originalList == null || originalList.Count == 0)
+ {
+ return default(TStructuralType);
+ }
+
+ Dictionary propertyInfos = new Dictionary();
+
+ foreach (string key in keyValues.Keys)
+ {
+ propertyInfos.Add(key, _clrType.GetProperty(key));
+ }
+
+ foreach (TStructuralType item in originalList)
+ {
+ bool isMatch = true;
+
+ foreach (KeyValuePair keyValue in keyValues)
+ {
+ if (!Equals(propertyInfos[keyValue.Key].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/DeltaDeletedEntityObjectOfT.cs b/src/Microsoft.AspNet.OData.Shared/DeltaDeletedEntityObjectOfT.cs
new file mode 100644
index 0000000000..fefa09cca0
--- /dev/null
+++ b/src/Microsoft.AspNet.OData.Shared/DeltaDeletedEntityObjectOfT.cs
@@ -0,0 +1,129 @@
+//-----------------------------------------------------------------------------
+//
+// 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.Reflection;
+using System.Threading;
+using Microsoft.AspNet.OData.Builder;
+using Microsoft.AspNet.OData.Common;
+using Microsoft.OData;
+using Microsoft.OData.Edm;
+
+namespace Microsoft.AspNet.OData
+{
+ ///
+ /// Represents an with a backing CLR .
+ /// Used to hold the Deleted Entry object in the Delta Feed Payload.
+ ///
+ [NonValidatingParameterBinding]
+ public class DeltaDeletedEntityObject : Delta, IDeltaDeletedEntityObject where TStructuralType : class
+ {
+ ///
+ /// Initializes a new instance of .
+ ///
+ public DeltaDeletedEntityObject()
+ : this(typeof(TStructuralType))
+ {
+ }
+
+ ///
+ /// Initializes a new instance of .
+ ///
+ /// The derived entity type or complex type for which the changes would be tracked.
+ /// should be assignable to instances of .
+ ///
+ public DeltaDeletedEntityObject(Type structuralType)
+ : this(structuralType, dynamicDictionaryPropertyInfo: null, instanceAnnotationsPropertyInfo: null)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of .
+ ///
+ /// The derived entity type or complex type for which the changes would be tracked.
+ /// should be assignable to instances of .
+ ///
+ /// Properties to update
+ public DeltaDeletedEntityObject(Type structuralType, IEnumerable updatableProperties)
+ : this(structuralType, updatableProperties, dynamicDictionaryPropertyInfo: null, instanceAnnotationsPropertyInfo: null)
+ {
+
+ }
+
+ ///
+ /// Initializes a new instance of .
+ ///
+ /// The derived entity type or complex type for which the changes would be tracked.
+ /// should be assignable to instances of .
+ ///
+ /// The property info that is used as container for Instance Annotations
+ public DeltaDeletedEntityObject(Type structuralType, PropertyInfo instanceAnnotationsPropertyInfo)
+ : this(structuralType, dynamicDictionaryPropertyInfo: null, instanceAnnotationsPropertyInfo)
+ {
+
+ }
+
+ ///
+ /// Initializes a new instance of .
+ ///
+ /// The derived entity type or complex type for which the changes would be tracked.
+ /// should be assignable to instances of .
+ ///
+ /// The property info that is used as dictionary of dynamic
+ /// properties. null means this entity type is not open.
+ /// The property info that is used as container for Instance Annotations
+ public DeltaDeletedEntityObject(Type structuralType, PropertyInfo dynamicDictionaryPropertyInfo, PropertyInfo instanceAnnotationsPropertyInfo)
+ : this(structuralType, updatableProperties: null , dynamicDictionaryPropertyInfo, instanceAnnotationsPropertyInfo)
+ {
+
+ }
+
+ ///
+ /// Initializes a new instance of .
+ ///
+ /// The derived entity type or complex type for which the changes would be tracked.
+ /// should be assignable to instances of .
+ ///
+ /// Properties that can be updated
+ /// The property info that is used as dictionary of dynamic
+ /// properties. null means this entity type is not open.
+ /// The property info that is used as container for Instance Annotations
+ public DeltaDeletedEntityObject(Type structuralType, IEnumerable updatableProperties, PropertyInfo dynamicDictionaryPropertyInfo, PropertyInfo instanceAnnotationsPropertyInfo)
+ : this(structuralType, updatableProperties, dynamicDictionaryPropertyInfo, false, instanceAnnotationsPropertyInfo)
+ {
+
+ }
+
+ ///
+ /// Initializes a new instance of .
+ ///
+ /// The derived entity type or complex type for which the changes would be tracked.
+ /// should be assignable to instances of .
+ ///
+ /// Properties that can be updated
+ /// The property info that is used as dictionary of dynamic
+ /// properties. null means this entity type is not open.
+ /// To determine if the entity is a complex type
+ /// The property info that is used as container for Instance Annotations
+ public DeltaDeletedEntityObject(Type structuralType, IEnumerable updatableProperties, PropertyInfo dynamicDictionaryPropertyInfo, bool isComplexType, PropertyInfo instanceAnnotationsPropertyInfo)
+ : base(structuralType, updatableProperties, dynamicDictionaryPropertyInfo, isComplexType, instanceAnnotationsPropertyInfo)
+ {
+ DeltaKind = EdmDeltaEntityKind.DeletedEntry;
+ }
+
+ ///
+ public Uri Id { get; set; }
+
+ ///
+ public DeltaDeletedEntryReason? Reason { get; set; }
+
+ ///
+ public IEdmNavigationSource NavigationSource { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.OData.Shared/DeltaOfTStructuralType.cs b/src/Microsoft.AspNet.OData.Shared/DeltaOfTStructuralType.cs
index 4dd0590eb2..618fbc7ef5 100644
--- a/src/Microsoft.AspNet.OData.Shared/DeltaOfTStructuralType.cs
+++ b/src/Microsoft.AspNet.OData.Shared/DeltaOfTStructuralType.cs
@@ -16,8 +16,11 @@
using System.Reflection;
using System.Runtime.CompilerServices;
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;
namespace Microsoft.AspNet.OData
{
@@ -26,7 +29,7 @@ namespace Microsoft.AspNet.OData
///
/// TStructuralType is the type of the instance this delta tracks changes for.
[NonValidatingParameterBinding]
- public class Delta : TypedDelta, IDelta where TStructuralType : class
+ public class Delta : TypedDelta, IDelta, IDeltaSetItem where TStructuralType : class
{
// cache property accessors for this type and all its derived types.
private static readonly ConcurrentDictionary>> _propertyCache
@@ -44,6 +47,7 @@ private static readonly ConcurrentDictionary _changedDynamicProperties;
private IDictionary _dynamicDictionaryCache;
@@ -79,6 +83,22 @@ public Delta(Type structuralType, IEnumerable updatableProperties)
{
}
+ ///
+ /// Initializes a new instance of .
+ ///
+ /// The derived entity type or complex type for which the changes would be tracked.
+ /// should be assignable to instances of .
+ ///
+ /// The set of properties that can be updated or reset. Unknown property
+ /// names, including those of dynamic properties, are ignored.
+ /// The property info that is used as dictionary of dynamic
+ /// properties. null means this entity type is not open.
+ public Delta(Type structuralType, IEnumerable updatableProperties, PropertyInfo dynamicDictionaryPropertyInfo)
+ : this(structuralType, updatableProperties: updatableProperties, dynamicDictionaryPropertyInfo, isComplexType: false)
+ {
+
+ }
+
///
/// Initializes a new instance of .
///
@@ -89,10 +109,12 @@ public Delta(Type structuralType, IEnumerable updatableProperties)
/// names, including those of dynamic properties, are ignored.
/// The property info that is used as dictionary of dynamic
/// properties. null means this entity type is not open.
+ /// Boolean value to determine if its a complex type
public Delta(Type structuralType, IEnumerable updatableProperties,
- PropertyInfo dynamicDictionaryPropertyInfo)
- : this(structuralType, updatableProperties: updatableProperties, dynamicDictionaryPropertyInfo, false)
+ PropertyInfo dynamicDictionaryPropertyInfo, bool isComplexType)
+ : this(structuralType, updatableProperties: updatableProperties, dynamicDictionaryPropertyInfo, isComplexType, instanceAnnotationsPropertyInfo: null)
{
+
}
///
@@ -106,20 +128,31 @@ public Delta(Type structuralType, IEnumerable updatableProperties,
/// The property info that is used as dictionary of dynamic
/// properties. null means this entity type is not open.
/// Boolean value to determine if its a complex type
+ /// The property info that is used as container for Instance Annotations
public Delta(Type structuralType, IEnumerable updatableProperties,
- PropertyInfo dynamicDictionaryPropertyInfo, bool isComplexType)
+ PropertyInfo dynamicDictionaryPropertyInfo, bool isComplexType, PropertyInfo instanceAnnotationsPropertyInfo)
{
_dynamicDictionaryPropertyinfo = dynamicDictionaryPropertyInfo;
Reset(structuralType);
InitializeProperties(updatableProperties);
+ TransientInstanceAnnotationContainer = new ODataInstanceAnnotationContainer();
+ _instanceAnnotationsPropertyInfo = instanceAnnotationsPropertyInfo;
+ DeltaKind = EdmDeltaEntityKind.Entry;
IsComplexType = isComplexType;
}
///
- public override Type StructuredType => _structuredType;
+ public override Type StructuredType
+ => _structuredType;
+
+ internal IDictionary DeltaNestedResources
+ {
+ get { return _deltaNestedResources; }
+ }
///
- public override Type ExpectedClrType => typeof(TStructuralType);
+ public override Type ExpectedClrType
+ => typeof(TStructuralType);
///
/// The list of property names that can be updated.
@@ -133,6 +166,23 @@ public Delta(Type structuralType, IEnumerable updatableProperties,
///
public bool IsComplexType { get; private set; }
+ ///
+ /// Gets the enum type of .
+ ///
+ public EdmDeltaEntityKind DeltaKind { get; protected set; }
+
+ ///
+ public IODataInstanceAnnotationContainer TransientInstanceAnnotationContainer { get; set; }
+
+ ///
+ public IODataIdContainer ODataIdContainer { get; set; }
+
+ ///
+ public ODataPath ODataPath { get; set; }
+
+ ///
+ internal PropertyInfo InstanceAnnotationsPropertyInfo { get { return _instanceAnnotationsPropertyInfo; } }
+
///
public override void Clear()
{
@@ -147,6 +197,19 @@ public override bool TrySetPropertyValue(string name, object value)
throw Error.ArgumentNull(nameof(name));
}
+ if (_instanceAnnotationsPropertyInfo != null && name == _instanceAnnotationsPropertyInfo.Name)
+ {
+ IODataInstanceAnnotationContainer annotationValue = value as IODataInstanceAnnotationContainer;
+ if (value != null && annotationValue == null)
+ {
+ return false;
+ }
+
+ _instanceAnnotationsPropertyInfo.SetValue(_instance, annotationValue);
+
+ return true;
+ }
+
if (_dynamicDictionaryPropertyinfo != null)
{
// Dynamic property can have the same name as the dynamic property dictionary.
@@ -165,7 +228,7 @@ public override bool TrySetPropertyValue(string name, object value)
}
}
- if (value is IDelta)
+ if (value is IDelta || value is IDeltaSet)
{
return TrySetNestedResourceInternal(name, value);
}
@@ -183,6 +246,19 @@ public override bool TryGetPropertyValue(string name, out object value)
throw Error.ArgumentNull(nameof(name));
}
+ if (_instanceAnnotationsPropertyInfo != null && name == _instanceAnnotationsPropertyInfo.Name)
+ {
+ object propertyValue = _instanceAnnotationsPropertyInfo.GetValue(_instance);
+ if (propertyValue != null)
+ {
+ value = (IODataInstanceAnnotationContainer)propertyValue;
+ return true;
+ }
+
+ value = null;
+ return false;
+ }
+
if (_dynamicDictionaryPropertyinfo != null)
{
if (_dynamicDictionaryCache == null)
@@ -203,6 +279,14 @@ public override bool TryGetPropertyValue(string name, out object value)
object deltaNestedResource = _deltaNestedResources[name];
Contract.Assert(deltaNestedResource != null, "deltaNestedResource != null");
+
+ //If DeltaSet collection, we are handling delta collections so the value will be that itself and no need to get instance value
+ if (deltaNestedResource is IDeltaSet)
+ {
+ value = deltaNestedResource;
+ return true;
+ }
+
Contract.Assert(IsDeltaOfT(deltaNestedResource.GetType()));
// Get the Delta<{NestedResourceType}>._instance using Reflection.
@@ -344,11 +428,18 @@ public override IEnumerable GetUnchangedPropertyNames()
/// to the entity recursively.
///
/// The entity to be updated.
- public TStructuralType CopyChangedValues(TStructuralType original)
+ public void CopyChangedValues(TStructuralType original)
+ {
+ CopyChangedValues(original, null, null);
+ }
+
+ [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity")]
+ [SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily")]
+ internal void CopyChangedValues(TStructuralType original, ODataAPIHandler apiHandler = null, ODataAPIHandlerFactory apiHandlerFactory = null)
{
if (original == null)
{
- throw Error.ArgumentNull(nameof(original));
+ throw Error.ArgumentNull("original");
}
// Delta parameter type cannot be derived type of original
@@ -358,6 +449,19 @@ public TStructuralType CopyChangedValues(TStructuralType original)
throw Error.Argument(nameof(original), SRResources.DeltaTypeMismatch, _structuredType, original.GetType());
}
+ //To apply ODataId based handler to apply properties on original.
+ if (apiHandlerFactory != null)
+ {
+ IODataAPIHandler refapiHandler = apiHandlerFactory.GetHandler(ODataPath);
+
+ if (refapiHandler != null && ODataPath.Any() && apiHandler.ToString() != refapiHandler.ToString())
+ {
+ ODataAPIHandler refapiHandlerOfT = refapiHandler as ODataAPIHandler;
+
+ ApplyPropertiesBasedOnOdataId(original, refapiHandlerOfT, ODataPath.GetKeys());
+ }
+ }
+
RuntimeHelpers.EnsureSufficientExecutionStack();
// For regular non-structural properties at current level.
@@ -376,97 +480,53 @@ public TStructuralType CopyChangedValues(TStructuralType original)
// Patch for each nested resource changed under this TStructuralType.
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 deltaObject = _deltaNestedResources[nestedResourceName];
- dynamic instance = deltaObject.GetInstance();
-
- // Recursively patch up the instance with the nested resources.
- deltaObject.CopyChangedValues(instance);
+ IODataAPIHandler apiHandlerNested = apiHandler.GetNestedHandler(original, nestedResourceName);
- _allProperties[nestedResourceName].SetValue(original, instance);
+ if (apiHandlerNested != null)
+ {
+ deltaNestedResource.CopyChangedValues(apiHandlerNested, 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 (deltaNestedResource.IsComplexType && newType != originalType)
+ if (!TryGetPropertyRef(original, nestedResourceName, out originalNestedResource))
{
- originalNestedResource = ReAssignComplexDerivedType(originalNestedResource, newType, originalType, deltaNestedResource.ExpectedClrType);
- _structuredType.GetProperty(nestedResourceName).SetValue(original, (object)originalNestedResource);
+ throw Error.Argument(nestedResourceName, SRResources.DeltaNestedResourceNameNotFound,
+ nestedResourceName, original.GetType());
}
- deltaNestedResource.CopyChangedValues(originalNestedResource);
- }
- }
-
- return original;
- }
-
- private static dynamic ReAssignComplexDerivedType(dynamic originalValue, Type newType, Type originalType, Type declaredType)
- {
- //As per OASIS discussion, changing a complex type from 1 derived type to another is allowed if both derived type have a common ancestor and the property
- //is declared in terms of a common ancestor. The logic below checks for a common ancestor. Create a new object of the derived type in delta request.
- //And copy the common properties.
-
- if(newType == originalType)
- {
- return originalValue;
- }
-
- Type newBaseType = newType;
- HashSet newBaseTypes = new HashSet();
-
- //Iterate till you find the declaring base type and add all that to hashset
- while (newBaseType != null && newBaseType != declaredType)
- {
- newBaseTypes.Add(newBaseType);
- newBaseType = newBaseType.BaseType;
- }
-
- newBaseTypes.Add(declaredType);
+ if (originalNestedResource == null)
+ {
+ // When patching original target of null value, directly set nested resource.
+ dynamic deltaObject = _deltaNestedResources[nestedResourceName];
+ dynamic instance = deltaObject.GetInstance();
- //Here original type is the type for original (T) resource.
- //We will keep going to base types and finally will get the Common Basetype for the derived complex types in to the originalType variable.
+ // Recursively patch up the instance with the nested resources.
+ deltaObject.CopyChangedValues(instance);
- //The new Original type, means the new complex type (T) which will replace the current complex type.
- dynamic newOriginalNestedResource = originalValue;
+ _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.");
- while (originalType != null)
- {
- if (newBaseTypes.Contains(originalType))
- {
- //Now originalType = common base type of the derived complex types.
- //OriginalNested Resource = T(of current Complex type). We are creating newOriginalNestedResource (T - new complex type).
- newOriginalNestedResource = Activator.CreateInstance(newType);
+ Type newType = deltaNestedResource.StructuredType;
+ Type originalType = originalNestedResource.GetType();
- //Here we get all the properties of common base type and get value from original complex type(T) and
- //copy it to the new complex type newOriginalNestedResource(came as a part of Delta)
+ if (deltaNestedResource.IsComplexType && newType != originalType)
+ {
+ originalNestedResource = ReAssignComplexDerivedType(original, nestedResourceName, originalNestedResource, newType, originalType, deltaNestedResource.ExpectedClrType);
+ }
- foreach (PropertyInfo property in originalType.GetProperties())
- {
- object value = property.GetValue(originalValue);
- property.SetValue(newOriginalNestedResource, value);
+ deltaNestedResource.CopyChangedValues(originalNestedResource);
}
-
- break;
}
- originalType = originalType.BaseType;
}
-
- return newOriginalNestedResource;
}
///
@@ -495,21 +555,52 @@ public void CopyUnchangedValues(TStructuralType original)
CopyUnchangedDynamicValues(original);
}
+ ///
+ /// Overwrites the entity with the changes tracked by this Delta.
+ /// The semantics of this operation are equivalent to a HTTP PATCH operation, hence the name.
+ ///
+ /// The entity to be updated.
+ public void Patch(TStructuralType original)
+ {
+ CopyChangedValues(original);
+ }
///
/// Overwrites the entity with the changes tracked by this Delta.
/// The semantics of this operation are equivalent to a HTTP PATCH operation, hence the name.
///
/// The entity to be updated.
- /// The original value after Patching
- public TStructuralType Patch(TStructuralType original)
+ /// API Handler for the entity.
+ /// API Handler Factory
+ internal void Patch(TStructuralType original, IODataAPIHandler apiHandler, ODataAPIHandlerFactory apiHandlerFactory)
+ {
+ Debug.Assert(apiHandler != null);
+
+ CopyChangedValues(original, apiHandler as ODataAPIHandler, apiHandlerFactory);
+ }
+
+ ///
+ /// This is basically Patch on ODataId. This applies ODataId parsed Navigation paths, get the value identified by that and copy it on original object
+ ///
+ private void ApplyPropertiesBasedOnOdataId(TStructuralType original, ODataAPIHandler refapiHandlerOfT, Dictionary keyProperties)
{
- if (IsComplexType)
+ Debug.Assert(refapiHandlerOfT != null);
+
+ TStructuralType referencedObj;
+ string error;
+
+ //todo: this logic feels brittle to me
+ //Checking to get the referenced entity, get the properties and apply it on original object
+ if (refapiHandlerOfT.TryGet(keyProperties, out referencedObj, out error) == ODataAPIResponseStatus.Success)
{
- original = ReAssignComplexDerivedType(original, _structuredType, original.GetType(), ExpectedClrType) as TStructuralType;
- }
+ foreach (string property in _updatableProperties)
+ {
+ PropertyInfo propertyInfo = _structuredType.GetProperty(property);
- return CopyChangedValues(original);
+ object value = propertyInfo.GetValue(referencedObj);
+ propertyInfo.SetValue(original, value);
+ }
+ }
}
///
@@ -523,6 +614,58 @@ public void Put(TStructuralType original)
CopyUnchangedValues(original);
}
+ private dynamic ReAssignComplexDerivedType(TStructuralType parent, string nestedPropertyName, dynamic originalValue, Type newType, Type originalType, Type declaredType)
+ {
+ //As per OASIS discussion, changing a complex type from 1 derived type to another is allowed if both derived type have a common ancestor and the property
+ //is declared in terms of a common ancestor. The logic below checks for a common ancestor. Create a new object of the derived type in delta request.
+ //And copy the common properties.
+
+ Type newBaseType = newType;
+ HashSet newBaseTypes = new HashSet();
+
+ //Iterate till you find the declaring base type and add all that to hashset
+ while (newBaseType != null && newBaseType != declaredType)
+ {
+ newBaseTypes.Add(newBaseType);
+ newBaseType = newBaseType.BaseType;
+ }
+
+ newBaseTypes.Add(declaredType);
+
+ //Here original type is the type for original (T) resource.
+ //We will keep going to base types and finally will get the Common Basetype for the derived complex types in to the originalType variable.
+
+ //The new Original type, means the new complex type (T) which will replace the current complex type.
+ dynamic newOriginalNestedResource = originalValue;
+
+ while (originalType != null)
+ {
+ if (newBaseTypes.Contains(originalType))
+ {
+ //Now originalType = common base type of the derived complex types.
+ //OriginalNested Resource = T(of current Complex type). We are creating newOriginalNestedResource (T - new complex type).
+ newOriginalNestedResource = Activator.CreateInstance(newType);
+
+ //Here we get all the properties of common base type and get value from original complex type(T) and
+ //copy it to the new complex type newOriginalNestedResource(came as a part of Delta)
+
+ foreach (PropertyInfo property in originalType.GetProperties())
+ {
+ object value = property.GetValue(originalValue);
+ property.SetValue(newOriginalNestedResource, value);
+ }
+
+ _structuredType.GetProperty(nestedPropertyName).SetValue(parent, (object)newOriginalNestedResource);
+
+ break;
+ }
+
+ originalType = originalType.BaseType;
+ }
+
+ return newOriginalNestedResource;
+ }
+
private static void CopyDynamicPropertyDictionary(IDictionary source,
IDictionary dest, PropertyInfo dynamicPropertyInfo, TStructuralType targetEntity)
{
@@ -820,11 +963,16 @@ private bool TrySetNestedResourceInternal(string name, object deltaNestedResourc
return false;
}
- PropertyAccessor cacheHit = _allProperties[name];
- // Get the Delta<{NestedResourceType}>._instance using Reflection.
- FieldInfo field = deltaNestedResource.GetType().GetField("_instance", BindingFlags.NonPublic | BindingFlags.Instance);
- Contract.Assert(field != null, "field != null");
- cacheHit.SetValue(_instance, field.GetValue(deltaNestedResource));
+ //If Edmchangedobject collection, we are handling delta collections so the instance value need not be set,
+ //as we consider the value as collection of Delta itself and not instance value of the field
+ if (!(deltaNestedResource is IDeltaSet))
+ {
+ PropertyAccessor cacheHit = _allProperties[name];
+ // Get the Delta<{NestedResourceType}>._instance using Reflection.
+ FieldInfo field = deltaNestedResource.GetType().GetField("_instance", BindingFlags.NonPublic | BindingFlags.Instance);
+ Contract.Assert(field != null, "field != null");
+ cacheHit.SetValue(_instance, field.GetValue(deltaNestedResource));
+ }
// Add the nested resource in the hierarchy.
// Note: We shouldn't add the structural properties to the _changedProperties
, which
@@ -834,4 +982,4 @@ private bool TrySetNestedResourceInternal(string name, object deltaNestedResourc
return true;
}
}
-}
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.OData.Shared/DeltaSetOfT.cs b/src/Microsoft.AspNet.OData.Shared/DeltaSetOfT.cs
new file mode 100644
index 0000000000..61c56a8b74
--- /dev/null
+++ b/src/Microsoft.AspNet.OData.Shared/DeltaSetOfT.cs
@@ -0,0 +1,343 @@
+//-----------------------------------------------------------------------------
+//
+// 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.Collections.ObjectModel;
+using System.Diagnostics;
+using System.Diagnostics.Contracts;
+using System.Linq;
+using System.Reflection;
+using Microsoft.AspNet.OData.Builder;
+using Microsoft.AspNet.OData.Common;
+using Microsoft.AspNet.OData.Extensions;
+using Org.OData.Core.V1;
+
+namespace Microsoft.AspNet.OData
+{
+ ///
+ /// Represents an that is a collection of s.
+ ///
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix")]
+ [NonValidatingParameterBinding]
+ public class DeltaSet : Collection, IDeltaSet where TStructuralType : class
+ {
+ private Type _clrType;
+ IList _keys;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// List of key names for the type
+ public DeltaSet(IList keys)
+ {
+ _keys = keys;
+ _clrType = typeof(TStructuralType);
+ }
+
+
+ ///
+ protected override void InsertItem(int index, IDeltaSetItem item)
+ {
+ Delta deltaItem = item as Delta;
+
+ //To ensure we dont insert null or a non related type to deltaset
+ if (deltaItem == null)
+ {
+ throw Error.Argument("item", SRResources.ChangedObjectTypeMismatch, item.GetType(), _clrType);
+ }
+
+ 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
+ ///
+ /// DeltaSet response
+ public DeltaSet Patch(ODataAPIHandler apiHandlerOfT, ODataAPIHandlerFactory apiHandlerFactory)
+ {
+ Debug.Assert(apiHandlerOfT != null);
+
+ return CopyChangedValues(apiHandlerOfT, apiHandlerFactory);
+ }
+
+
+ [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 list of collection
+
+ ODataAPIHandler apiHandlerOfT = apiHandler as ODataAPIHandler;
+
+ Debug.Assert(apiHandlerOfT != null);
+
+ DeltaSet deltaSet = CreateDeltaSet();
+
+ foreach (Delta changedObj in Items)
+ {
+ DataModificationOperationKind operation = DataModificationOperationKind.Update;
+
+ //Get filtered item based on keys
+ TStructuralType original = null;
+ string errorMessage = string.Empty;
+ string getErrorMessage = string.Empty;
+
+ Dictionary keyValues = new Dictionary();
+
+ foreach (string key in _keys)
+ {
+ object value;
+
+ if (changedObj.TryGetPropertyValue(key, out value))
+ {
+ keyValues.Add(key, value);
+ }
+ }
+
+ try
+ {
+ ODataAPIResponseStatus ODataAPIResponseStatus = apiHandlerOfT.TryGet(changedObj.ODataPath.GetKeys(), out original, out getErrorMessage);
+
+ DeltaDeletedEntityObject deletedObj = changedObj as DeltaDeletedEntityObject;
+
+ if (ODataAPIResponseStatus == ODataAPIResponseStatus.Failure || (deletedObj != null && ODataAPIResponseStatus == ODataAPIResponseStatus.NotFound))
+ {
+ 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;
+
+ changedObj.CopyChangedValues(original, apiHandlerOfT, apiHandlerFactory);
+
+ if (apiHandlerOfT.TryDelete(keyValues, out errorMessage) != ODataAPIResponseStatus.Success)
+ {
+ //Handle Failed Operation - Delete
+
+ if (ODataAPIResponseStatus == ODataAPIResponseStatus.Success)
+ {
+ IDeltaSetItem changedObject = HandleFailedOperation(changedObj, operation, original, errorMessage);
+ deltaSet.Add(changedObject);
+ continue;
+ }
+ }
+
+ deltaSet.Add(deletedObj);
+ }
+ else
+ {
+ if (ODataAPIResponseStatus == ODataAPIResponseStatus.NotFound)
+ {
+ operation = DataModificationOperationKind.Insert;
+
+ if (apiHandlerOfT.TryCreate(keyValues, out original, out errorMessage) != ODataAPIResponseStatus.Success)
+ {
+ //Handle failed Opreataion - create
+ IDeltaSetItem changedObject = HandleFailedOperation(changedObj, operation, original, errorMessage);
+ deltaSet.Add(changedObject);
+ continue;
+ }
+ }
+ else if (ODataAPIResponseStatus == ODataAPIResponseStatus.Success)
+ {
+ operation = DataModificationOperationKind.Update;
+ }
+ else
+ {
+ //Handle failed operation
+ IDeltaSetItem changedObject = HandleFailedOperation(changedObj, operation, original, getErrorMessage);
+ deltaSet.Add(changedObject);
+ continue;
+ }
+
+ //Patch for addition/update. This will call Delta for each item in the collection
+ // This will work in case we use delegates for using users method to create an object
+ changedObj.CopyChangedValues(original, apiHandlerOfT, 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 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:
+ 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);
+
+ Contract.Assert(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 instAnnValue;
+ changedObj.TryGetPropertyValue(deltaObject.InstanceAnnotationsPropertyInfo.Name, out instAnnValue);
+ if (instAnnValue != null)
+ {
+ IODataInstanceAnnotationContainer instanceAnnotations = instAnnValue 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, true, changedObj.InstanceAnnotationsPropertyInfo) as DeltaDeletedEntityObject;
+
+ foreach (string property in changedObj.GetChangedPropertyNames())
+ {
+ SetPropertyValues(changedObj, deletedObject, property);
+ }
+
+ foreach (string property in changedObj.GetUnchangedPropertyNames())
+ {
+ SetPropertyValues(changedObj, deletedObject, property);
+ }
+
+ object annValue;
+ if (changedObj.TryGetPropertyValue(changedObj.InstanceAnnotationsPropertyInfo.Name, out annValue))
+ {
+ IODataInstanceAnnotationContainer instanceAnnotations = annValue 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.
+ 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);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.OData.Shared/EdmChangedObjectCollection.cs b/src/Microsoft.AspNet.OData.Shared/EdmChangedObjectCollection.cs
index 26f2851eea..f8244dd51f 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
{
@@ -22,7 +27,7 @@ public class EdmChangedObjectCollection : Collection, IEdmObj
private IEdmEntityType _entityType;
private EdmDeltaCollectionType _edmType;
private IEdmCollectionTypeReference _edmTypeReference;
-
+
///
/// Initializes a new instance of the class.
///
@@ -32,7 +37,7 @@ public EdmChangedObjectCollection(IEdmEntityType entityType)
{
Initialize(entityType);
}
-
+
///
/// Initializes a new instance of the class.
///
@@ -43,7 +48,13 @@ public EdmChangedObjectCollection(IEdmEntityType entityType, IList
+ /// Represents EntityType of the changedobject
+ ///
+ public IEdmEntityType EntityType { get { return _entityType; } }
+
///
public IEdmTypeReference GetEdmType()
{
@@ -61,5 +72,316 @@ private void Initialize(IEdmEntityType entityType)
_edmType = new EdmDeltaCollectionType(new EdmEntityTypeReference(_entityType, isNullable: true));
_edmTypeReference = new EdmCollectionTypeReference(_edmType);
}
+
+ ///
+ /// Patch for Types without underlying CLR types
+ ///
+ ///
+ /// ChangedObjectCollection response
+ internal EdmChangedObjectCollection Patch(ICollection originalCollection)
+ {
+ EdmODataAPIHandler apiHandler = new DefaultEdmODataAPIHandler(originalCollection, _entityType);
+
+ return CopyChangedValues(apiHandler);
+ }
+
+ ///
+ /// Patch for EdmChangedObjectCollection, a collection for IEdmChangedObject
+ ///
+ /// ChangedObjectCollection response
+ internal EdmChangedObjectCollection Patch(EdmODataAPIHandler apiHandler, ODataEdmAPIHandlerFactory 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, ODataEdmAPIHandlerFactory apiHandlerFactory = null)
+ {
+ EdmChangedObjectCollection changedObjectCollection = new EdmChangedObjectCollection(_entityType);
+ string[] keys = _entityType.Key().Select(x => x.Name).ToArray();
+
+ foreach (IEdmChangedObject changedObj in Items)
+ {
+ DataModificationOperationKind operation = DataModificationOperationKind.Update;
+ EdmStructuredObject originalObj = null;
+ string errorMessage = string.Empty;
+ string getErrorMessage = string.Empty;
+ IDictionary keyValues = GetKeyValues(keys, changedObj);
+
+ try
+ {
+ IEdmStructuredObject original = null;
+ 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(changedObj, out original, out errorMessage) != ODataAPIResponseStatus.Success)
+ {
+ //Handle failed Opreataion - 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, originalObj, keys, ex.Message, apiHandler);
+
+ Contract.Assert(changedObject != null);
+ changedObjectCollection.Add(changedObject);
+ }
+ }
+
+ return changedObjectCollection;
+ }
+
+ private static IDictionary GetKeyValues(string[] keys, IEdmChangedObject changedObj)
+ {
+ IDictionary keyValues = new Dictionary();
+
+ foreach (string key in keys)
+ {
+ object value;
+ changedObj.TryGetPropertyValue(key, out value);
+
+ if (value != null)
+ {
+ keyValues.Add(key, value);
+ }
+ }
+
+ return keyValues;
+ }
+
+ private void PatchItem(EdmStructuredObject changedObj, EdmStructuredObject originalObj, EdmODataAPIHandler apiHandler, ODataEdmAPIHandlerFactory 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, ODataEdmAPIHandlerFactory 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, ODataEdmAPIHandlerFactory 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,
+ string[] 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(string[] keys, EdmDeltaDeletedEntityObject edmDeletedObject)
+ {
+ bool hasNullKeys = false;
+ for (int i = 0; i < keys.Length; i++)
+ {
+ object value;
+ if (edmDeletedObject.TryGetPropertyValue(keys[i], out value))
+ {
+ hasNullKeys = true;
+ break;
+ }
+ }
+
+ if (hasNullKeys)
+ {
+ edmDeletedObject.Id = string.Empty;
+ }
+ }
}
}
diff --git a/src/Microsoft.AspNet.OData.Shared/EdmDeltaDeletedEntityObject.cs b/src/Microsoft.AspNet.OData.Shared/EdmDeltaDeletedEntityObject.cs
index bc9326d13f..3cf8d0f952 100644
--- a/src/Microsoft.AspNet.OData.Shared/EdmDeltaDeletedEntityObject.cs
+++ b/src/Microsoft.AspNet.OData.Shared/EdmDeltaDeletedEntityObject.cs
@@ -21,7 +21,6 @@ public class EdmDeltaDeletedEntityObject : EdmEntityObject, IEdmDeltaDeletedEnti
{
private string _id;
private DeltaDeletedEntryReason _reason;
- private EdmDeltaType _edmType;
private IEdmNavigationSource _navigationSource;
///
@@ -50,7 +49,7 @@ public EdmDeltaDeletedEntityObject(IEdmEntityTypeReference entityTypeReference)
public EdmDeltaDeletedEntityObject(IEdmEntityType entityType, bool isNullable)
: base(entityType, isNullable)
{
- _edmType = new EdmDeltaType(entityType, EdmDeltaEntityKind.DeletedEntry);
+
}
///
@@ -80,12 +79,11 @@ public DeltaDeletedEntryReason Reason
}
///
- public EdmDeltaEntityKind DeltaKind
+ public override EdmDeltaEntityKind DeltaKind
{
get
- {
- Contract.Assert(_edmType != null);
- return _edmType.DeltaKind;
+ {
+ return EdmDeltaEntityKind.DeletedEntry;
}
}
diff --git a/src/Microsoft.AspNet.OData.Shared/EdmDeltaDeletedLink.cs b/src/Microsoft.AspNet.OData.Shared/EdmDeltaDeletedLink.cs
index df81698dfc..ba9011822b 100644
--- a/src/Microsoft.AspNet.OData.Shared/EdmDeltaDeletedLink.cs
+++ b/src/Microsoft.AspNet.OData.Shared/EdmDeltaDeletedLink.cs
@@ -21,7 +21,6 @@ public class EdmDeltaDeletedLink : EdmEntityObject, IEdmDeltaDeletedLink
private Uri _source;
private Uri _target;
private string _relationship;
- private EdmDeltaType _edmType;
///
/// Initializes a new instance of the class.
@@ -49,7 +48,7 @@ public EdmDeltaDeletedLink(IEdmEntityTypeReference entityTypeReference)
public EdmDeltaDeletedLink(IEdmEntityType entityType, bool isNullable)
: base(entityType, isNullable)
{
- _edmType = new EdmDeltaType(entityType, EdmDeltaEntityKind.DeletedLinkEntry);
+
}
///
@@ -92,12 +91,11 @@ public string Relationship
}
///
- public EdmDeltaEntityKind DeltaKind
+ public override EdmDeltaEntityKind DeltaKind
{
get
- {
- Contract.Assert(_edmType != null);
- return _edmType.DeltaKind;
+ {
+ return EdmDeltaEntityKind.DeletedLinkEntry;
}
}
}
diff --git a/src/Microsoft.AspNet.OData.Shared/EdmDeltaEntityObject.cs b/src/Microsoft.AspNet.OData.Shared/EdmDeltaEntityObject.cs
index e90a72ffaf..ce8131b476 100644
--- a/src/Microsoft.AspNet.OData.Shared/EdmDeltaEntityObject.cs
+++ b/src/Microsoft.AspNet.OData.Shared/EdmDeltaEntityObject.cs
@@ -17,8 +17,7 @@ namespace Microsoft.AspNet.OData
///
[NonValidatingParameterBinding]
public class EdmDeltaEntityObject : EdmEntityObject, IEdmChangedObject
- {
- private EdmDeltaType _edmType;
+ {
private IEdmNavigationSource _navigationSource;
///
@@ -47,16 +46,15 @@ public EdmDeltaEntityObject(IEdmEntityTypeReference entityTypeReference)
public EdmDeltaEntityObject(IEdmEntityType entityType, bool isNullable)
: base(entityType, isNullable)
{
- _edmType = new EdmDeltaType(entityType, EdmDeltaEntityKind.Entry);
+
}
///
- public EdmDeltaEntityKind DeltaKind
+ public override EdmDeltaEntityKind DeltaKind
{
get
{
- Contract.Assert(_edmType != null);
- return _edmType.DeltaKind;
+ return EdmDeltaEntityKind.Entry;
}
}
diff --git a/src/Microsoft.AspNet.OData.Shared/EdmDeltaLink.cs b/src/Microsoft.AspNet.OData.Shared/EdmDeltaLink.cs
index 2d329f4b21..d79a158407 100644
--- a/src/Microsoft.AspNet.OData.Shared/EdmDeltaLink.cs
+++ b/src/Microsoft.AspNet.OData.Shared/EdmDeltaLink.cs
@@ -92,12 +92,11 @@ public string Relationship
}
///
- public EdmDeltaEntityKind DeltaKind
+ public override EdmDeltaEntityKind DeltaKind
{
get
- {
- Contract.Assert(_edmType != null);
- return _edmType.DeltaKind;
+ {
+ return EdmDeltaEntityKind.LinkEntry;
}
}
}
diff --git a/src/Microsoft.AspNet.OData.Shared/EdmEntityObject.cs b/src/Microsoft.AspNet.OData.Shared/EdmEntityObject.cs
index d0f436fa02..39a4268580 100644
--- a/src/Microsoft.AspNet.OData.Shared/EdmEntityObject.cs
+++ b/src/Microsoft.AspNet.OData.Shared/EdmEntityObject.cs
@@ -6,7 +6,13 @@
//------------------------------------------------------------------------------
using System;
+using System.Diagnostics.Contracts;
+using System.Reflection;
+using Microsoft.AspNet.OData.Builder;
+using Microsoft.AspNet.OData.Common;
using Microsoft.OData.Edm;
+using Microsoft.OData.UriParser;
+using Org.OData.Core.V1;
namespace Microsoft.AspNet.OData
{
@@ -14,7 +20,7 @@ namespace Microsoft.AspNet.OData
/// Represents an with no backing CLR .
///
[NonValidatingParameterBinding]
- public class EdmEntityObject : EdmStructuredObject, IEdmEntityObject
+ public class EdmEntityObject : EdmStructuredObject, IEdmEntityObject, IEdmChangedObject
{
///
/// Initializes a new instance of the class.
@@ -41,7 +47,57 @@ public EdmEntityObject(IEdmEntityTypeReference edmType)
/// true if this object can be nullable; otherwise, false.
public EdmEntityObject(IEdmEntityType edmType, bool isNullable)
: base(edmType, isNullable)
+ {
+ PersistentInstanceAnnotationsContainer = new ODataInstanceAnnotationContainer();
+ TransientInstanceAnnotationContainer = new ODataInstanceAnnotationContainer();
+ }
+
+ ///
+ /// Instance Annotation container to hold Transient Annotations
+ ///
+ internal IODataInstanceAnnotationContainer TransientInstanceAnnotationContainer { get; set; }
+
+ ///
+ /// Instance Annotation container to hold Persistent Annotations
+ ///
+ public IODataInstanceAnnotationContainer PersistentInstanceAnnotationsContainer { get; set; }
+
+ ///
+ /// Container to hold ODataId
+ ///
+ public IODataIdContainer ODataIdContainer { get; set; }
+
+ ///
+ /// OData Path for the Item
+ ///
+ internal ODataPath ODataPath { get; set; }
+
+ ///
+ /// DeltaKind as Entry
+ ///
+ public virtual EdmDeltaEntityKind DeltaKind { get { return EdmDeltaEntityKind.Entry; } }
+
+ ///
+ /// Method to Add Data Modification Exception
+ ///
+ public void AddDataException(DataModificationExceptionType dataModificationException)
{
+ Contract.Assert(TransientInstanceAnnotationContainer != null);
+
+ TransientInstanceAnnotationContainer.AddResourceAnnotation(SRResources.DataModificationException, dataModificationException);
+ }
+
+ ///
+ /// Method to Add Data Modification Exception
+ ///
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate")]
+ public DataModificationExceptionType GetDataException()
+ {
+ Contract.Assert(TransientInstanceAnnotationContainer != null);
+
+ DataModificationExceptionType dataModificationExceptionType = TransientInstanceAnnotationContainer.GetResourceAnnotation(SRResources.DataModificationException) as DataModificationExceptionType;
+
+ return dataModificationExceptionType;
}
}
}
diff --git a/src/Microsoft.AspNet.OData.Shared/EdmODataAPIHandler.cs b/src/Microsoft.AspNet.OData.Shared/EdmODataAPIHandler.cs
new file mode 100644
index 0000000000..8d6472d95a
--- /dev/null
+++ b/src/Microsoft.AspNet.OData.Shared/EdmODataAPIHandler.cs
@@ -0,0 +1,46 @@
+using System.Collections.Generic;
+
+namespace Microsoft.AspNet.OData
+{
+
+ ///
+ /// Handler Class to handle users methods for create, delete and update.
+ /// This is the handler for data modification where there is no CLR type.
+ ///
+ internal abstract class EdmODataAPIHandler
+ {
+ ///
+ /// TryCreate method to create a new object.
+ ///
+ /// Changed object which can be appied on creted object, optional
+ /// The created object (Typeless)
+ /// Any error message in case of an exception
+ /// The status of the TryCreate Method, statuses are
+ public abstract ODataAPIResponseStatus TryCreate(IEdmChangedObject changedObject, out IEdmStructuredObject createdObject, out string errorMessage);
+
+ ///
+ /// TryGet method which tries to get the Origignal object based on a keyvalues.
+ ///
+ /// Key value pair 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);
+
+ ///
+ /// TryDelete Method which will delete the object based on keyvalue pairs.
+ ///
+ ///
+ ///
+ /// The status of the TryDelete Method, statuses are
+ public abstract ODataAPIResponseStatus TryDelete(IDictionary keyValues, out string errorMessage);
+
+ ///
+ /// Get the API handler for the nested type
+ ///
+ /// Parent instance.
+ /// The name of the navigation property for the handler
+ /// Nested Patch Method handler for the navigation property
+ public abstract EdmODataAPIHandler GetNestedHandler(IEdmStructuredObject parent, string navigationPropertyName);
+ }
+}
diff --git a/src/Microsoft.AspNet.OData.Shared/EdmStructuredObject.cs b/src/Microsoft.AspNet.OData.Shared/EdmStructuredObject.cs
index 0355462163..094d3a5ecf 100644
--- a/src/Microsoft.AspNet.OData.Shared/EdmStructuredObject.cs
+++ b/src/Microsoft.AspNet.OData.Shared/EdmStructuredObject.cs
@@ -9,6 +9,7 @@
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Linq;
+using System.Reflection;
using Microsoft.AspNet.OData.Common;
using Microsoft.AspNet.OData.Formatter;
using Microsoft.OData.Edm;
@@ -224,8 +225,15 @@ internal static object GetDefaultValue(IEdmTypeReference propertyType)
if (propertyType.IsPrimitive() ||
(isCollection && propertyType.AsCollection().ElementType().IsPrimitive()))
{
+
+ bool hasDefaultConstructor = (!clrType.IsClass) || clrType.GetConstructors(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static).
+ Any(x => x.GetParameters().Count() == 0);
+
// primitive or primitive collection
- return Activator.CreateInstance(clrType);
+ if (hasDefaultConstructor)
+ {
+ return Activator.CreateInstance(clrType);
+ }
}
else
{
diff --git a/src/Microsoft.AspNet.OData.Shared/EdmTypeExtensions.cs b/src/Microsoft.AspNet.OData.Shared/EdmTypeExtensions.cs
index 568dfb263b..489d68f49c 100644
--- a/src/Microsoft.AspNet.OData.Shared/EdmTypeExtensions.cs
+++ b/src/Microsoft.AspNet.OData.Shared/EdmTypeExtensions.cs
@@ -26,7 +26,7 @@ public static bool IsDeltaFeed(this IEdmType type)
{
throw Error.ArgumentNull("type");
}
- return (type.GetType() == typeof(EdmDeltaCollectionType));
+ return (type.GetType() == typeof(EdmDeltaCollectionType)) || (type.GetType() == typeof(IDeltaSet));
}
///
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..70467cf20d
--- /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
+{
+ ///
+ /// Extensions method for .
+ ///
+ internal static class ODataPathExtensions
+ {
+ ///
+ /// Get keys from the last .
+ ///
+ /// .
+ /// Dictionary of keys.
+ internal 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/CollectionDeserializationHelpers.cs b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/CollectionDeserializationHelpers.cs
index 53fdece31f..285f8b1f11 100644
--- a/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/CollectionDeserializationHelpers.cs
+++ b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/CollectionDeserializationHelpers.cs
@@ -8,6 +8,7 @@
using System;
using System.Collections;
using System.Collections.Generic;
+using System.Collections.ObjectModel;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Reflection;
@@ -119,10 +120,26 @@ public static void Clear(this IEnumerable collection, string propertyName, Type
clearMethod.Invoke(collection, _emptyObjectArray);
}
- public static bool TryCreateInstance(Type collectionType, IEdmCollectionTypeReference edmCollectionType, Type elementType, out IEnumerable instance)
+ public static bool TryCreateInstance(Type collectionType, IEdmCollectionTypeReference edmCollectionType, Type elementType, out IEnumerable instance, bool isDelta = false)
{
Contract.Assert(collectionType != null);
+ //For Delta Collection requests
+ if (isDelta)
+ {
+ if (elementType == typeof(IEdmEntityObject))
+ {
+ instance = new EdmChangedObjectCollection(edmCollectionType.ElementType().AsEntity().Definition as IEdmEntityType);
+ }
+ else
+ {
+ Type type = typeof(DeltaSet<>).MakeGenericType(elementType);
+ instance = Activator.CreateInstance(type, edmCollectionType.ElementType().AsEntity().Key().Select(x => x.Name).ToList()) as ICollection;
+ }
+
+ return true;
+ }
+
if (collectionType == typeof(EdmComplexObjectCollection))
{
instance = new EdmComplexObjectCollection(edmCollectionType);
diff --git a/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/DefaultODataDeserializerProvider.cs b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/DefaultODataDeserializerProvider.cs
index fd25f376da..b89f1355c9 100644
--- a/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/DefaultODataDeserializerProvider.cs
+++ b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/DefaultODataDeserializerProvider.cs
@@ -92,8 +92,6 @@ internal ODataDeserializer GetODataDeserializerImpl(Type type, Func m
return _rootContainer.GetRequiredService();
}
- // Get the model. Using a Func to delay evaluation of the model
- // until after the above checks have passed.
IEdmModel model = modelFunction();
ClrTypeCache typeMappingCache = model.GetTypeMappingCache();
IEdmTypeReference edmType = typeMappingCache.GetEdmType(type, model);
diff --git a/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/DeserializationHelpers.cs b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/DeserializationHelpers.cs
index 235c05c44f..cd6378a176 100644
--- a/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/DeserializationHelpers.cs
+++ b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/DeserializationHelpers.cs
@@ -87,18 +87,82 @@ internal static void ApplyProperty(ODataProperty property, IEdmStructuredTypeRef
}
}
- internal static void ApplyInstanceAnnotations(object resource, IEdmStructuredTypeReference structuredType, ODataResource oDataResource,
+ internal static void ApplyInstanceAnnotations(object resource, IEdmStructuredTypeReference structuredType, ODataResourceBase oDataResource,
ODataDeserializerProvider deserializerProvider, ODataDeserializerContext readContext)
{
- PropertyInfo propertyInfo = EdmLibHelpers.GetInstanceAnnotationsContainer(structuredType.StructuredDefinition(), readContext.Model);
- if (propertyInfo == null)
+ //Apply instance annotations for both entityobject/changedobject/delta and normal resources
+
+ IODataInstanceAnnotationContainer instanceAnnotationContainer = null;
+ IODataInstanceAnnotationContainer transientAnnotationContainer = null;
+
+ EdmEntityObject edmObject = resource as EdmEntityObject;
+
+ if (edmObject != null)
+ {
+ instanceAnnotationContainer = edmObject.PersistentInstanceAnnotationsContainer;
+ transientAnnotationContainer = edmObject.TransientInstanceAnnotationContainer;
+ }
+ else
+ {
+ PropertyInfo propertyInfo = EdmLibHelpers.GetInstanceAnnotationsContainer(structuredType.StructuredDefinition(), readContext.Model);
+ if (propertyInfo != null)
+ {
+ instanceAnnotationContainer = GetAnnotationContainer(propertyInfo, resource);
+ }
+
+ IDeltaSetItem deltaItem = resource as IDeltaSetItem;
+
+ if (deltaItem != null)
+ {
+ transientAnnotationContainer = deltaItem.TransientInstanceAnnotationContainer;
+ }
+ }
+
+ if (instanceAnnotationContainer == null && transientAnnotationContainer == null)
{
return;
}
- IODataInstanceAnnotationContainer instanceAnnotationContainer = GetAnnotationContainer(propertyInfo, resource);
+ SetInstanceAnnotations(oDataResource, instanceAnnotationContainer, transientAnnotationContainer, deserializerProvider, readContext);
+ }
+
+ internal static void ApplyODataIdContainer(object resource, IEdmStructuredTypeReference structuredType, ODataResourceBase oDataResource,
+ ODataDeserializerProvider deserializerProvider, ODataDeserializerContext readContext)
+ {
+ //Apply instance annotations for both entityobject/changedobject/delta and normal resources
+
+ IODataInstanceAnnotationContainer instanceAnnotationContainer = null;
+ IODataInstanceAnnotationContainer transientAnnotationContainer = null;
+
+ EdmEntityObject edmObject = resource as EdmEntityObject;
+
+ if (edmObject != null)
+ {
+ instanceAnnotationContainer = edmObject.PersistentInstanceAnnotationsContainer;
+ transientAnnotationContainer = edmObject.TransientInstanceAnnotationContainer;
+ }
+ else
+ {
+ PropertyInfo propertyInfo = EdmLibHelpers.GetInstanceAnnotationsContainer(structuredType.StructuredDefinition(), readContext.Model);
+ if (propertyInfo != null)
+ {
+ instanceAnnotationContainer = GetAnnotationContainer(propertyInfo, resource);
+ }
+
+ IDeltaSetItem deltaItem = resource as IDeltaSetItem;
+
+ if (deltaItem != null)
+ {
+ transientAnnotationContainer = deltaItem.TransientInstanceAnnotationContainer;
+ }
+ }
+
+ if (instanceAnnotationContainer == null && transientAnnotationContainer == null)
+ {
+ return;
+ }
- SetInstanceAnnotations(oDataResource, instanceAnnotationContainer, deserializerProvider, readContext);
+ SetInstanceAnnotations(oDataResource, instanceAnnotationContainer, transientAnnotationContainer, deserializerProvider, readContext);
}
internal static void SetDynamicProperty(object resource, IEdmStructuredTypeReference resourceType,
@@ -140,15 +204,15 @@ internal static void SetDeclaredProperty(object resource, EdmTypeKind propertyKi
}
}
- internal static void SetCollectionProperty(object resource, IEdmProperty edmProperty, object value, string propertyName)
+ internal static void SetCollectionProperty(object resource, IEdmProperty edmProperty, object value, string propertyName, bool isDelta = false)
{
Contract.Assert(edmProperty != null);
- SetCollectionProperty(resource, propertyName, edmProperty.Type.AsCollection(), value, clearCollection: false);
+ SetCollectionProperty(resource, propertyName, edmProperty.Type.AsCollection(), value, clearCollection: false, isDelta);
}
internal static void SetCollectionProperty(object resource, string propertyName,
- IEdmCollectionTypeReference edmPropertyType, object value, bool clearCollection)
+ IEdmCollectionTypeReference edmPropertyType, object value, bool clearCollection, bool isDelta = false)
{
if (value != null)
{
@@ -168,7 +232,7 @@ internal static void SetCollectionProperty(object resource, string propertyName,
IEnumerable newCollection;
if (CanSetProperty(resource, propertyName) &&
- CollectionDeserializationHelpers.TryCreateInstance(propertyType, edmPropertyType, elementType, out newCollection))
+ CollectionDeserializationHelpers.TryCreateInstance(propertyType, edmPropertyType, elementType, out newCollection, isDelta))
{
// settable collections
collection.AddToCollection(newCollection, elementType, resourceType, propertyName, propertyType);
@@ -279,14 +343,21 @@ internal static void SetDynamicProperty(object resource, string propertyName, ob
}
}
- internal static void SetInstanceAnnotations(ODataResource oDataResource, IODataInstanceAnnotationContainer instanceAnnotationContainer,
- ODataDeserializerProvider deserializerProvider, ODataDeserializerContext readContext)
+ internal static void SetInstanceAnnotations(ODataResourceBase oDataResource, IODataInstanceAnnotationContainer instanceAnnotationContainer,
+ IODataInstanceAnnotationContainer transientAnnotationContainer, ODataDeserializerProvider deserializerProvider, ODataDeserializerContext readContext)
{
if(oDataResource.InstanceAnnotations != null)
{
foreach (ODataInstanceAnnotation annotation in oDataResource.InstanceAnnotations)
{
- AddInstanceAnnotationToContainer(instanceAnnotationContainer, deserializerProvider, readContext, annotation,string.Empty);
+ if (!TransientAnnotations.TransientAnnotationTerms.Contains(annotation.Name))
+ {
+ AddInstanceAnnotationToContainer(instanceAnnotationContainer, deserializerProvider, readContext, annotation, string.Empty);
+ }
+ else
+ {
+ AddInstanceAnnotationToContainer(transientAnnotationContainer, deserializerProvider, readContext, annotation, string.Empty);
+ }
}
}
@@ -318,11 +389,12 @@ private static void AddInstanceAnnotationToContainer(IODataInstanceAnnotationCon
instanceAnnotationContainer.AddPropertyAnnotation(propertyName,annotation.Name, annotationValue);
}
}
-
+
public static IODataInstanceAnnotationContainer GetAnnotationContainer(PropertyInfo propertyInfo, object resource)
- {
+ {
+ object value;
IDelta delta = resource as IDelta;
- object value;
+
if (delta != null)
{
delta.TryGetPropertyValue(propertyInfo.Name, out value);
@@ -330,7 +402,7 @@ public static IODataInstanceAnnotationContainer GetAnnotationContainer(PropertyI
else
{
value = propertyInfo.GetValue(resource);
- }
+ }
IODataInstanceAnnotationContainer instanceAnnotationContainer = value as IODataInstanceAnnotationContainer;
@@ -346,8 +418,8 @@ public static IODataInstanceAnnotationContainer GetAnnotationContainer(PropertyI
{
instanceAnnotationContainer = Activator.CreateInstance(propertyInfo.PropertyType) as IODataInstanceAnnotationContainer;
}
-
- if(delta != null)
+
+ if (delta != null)
{
delta.TrySetPropertyValue(propertyInfo.Name, instanceAnnotationContainer);
}
diff --git a/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataDeltaResourceSetWrapper.cs b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataDeltaResourceSetWrapper.cs
new file mode 100644
index 0000000000..d3a53e2771
--- /dev/null
+++ b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataDeltaResourceSetWrapper.cs
@@ -0,0 +1,30 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (c) .NET Foundation and Contributors. All rights reserved.
+// See License.txt in the project root for license information.
+//
+//------------------------------------------------------------------------------
+
+using System.Collections.Generic;
+using Microsoft.OData;
+
+namespace Microsoft.AspNet.OData.Formatter.Deserialization
+{
+ ///
+ /// Encapsulates an and the 's that are part of it.
+ ///
+ public sealed class ODataDeltaResourceSetWrapper : ODataResourceSetWrapperBase
+ {
+ ///
+ /// Initializes a new instance of .
+ ///
+ /// The wrapped item.
+ public ODataDeltaResourceSetWrapper(ODataDeltaResourceSet item)
+ : base(item)
+ {
+
+ }
+
+ internal override ResourceSetType ResourceSetType => ResourceSetType.DeltaResourceSet;
+ }
+}
diff --git a/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataDeserializerContext.cs b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataDeserializerContext.cs
index 75d5506a71..0dabf8e822 100644
--- a/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataDeserializerContext.cs
+++ b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataDeserializerContext.cs
@@ -18,7 +18,11 @@ namespace Microsoft.AspNet.OData.Formatter.Deserialization
public partial class ODataDeserializerContext
{
private bool? _isDeltaOfT;
+ private bool? _isDeletedDeltaOfT;
private bool? _isUntyped;
+ private bool? _isChangedObjectCollection;
+ private bool? _isDeltaEntity;
+ private bool? _isDeltaDeletedEntity;
///
/// Gets or sets the type of the top-level object the request needs to be deserialized into.
@@ -62,20 +66,75 @@ internal bool IsDeltaOfT
{
if (!_isDeltaOfT.HasValue)
{
- _isDeltaOfT = ResourceType != null && TypeHelper.IsGenericType(ResourceType) && ResourceType.GetGenericTypeDefinition() == typeof(Delta<>);
+ _isDeltaOfT = ResourceType != null && TypeHelper.IsGenericType(ResourceType) && (ResourceType.GetGenericTypeDefinition() == typeof(Delta<>) ||
+ ResourceType.GetGenericTypeDefinition() == typeof(DeltaDeletedEntityObject<>));
}
return _isDeltaOfT.Value;
}
}
+ internal bool IsDeletedDeltaOfT
+ {
+ get
+ {
+ if (!_isDeletedDeltaOfT.HasValue)
+ {
+ _isDeletedDeltaOfT = ResourceType != null && TypeHelper.IsGenericType(ResourceType) && (ResourceType.GetGenericTypeDefinition() == typeof(DeltaDeletedEntityObject<>) ||
+ ResourceType.GetGenericTypeDefinition() == typeof(DeltaDeletedEntityObject<>));
+ }
+
+ return _isDeletedDeltaOfT.Value;
+ }
+ }
+
+ internal bool IsDeltaEntity
+ {
+ get
+ {
+ if (!_isDeltaEntity.HasValue)
+ {
+ _isDeltaEntity = ResourceType != null && (ResourceType == typeof(EdmDeltaEntityObject) || ResourceType == typeof(EdmDeltaDeletedEntityObject));
+ }
+
+ return _isDeltaEntity.Value;
+ }
+ }
+
+ internal bool IsDeltaDeletedEntity
+ {
+ get
+ {
+ if (!_isDeltaDeletedEntity.HasValue)
+ {
+ _isDeltaDeletedEntity = ResourceType != null && ResourceType == typeof(EdmDeltaDeletedEntityObject);
+ }
+
+ return _isDeltaDeletedEntity.Value;
+ }
+ }
+
+ internal bool IsChangedObjectCollection
+ {
+ get
+ {
+ if (!_isChangedObjectCollection.HasValue)
+ {
+ _isChangedObjectCollection = ResourceType != null && (ResourceType == typeof(EdmChangedObjectCollection) || (TypeHelper.IsGenericType(ResourceType) &&
+ ResourceType.GetGenericTypeDefinition() == typeof(DeltaSet<>) ));
+ }
+
+ return _isChangedObjectCollection.Value;
+ }
+ }
+
internal bool IsUntyped
{
get
{
if (!_isUntyped.HasValue)
{
- _isUntyped = TypeHelper.IsTypeAssignableFrom(typeof(IEdmObject), ResourceType) ||
+ _isUntyped = IsChangedObjectCollection ? !TypeHelper.IsGenericType(ResourceType) : (TypeHelper.IsTypeAssignableFrom(typeof(IEdmObject), ResourceType) && !IsDeltaOfT) ||
typeof(ODataUntypedActionParameters) == ResourceType;
}
diff --git a/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataEntityReferenceLinkBase.cs b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataEntityReferenceLinkBase.cs
index 1fd669a07c..ebbbf435f9 100644
--- a/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataEntityReferenceLinkBase.cs
+++ b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataEntityReferenceLinkBase.cs
@@ -21,17 +21,13 @@ public class ODataEntityReferenceLinkBase : ODataItemBase
public ODataEntityReferenceLinkBase(ODataEntityReferenceLink item)
: base(item)
{
+ EntityReferenceLink = item;
}
///
/// Gets the wrapped .
///
- public ODataEntityReferenceLink EntityReferenceLink
- {
- get
- {
- return Item as ODataEntityReferenceLink;
- }
- }
+ public ODataEntityReferenceLink EntityReferenceLink { get; }
+
}
}
diff --git a/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataNestedResourceInfoWrapper.cs b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataNestedResourceInfoWrapper.cs
index 3d7a5d5d24..1793290957 100644
--- a/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataNestedResourceInfoWrapper.cs
+++ b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataNestedResourceInfoWrapper.cs
@@ -44,5 +44,6 @@ public ODataNestedResourceInfo NestedResourceInfo
/// Gets the nested items that are part of this nested resource info.
///
public IList NestedItems { get; private set; }
+
}
}
diff --git a/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataReaderExtensions.cs b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataReaderExtensions.cs
index d2cc2b3427..a7d1d069b7 100644
--- a/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataReaderExtensions.cs
+++ b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataReaderExtensions.cs
@@ -89,30 +89,31 @@ private static void ReadCollectionItem(ODataReader reader, Stack
}
else
{
- ODataItemBase parentItem = itemsStack.Peek();
- ODataResourceSetWrapper parentResourceSet = parentItem as ODataResourceSetWrapper;
- if (parentResourceSet != null)
- {
- parentResourceSet.Resources.Add(resourceWrapper);
- }
- else
- {
- ODataNestedResourceInfoWrapper parentNestedResource = (ODataNestedResourceInfoWrapper)parentItem;
- Contract.Assert(parentNestedResource.NestedResourceInfo.IsCollection == false, "Only singleton nested properties can contain resource as their child.");
- Contract.Assert(parentNestedResource.NestedItems.Count == 0, "Each nested property can contain only one resource as its direct child.");
- parentNestedResource.NestedItems.Add(resourceWrapper);
- }
+ AddResourceToParent(itemsStack, resourceWrapper);
}
itemsStack.Push(resourceWrapper);
break;
case ODataReaderState.ResourceEnd:
+ case ODataReaderState.DeletedResourceEnd:
Contract.Assert(
itemsStack.Count > 0 && (reader.Item == null || itemsStack.Peek().Item == reader.Item),
"The resource which is ending should be on the top of the items stack.");
itemsStack.Pop();
break;
+ case ODataReaderState.DeletedResourceStart:
+ ODataDeletedResource deletedResource = (ODataDeletedResource)reader.Item;
+ Contract.Assert(deletedResource != null, "Deleted resource should not be null");
+
+ ODataResourceWrapper deletedResourceWrapper = new ODataResourceWrapper(deletedResource);
+
+ Contract.Assert(itemsStack.Count != 0, "Deleted Resource should not be top level item");
+
+ AddResourceToParent(itemsStack, deletedResourceWrapper);
+
+ itemsStack.Push(deletedResourceWrapper);
+ break;
case ODataReaderState.NestedResourceInfoStart:
ODataNestedResourceInfo nestedResourceInfo = (ODataNestedResourceInfo)reader.Item;
@@ -139,12 +140,10 @@ private static void ReadCollectionItem(ODataReader reader, Stack
Contract.Assert(resourceSet != null, "ResourceSet should never be null.");
ODataResourceSetWrapper resourceSetWrapper = new ODataResourceSetWrapper(resourceSet);
+
if (itemsStack.Count > 0)
{
- ODataNestedResourceInfoWrapper parentNestedResourceInfo = (ODataNestedResourceInfoWrapper)itemsStack.Peek();
- Contract.Assert(parentNestedResourceInfo != null, "this has to be an inner resource set. inner resource sets always have a nested resource info.");
- Contract.Assert(parentNestedResourceInfo.NestedResourceInfo.IsCollection == true, "Only collection nested properties can contain resource set as their child.");
- parentNestedResourceInfo.NestedItems.Add(resourceSetWrapper);
+ AddNestedResourceInfo(itemsStack, resourceSetWrapper);
}
else
{
@@ -155,6 +154,7 @@ private static void ReadCollectionItem(ODataReader reader, Stack
break;
case ODataReaderState.ResourceSetEnd:
+ case ODataReaderState.DeltaResourceSetEnd:
Contract.Assert(itemsStack.Count > 0 && itemsStack.Peek().Item == reader.Item, "The resource set which is ending should be on the top of the items stack.");
itemsStack.Pop();
break;
@@ -171,11 +171,58 @@ private static void ReadCollectionItem(ODataReader reader, Stack
}
break;
+ case ODataReaderState.DeltaResourceSetStart:
+ ODataDeltaResourceSet deltaResourceSet = (ODataDeltaResourceSet)reader.Item;
+ Contract.Assert(deltaResourceSet != null, "ResourceSet should never be null.");
+
+ ODataDeltaResourceSetWrapper deltaResourceSetWrapper = new ODataDeltaResourceSetWrapper(deltaResourceSet);
+
+ if (itemsStack.Count > 0)
+ {
+ AddNestedResourceInfo(itemsStack, deltaResourceSetWrapper);
+ }
+ else
+ {
+ topLevelItem = deltaResourceSetWrapper;
+ }
+
+ itemsStack.Push(deltaResourceSetWrapper);
+ break;
+ case ODataReaderState.DeltaLink:
+ case ODataReaderState.DeltaDeletedLink:
+
+ //Throw error if Delta Link appears
+ throw Error.NotSupported(SRResources.DeltaLinkNotSupported);
+
default:
Contract.Assert(false, "We should never get here, it means the ODataReader reported a wrong state.");
break;
}
}
+
+
+ private static void AddNestedResourceInfo(Stack itemsStack, ODataResourceSetWrapperBase resourceSetWrapper)
+ {
+ ODataNestedResourceInfoWrapper parentNestedResourceInfo = (ODataNestedResourceInfoWrapper)itemsStack.Peek();
+ Contract.Assert(parentNestedResourceInfo != null, "this has to be an inner resource set. inner resource sets always have a nested resource info.");
+ Contract.Assert(parentNestedResourceInfo.NestedResourceInfo.IsCollection == true, "Only collection nested properties can contain resource set as their child.");
+ parentNestedResourceInfo.NestedItems.Add(resourceSetWrapper);
+ }
+
+ private static void AddResourceToParent(Stack itemsStack, ODataResourceWrapper resourceWrapper)
+ {
+ ODataItemBase parentItem = itemsStack.Peek();
+ ODataResourceSetWrapperBase parentResourceSet = parentItem as ODataResourceSetWrapperBase;
+ if (parentResourceSet != null)
+ {
+ parentResourceSet.Resources.Add(resourceWrapper);
+ }
+ else
+ {
+ ODataNestedResourceInfoWrapper parentNestedResource = (ODataNestedResourceInfoWrapper)parentItem;
+ parentNestedResource.NestedItems.Add(resourceWrapper);
+ }
+ }
}
}
diff --git a/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataResourceDeserializer.cs b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataResourceDeserializer.cs
index c75ba4747f..b0384a4d50 100644
--- a/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataResourceDeserializer.cs
+++ b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataResourceDeserializer.cs
@@ -15,15 +15,17 @@
using System.Runtime.Serialization;
using System.Threading.Tasks;
using Microsoft.AspNet.OData.Common;
-using Microsoft.AspNet.OData.Formatter.Serialization;
+using Microsoft.AspNet.OData.Interfaces;
using Microsoft.OData;
using Microsoft.OData.Edm;
+using Microsoft.OData.UriParser;
namespace Microsoft.AspNet.OData.Formatter.Deserialization
{
///
/// Represents an for reading OData resource payloads.
///
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling")]
public class ODataResourceDeserializer : ODataEdmTypeDeserializer
{
///
@@ -87,7 +89,8 @@ public sealed override object ReadInline(object item, IEdmTypeReference edmType,
throw Error.ArgumentNull("item");
}
- if (!edmType.IsStructured())
+ IEdmStructuredTypeReference structuredType = edmType.AsStructured();
+ if (structuredType == null)
{
throw Error.Argument("edmType", SRResources.ArgumentMustBeOfType, "Entity or Complex");
}
@@ -98,10 +101,13 @@ public sealed override object ReadInline(object item, IEdmTypeReference edmType,
throw Error.Argument("item", SRResources.ArgumentMustBeOfType, typeof(ODataResource).Name);
}
+ // Create the appropriate nested context
+ ODataDeserializerContext nestedContext = BuildNestedContextFromCurrentContext(readContext, structuredType.IsEntity() ? ApplyIdToPath(readContext, resourceWrapper) : readContext.Path);
+
// Recursion guard to avoid stack overflows
RuntimeHelpers.EnsureSufficientExecutionStack();
- return ReadResource(resourceWrapper, edmType.AsStructured(), readContext);
+ return ReadResource(resourceWrapper, edmType.AsStructured(), nestedContext);
}
///
@@ -123,8 +129,8 @@ public virtual object ReadResource(ODataResourceWrapper resourceWrapper, IEdmStr
{
throw Error.ArgumentNull("readContext");
}
-
- if (!String.IsNullOrEmpty(resourceWrapper.Resource.TypeName) && structuredType.FullName() != resourceWrapper.Resource.TypeName)
+
+ if (!String.IsNullOrEmpty(resourceWrapper.ResourceBase.TypeName) && structuredType.FullName() != resourceWrapper.ResourceBase.TypeName)
{
// received a derived type in a base type deserializer. delegate it to the appropriate derived type deserializer.
IEdmModel model = readContext.Model;
@@ -134,20 +140,21 @@ public virtual object ReadResource(ODataResourceWrapper resourceWrapper, IEdmStr
throw Error.Argument("readContext", SRResources.ModelMissingFromReadContext);
}
- IEdmStructuredType actualType = model.FindType(resourceWrapper.Resource.TypeName) as IEdmStructuredType;
+ IEdmStructuredType actualType = model.FindType(resourceWrapper.ResourceBase.TypeName) as IEdmStructuredType;
if (actualType == null)
{
- throw new ODataException(Error.Format(SRResources.ResourceTypeNotInModel, resourceWrapper.Resource.TypeName));
+ throw new ODataException(Error.Format(SRResources.ResourceTypeNotInModel, resourceWrapper.ResourceBase.TypeName));
}
if (actualType.IsAbstract)
{
- string message = Error.Format(SRResources.CannotInstantiateAbstractResourceType, resourceWrapper.Resource.TypeName);
+ string message = Error.Format(SRResources.CannotInstantiateAbstractResourceType, resourceWrapper.ResourceBase.TypeName);
throw new ODataException(message);
}
IEdmTypeReference actualStructuredType;
IEdmEntityType actualEntityType = actualType as IEdmEntityType;
+
if (actualEntityType != null)
{
actualStructuredType = new EdmEntityTypeReference(actualEntityType, isNullable: false);
@@ -178,10 +185,19 @@ public virtual object ReadResource(ODataResourceWrapper resourceWrapper, IEdmStr
{
object resource = CreateResourceInstance(structuredType, readContext);
ApplyResourceProperties(resource, resourceWrapper, structuredType, readContext);
+
+ ODataDeletedResource deletedResource = resourceWrapper.ResourceBase as ODataDeletedResource;
+
+ if (deletedResource != null)
+ {
+ AppendDeletedProperties(resource, deletedResource, readContext.IsUntyped);
+ }
+
return resource;
}
}
+
///
/// Creates a new instance of the backing CLR object for the given resource type.
///
@@ -210,6 +226,11 @@ public virtual object CreateResourceInstance(IEdmStructuredTypeReference structu
{
if (structuredType.IsEntity())
{
+ if (readContext.IsDeltaDeletedEntity)
+ {
+ return new EdmDeltaDeletedEntityObject(structuredType.AsEntity());
+ }
+
return new EdmEntityObject(structuredType.AsEntity());
}
@@ -226,20 +247,23 @@ public virtual object CreateResourceInstance(IEdmStructuredTypeReference structu
if (readContext.IsDeltaOfT)
{
- IEnumerable structuralProperties = structuredType.StructuralProperties()
+ IEnumerable structuralProperties = structuredType.StructuredDefinition().Properties()
.Select(edmProperty => EdmLibHelpers.GetClrPropertyName(edmProperty, model));
+ PropertyInfo instanceAnnotationProperty = EdmLibHelpers.GetInstanceAnnotationsContainer(
+ structuredType.StructuredDefinition(), model);
+
if (structuredType.IsOpen())
{
PropertyInfo dynamicDictionaryPropertyInfo = EdmLibHelpers.GetDynamicPropertyDictionary(
structuredType.StructuredDefinition(), model);
return Activator.CreateInstance(readContext.ResourceType, clrType, structuralProperties,
- dynamicDictionaryPropertyInfo, structuredType.IsComplex());
+ dynamicDictionaryPropertyInfo, structuredType.IsComplex(), instanceAnnotationProperty);
}
else
{
- return Activator.CreateInstance(readContext.ResourceType, clrType, structuralProperties, null, structuredType.IsComplex());
+ return Activator.CreateInstance(readContext.ResourceType, clrType, structuralProperties, null, structuredType.IsComplex(), instanceAnnotationProperty);
}
}
else
@@ -249,6 +273,20 @@ public virtual object CreateResourceInstance(IEdmStructuredTypeReference structu
}
}
+ private static void AppendDeletedProperties(dynamic resource, ODataDeletedResource deletedResource, bool isUntyped)
+ {
+ if (isUntyped)
+ {
+ resource.Id = deletedResource.Id.ToString();
+ }
+ else
+ {
+ resource.Id = deletedResource.Id;
+ }
+
+ resource.Reason = deletedResource.Reason.Value;
+ }
+
///
/// Deserializes the nested properties from into .
///
@@ -301,7 +339,41 @@ public virtual void ApplyNestedProperty(object resource, ODataNestedResourceInfo
}
}
- foreach (ODataItemBase childItem in resourceInfoWrapper.NestedItems)
+ IList nestedItems;
+ ODataEntityReferenceLinkBase[] referenceLinks = resourceInfoWrapper.NestedItems.OfType().ToArray();
+ if (referenceLinks.Length > 0)
+ {
+ // Be noted:
+ // 1) OData v4.0, it's "Orders@odata.bind", and we get "ODataEntityReferenceLinkWrapper"(s) for that.
+ // 2) OData v4.01, it's {"odata.id" ...}, and we get "ODataResource"(s) for that.
+ // So, in OData v4, if it's a single, NestedItems contains one ODataEntityReferenceLinkWrapper,
+ // if it's a collection, NestedItems contains multiple ODataEntityReferenceLinkWrapper(s)
+ // We can use the following code to adjust the `ODataEntityReferenceLinkWrapper` to `ODataResourceWrapper`.
+ // In OData v4.01, we will not be here.
+ // Only supports declared property
+ Contract.Assert(edmProperty != null);
+
+ nestedItems = new List();
+ if (edmProperty.Type.IsCollection())
+ {
+ IEdmCollectionTypeReference edmCollectionTypeReference = edmProperty.Type.AsCollection();
+ ODataResourceSetWrapper resourceSetWrapper = CreateResourceSetWrapper(edmCollectionTypeReference, referenceLinks, readContext);
+ nestedItems.Add(resourceSetWrapper);
+ }
+ else
+ {
+ ODataResourceWrapper resourceWrapper = CreateResourceWrapper(edmProperty.Type, referenceLinks[0], readContext);
+ nestedItems.Add(resourceWrapper);
+ }
+ }
+ else
+ {
+ nestedItems = resourceInfoWrapper.NestedItems;
+ }
+
+ ODataDeserializerContext nestedReadContext = GenerateNestedReadContext(resourceInfoWrapper, readContext, edmProperty);
+
+ foreach (ODataItemBase childItem in nestedItems)
{
// it maybe null.
if (childItem == null)
@@ -310,32 +382,25 @@ public virtual void ApplyNestedProperty(object resource, ODataNestedResourceInfo
{
// for the dynamic, OData.net has a bug. see https://github.com/OData/odata.net/issues/977
ApplyDynamicResourceInNestedProperty(resourceInfoWrapper.NestedResourceInfo.Name, resource,
- structuredType, null, readContext);
+ structuredType, null, nestedReadContext);
}
else
{
- ApplyResourceInNestedProperty(edmProperty, resource, null, readContext);
+ ApplyResourceInNestedProperty(edmProperty, resource, null, nestedReadContext);
}
}
- ODataEntityReferenceLinkBase entityReferenceLink = childItem as ODataEntityReferenceLinkBase;
- if (entityReferenceLink != null)
- {
- // ignore entity reference links.
- continue;
- }
-
- ODataResourceSetWrapper resourceSetWrapper = childItem as ODataResourceSetWrapper;
+ ODataResourceSetWrapperBase resourceSetWrapper = childItem as ODataResourceSetWrapperBase;
if (resourceSetWrapper != null)
{
if (edmProperty == null)
{
ApplyDynamicResourceSetInNestedProperty(resourceInfoWrapper.NestedResourceInfo.Name,
- resource, structuredType, resourceSetWrapper, readContext);
+ resource, structuredType, resourceSetWrapper, nestedReadContext);
}
else
{
- ApplyResourceSetInNestedProperty(edmProperty, resource, resourceSetWrapper, readContext);
+ ApplyResourceSetInNestedProperty(edmProperty, resource, resourceSetWrapper, nestedReadContext);
}
continue;
@@ -348,14 +413,361 @@ public virtual void ApplyNestedProperty(object resource, ODataNestedResourceInfo
if (edmProperty == null)
{
ApplyDynamicResourceInNestedProperty(resourceInfoWrapper.NestedResourceInfo.Name, resource,
- structuredType, resourceWrapper, readContext);
+ structuredType, resourceWrapper, nestedReadContext);
+ }
+ else
+ {
+ ApplyResourceInNestedProperty(edmProperty, resource, resourceWrapper, nestedReadContext);
+ }
+ }
+ }
+ }
+
+ private static ODataDeserializerContext GenerateNestedReadContext(ODataNestedResourceInfoWrapper resourceInfoWrapper, ODataDeserializerContext readContext, IEdmProperty edmProperty)
+ {
+ ODataDeserializerContext nestedReadContext = null;
+ Routing.ODataPath path = readContext.Path;
+
+ try
+ {
+
+ // this code attempts to make sure that the path is always correct for the level that we are reading.
+ if (edmProperty == null)
+ {
+ ODataNestedResourceInfo nestedInfo = resourceInfoWrapper.NestedResourceInfo;
+
+ IEdmType segmentType = null;
+ string propertyTypeName = nestedInfo.TypeAnnotation?.TypeName;
+ if (!string.IsNullOrEmpty(propertyTypeName))
+ {
+ segmentType = readContext.Model.FindType(propertyTypeName);
+ }
+
+ // could it be a problem later that the navigationSource is null?
+ DynamicPathSegment pathSegment = new DynamicPathSegment(
+ nestedInfo.Name,
+ segmentType,
+ null,
+ nestedInfo.IsCollection != true
+ );
+
+ path = AppendToPath(path, pathSegment);
+ }
+ else
+ {
+ if (edmProperty.PropertyKind == EdmPropertyKind.Navigation)
+ {
+ Contract.Assert(readContext.Path.NavigationSource != null, "Navigation property segment with null navigationSource");
+ IEdmNavigationProperty navigationProperty = edmProperty as IEdmNavigationProperty;
+ IEdmNavigationSource parentNavigationSource = readContext.Path.NavigationSource;
+ IEdmNavigationSource navigationSource = parentNavigationSource.FindNavigationTarget(navigationProperty);
+
+ if (navigationProperty.ContainsTarget)
+ {
+ path = AppendToPath(path, new NavigationPropertySegment(navigationProperty, navigationSource), navigationProperty.DeclaringType, parentNavigationSource);
+ }
+ else
+ {
+ path = new Routing.ODataPath(new ODataUriParser(readContext.Model, new Uri(navigationSource.Path.Path, UriKind.Relative)).ParsePath());
+ }
}
else
{
- ApplyResourceInNestedProperty(edmProperty, resource, resourceWrapper, readContext);
+ IEdmStructuralProperty structuralProperty = edmProperty as IEdmStructuralProperty;
+ path = AppendToPath(path, new PropertySegment(structuralProperty), structuralProperty.DeclaringType, null);
}
}
+
+ nestedReadContext = BuildNestedContextFromCurrentContext(readContext, path);
+ }
+ catch
+ {
+ nestedReadContext = BuildNestedContextFromCurrentContext(readContext);
+ }
+
+ return nestedReadContext;
+ }
+
+ private static ODataDeserializerContext BuildNestedContextFromCurrentContext(ODataDeserializerContext readContext, Routing.ODataPath path =null, Type resourceType = null)
+ {
+ return new ODataDeserializerContext
+ {
+ Path = path??readContext.Path,
+ Model = readContext.Model,
+ Request = readContext.Request,
+ ResourceType = resourceType??readContext.ResourceType
+ };
+ }
+
+ //Appends a new segment to an ODataPath
+ private static Routing.ODataPath AppendToPath(Routing.ODataPath path, ODataPathSegment segment)
+ {
+ return AppendToPath(path, segment, null, null);
+ }
+
+ //Appends a new segment to an ODataPath, adding a type segment if required
+ private static Routing.ODataPath AppendToPath(Routing.ODataPath path, ODataPathSegment segment, IEdmType declaringType, IEdmNavigationSource navigationSource)
+ {
+ List segments = new List(path.Segments);
+
+ // Append type cast segment if required
+ if (declaringType != null && path.EdmType != declaringType)
+ {
+ segments.Add(new TypeSegment(declaringType, navigationSource));
+ }
+
+ segments.Add(segment);
+
+ return new Routing.ODataPath(segments);
+ }
+
+ private ODataResourceSetWrapper CreateResourceSetWrapper(IEdmCollectionTypeReference edmPropertyType,
+ IList refLinks, ODataDeserializerContext readContext)
+ {
+ ODataResourceSet resourceSet = new ODataResourceSet
+ {
+ TypeName = edmPropertyType.FullName(),
+ };
+
+ IEdmTypeReference elementType = edmPropertyType.ElementType();
+ ODataResourceSetWrapper resourceSetWrapper = new ODataResourceSetWrapper(resourceSet);
+ foreach (ODataEntityReferenceLinkBase refLinkWrapper in refLinks)
+ {
+ ODataResourceWrapper resourceWrapper = CreateResourceWrapper(elementType, refLinkWrapper, readContext);
+ resourceSetWrapper.Resources.Add(resourceWrapper);
+ }
+
+ return resourceSetWrapper;
+ }
+
+ private ODataResourceWrapper CreateResourceWrapper(IEdmTypeReference edmPropertyType, ODataEntityReferenceLinkBase refLink, ODataDeserializerContext readContext)
+ {
+ Contract.Assert(readContext != null);
+
+ ODataResource resource = new ODataResource
+ {
+ TypeName = edmPropertyType.FullName(),
+ };
+
+ resource.Properties = CreateKeyProperties(refLink.EntityReferenceLink.Url, readContext);
+
+ if (refLink.EntityReferenceLink.InstanceAnnotations != null)
+ {
+ foreach (ODataInstanceAnnotation instanceAnnotation in refLink.EntityReferenceLink.InstanceAnnotations)
+ {
+ resource.InstanceAnnotations.Add(instanceAnnotation);
+ };
}
+
+ return new ODataResourceWrapper(resource);
+ }
+
+
+ ///
+ /// Do uri parsing to get the key values.
+ ///
+ /// The key Id.
+ /// The reader context.
+ /// The key properties.
+ private static IList CreateKeyProperties(Uri id, ODataDeserializerContext readContext)
+ {
+ Contract.Assert(id != null);
+ Contract.Assert(readContext != null);
+ IList properties = new List();
+ if (readContext.Request == null)
+ {
+ return properties;
+ }
+
+ ODataPath odataPath = GetODataPath(id.OriginalString, readContext);
+ if (odataPath != null)
+ {
+ KeySegment keySegment = odataPath.OfType().LastOrDefault();
+
+ if (keySegment != null)
+ {
+ foreach (KeyValuePair key in keySegment.Keys)
+ {
+ properties.Add(new ODataProperty
+ {
+ Name = key.Key,
+ Value = key.Value
+ });
+ }
+ }
+ }
+
+ return properties;
+ }
+
+ private static ODataPath GetODataPath(string id, ODataDeserializerContext readContext)
+ {
+ // should we just use ODataUriParser?
+ /*
+ try
+ {
+ return new ODataUriParser(readContext.Model, new Uri(id, UriKind.Relative)).ParsePath();
+ }
+ catch
+ {
+ return null;
+ }
+ */
+
+ try
+ {
+ Routing.IODataPathHandler pathHandler = readContext.InternalRequest.PathHandler;
+ IWebApiRequestMessage internalRequest = readContext.InternalRequest;
+ IWebApiUrlHelper urlHelper = readContext.InternalUrlHelper;
+
+ string serviceRoot = urlHelper.CreateODataLink(
+ internalRequest.Context.RouteName,
+ internalRequest.PathHandler,
+ new List());
+ ODataPath odataPath = pathHandler.Parse(serviceRoot, id, internalRequest.RequestContainer).Path;
+
+ return odataPath;
+ }
+ catch (Exception)
+ {
+ return null;
+ }
+ }
+
+ private static void ApplyODataIDContainer(object resource, ODataResourceWrapper resourceWrapper,
+ ODataDeserializerContext readContext)
+ {
+ Routing.ODataPath path = readContext.Path;
+ if (path == null)
+ {
+ return;
+ }
+
+ IEdmEntityType entityType = path.EdmType.AsElementType() as IEdmEntityType;
+
+ if (entityType != null)
+ {
+ //Setting Odataid , for POCO classes, as a property in the POCO object itself(if user has OdataIDContainer property),
+ //for Delta and EdmEntity object setting as an added property ODataIdcontianer in those classes
+
+ ODataPath ODataPath = new ODataPath(path.Segments);
+
+ // if there is no Id on the resource, try to compute one from path
+ if (resourceWrapper.ResourceBase.Id == null)
+ {
+ ODataUri odataUri = new ODataUri { Path = ODataPath };
+ resourceWrapper.ResourceBase.Id = odataUri.BuildUri(ODataUrlKeyDelimiter.Parentheses);
+ }
+
+ if (resourceWrapper.ResourceBase.Id != null)
+ {
+ string odataId = resourceWrapper.ResourceBase.Id.OriginalString;
+
+ IODataIdContainer container = new ODataIdContainer();
+ container.ODataId = odataId;
+
+ if (resource is EdmEntityObject edmObject)
+ {
+ edmObject.ODataIdContainer = container;
+ edmObject.ODataPath = ODataPath;
+ }
+ else if (resource is IDeltaSetItem deltasetItem)
+ {
+ deltasetItem.ODataIdContainer = container;
+ deltasetItem.ODataPath = ODataPath;
+ }
+ else
+ {
+ PropertyInfo containerPropertyInfo = EdmLibHelpers.GetClrType(entityType, readContext.Model).GetProperties().Where(x => x.PropertyType == typeof(IODataIdContainer)).FirstOrDefault();
+ if (containerPropertyInfo != null)
+ {
+ IODataIdContainer resourceContainer = containerPropertyInfo.GetValue(resource) as IODataIdContainer;
+ if (resourceContainer != null)
+ {
+ containerPropertyInfo.SetValue(resource, resourceContainer);
+ }
+ else
+ {
+ containerPropertyInfo.SetValue(resource, container);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private static Routing.ODataPath ApplyIdToPath(ODataDeserializerContext readContext, ODataResourceWrapper resourceWrapper)
+ {
+ // If an odata.id is provided, try to parse it as an OData Url.
+ // This could fail (as the id is not required to be a valid OData Url)
+ // in which case we fall back to building the path based on the current path and segments.
+ if (resourceWrapper.ResourceBase.Id != null)
+ {
+ try
+ {
+ IWebApiRequestMessage internalRequest = readContext.InternalRequest;
+ IWebApiUrlHelper urlHelper = readContext.InternalUrlHelper;
+
+ string serviceRoot = urlHelper.CreateODataLink(
+ internalRequest.Context.RouteName,
+ internalRequest.PathHandler,
+ new List());
+
+ ODataUriParser parser = new ODataUriParser(readContext.Model, new Uri(serviceRoot), resourceWrapper.ResourceBase.Id);
+
+ ODataPath odataPath = parser.ParsePath();
+ if (odataPath != null)
+ {
+ return new Routing.ODataPath(odataPath);
+ }
+ }
+ catch
+ {
+ };
+ }
+
+ Routing.ODataPath path = readContext.Path;
+
+ if(path == null)
+ {
+ return null;
+ }
+
+ IEdmEntityType entityType = path.EdmType.AsElementType() as IEdmEntityType;
+
+ if (entityType != null && path.EdmType.TypeKind == EdmTypeKind.Collection)
+ {
+ // create the uri for the current object, using path and key values
+ List> keys = new List>();
+ foreach (IEdmStructuralProperty keyProperty in entityType.Key())
+ {
+ string keyName = keyProperty.Name;
+ ODataProperty property = resourceWrapper.ResourceBase.Properties.Where(p => p.Name == keyName).FirstOrDefault();
+ if (property == null && !readContext.DisableCaseInsensitiveRequestPropertyBinding)
+ {
+ //try case insensitive
+ List candidates = resourceWrapper.ResourceBase.Properties.Where(p => String.Equals(p.Name, keyName, StringComparison.InvariantCultureIgnoreCase)).ToList();
+ property = candidates.Count == 1 ? candidates.First() : null;
+ }
+
+ object keyValue = property?.Value;
+ if (keyValue == null)
+ {
+ // 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.
+ // Consider adding tests to ODL to ensure we don't validate key property type in future.
+ keyValue = "Null";
+ }
+
+ keys.Add(new KeyValuePair(keyName, keyValue));
+ }
+
+ KeySegment keySegment = new KeySegment(keys, entityType, path.NavigationSource);
+ return AppendToPath(path, keySegment);
+ }
+
+ return path;
}
///
@@ -373,7 +785,7 @@ public virtual void ApplyStructuralProperties(object resource, ODataResourceWrap
throw Error.ArgumentNull("resourceWrapper");
}
- foreach (ODataProperty property in resourceWrapper.Resource.Properties)
+ foreach (ODataProperty property in resourceWrapper.ResourceBase.Properties)
{
ApplyStructuralProperty(resource, property, structuredType, readContext);
}
@@ -394,7 +806,7 @@ public virtual void ApplyInstanceAnnotations(object resource, ODataResourceWrapp
throw Error.ArgumentNull("resourceWrapper");
}
- DeserializationHelpers.ApplyInstanceAnnotations(resource, structuredType, resourceWrapper.Resource,DeserializerProvider, readContext);
+ DeserializationHelpers.ApplyInstanceAnnotations(resource, structuredType, resourceWrapper.ResourceBase, DeserializerProvider, readContext);
}
///
@@ -406,7 +818,7 @@ public virtual void ApplyInstanceAnnotations(object resource, ODataResourceWrapp
/// The deserializer context.
public virtual void ApplyStructuralProperty(object resource, ODataProperty structuralProperty,
IEdmStructuredTypeReference structuredType, ODataDeserializerContext readContext)
- {
+ {
if (resource == null)
{
throw Error.ArgumentNull("resource");
@@ -426,6 +838,7 @@ private void ApplyResourceProperties(object resource, ODataResourceWrapper resou
ApplyStructuralProperties(resource, resourceWrapper, structuredType, readContext);
ApplyNestedProperties(resource, resourceWrapper, structuredType, readContext);
ApplyInstanceAnnotations(resource, resourceWrapper, structuredType, readContext);
+ ApplyODataIDContainer(resource, resourceWrapper, readContext);
}
private void ApplyResourceInNestedProperty(IEdmProperty nestedProperty, object resource,
@@ -435,17 +848,6 @@ private void ApplyResourceInNestedProperty(IEdmProperty nestedProperty, object r
Contract.Assert(resource != null);
Contract.Assert(readContext != null);
- if (readContext.IsDeltaOfT)
- {
- IEdmNavigationProperty navigationProperty = nestedProperty as IEdmNavigationProperty;
- if (navigationProperty != null)
- {
- string message = Error.Format(SRResources.CannotPatchNavigationProperties, navigationProperty.Name,
- navigationProperty.DeclaringEntityType().FullName());
- throw new ODataException(message);
- }
- }
-
object value = ReadNestedResourceInline(resourceWrapper, nestedProperty.Type, readContext);
// First resolve Data member alias or annotation, then set the regular
@@ -464,7 +866,7 @@ private void ApplyDynamicResourceInNestedProperty(string propertyName, object re
object value = null;
if (resourceWrapper != null)
{
- IEdmSchemaType elementType = readContext.Model.FindDeclaredType(resourceWrapper.Resource.TypeName);
+ IEdmSchemaType elementType = readContext.Model.FindDeclaredType(resourceWrapper.ResourceBase.TypeName);
IEdmTypeReference edmTypeReference = elementType.ToEdmTypeReference(true);
value = ReadNestedResourceInline(resourceWrapper, edmTypeReference, readContext);
@@ -492,18 +894,11 @@ private object ReadNestedResourceInline(ODataResourceWrapper resourceWrapper, IE
IEdmStructuredTypeReference structuredType = edmType.AsStructured();
- var nestedReadContext = new ODataDeserializerContext
- {
- Path = readContext.Path,
- Model = readContext.Model,
- Request = readContext.Request,
- };
-
- Type clrType = null;
+ Type clrType;
if (readContext.IsUntyped)
{
clrType = structuredType.IsEntity()
- ? typeof(EdmEntityObject)
+ ? (readContext.IsDeltaEntity ? (readContext.IsDeltaDeletedEntity ? typeof(EdmDeltaDeletedEntityObject) : typeof(EdmDeltaEntityObject)) : typeof(EdmEntityObject))
: typeof(EdmComplexObject);
}
else
@@ -517,50 +912,40 @@ private object ReadNestedResourceInline(ODataResourceWrapper resourceWrapper, IE
}
}
- nestedReadContext.ResourceType = readContext.IsDeltaOfT
+ ODataDeserializerContext nestedContext = BuildNestedContextFromCurrentContext(readContext, null, readContext.IsDeltaOfT
? typeof(Delta<>).MakeGenericType(clrType)
- : clrType;
- return deserializer.ReadInline(resourceWrapper, edmType, nestedReadContext);
+ : clrType);
+
+ return deserializer.ReadInline(resourceWrapper, edmType, nestedContext);
}
private void ApplyResourceSetInNestedProperty(IEdmProperty nestedProperty, object resource,
- ODataResourceSetWrapper resourceSetWrapper, ODataDeserializerContext readContext)
+ ODataResourceSetWrapperBase resourceSetWrapper, ODataDeserializerContext readContext)
{
Contract.Assert(nestedProperty != null);
Contract.Assert(resource != null);
Contract.Assert(readContext != null);
- if (readContext.IsDeltaOfT)
- {
- IEdmNavigationProperty navigationProperty = nestedProperty as IEdmNavigationProperty;
- if (navigationProperty != null)
- {
- string message = Error.Format(SRResources.CannotPatchNavigationProperties, navigationProperty.Name,
- navigationProperty.DeclaringEntityType().FullName());
- throw new ODataException(message);
- }
- }
-
object value = ReadNestedResourceSetInline(resourceSetWrapper, nestedProperty.Type, readContext);
string propertyName = EdmLibHelpers.GetClrPropertyName(nestedProperty, readContext.Model);
- DeserializationHelpers.SetCollectionProperty(resource, nestedProperty, value, propertyName);
+ DeserializationHelpers.SetCollectionProperty(resource, nestedProperty, value, propertyName, resourceSetWrapper.ResourceSetType == ResourceSetType.DeltaResourceSet);
}
private void ApplyDynamicResourceSetInNestedProperty(string propertyName, object resource, IEdmStructuredTypeReference structuredType,
- ODataResourceSetWrapper resourceSetWrapper, ODataDeserializerContext readContext)
+ ODataResourceSetWrapperBase resourceSetWrapper, ODataDeserializerContext readContext)
{
Contract.Assert(resource != null);
Contract.Assert(readContext != null);
- if (String.IsNullOrEmpty(resourceSetWrapper.ResourceSet.TypeName))
+ if (String.IsNullOrEmpty(resourceSetWrapper.ResourceSetBase.TypeName))
{
string message = Error.Format(SRResources.DynamicResourceSetTypeNameIsRequired, propertyName);
throw new ODataException(message);
}
string elementTypeName =
- DeserializationHelpers.GetCollectionElementTypeName(resourceSetWrapper.ResourceSet.TypeName,
+ DeserializationHelpers.GetCollectionElementTypeName(resourceSetWrapper.ResourceSetBase.TypeName,
isNested: false);
IEdmSchemaType elementType = readContext.Model.FindDeclaredType(elementTypeName);
@@ -587,12 +972,12 @@ private void ApplyDynamicResourceSetInNestedProperty(string propertyName, object
result, collectionType, readContext.Model);
}
- private object ReadNestedResourceSetInline(ODataResourceSetWrapper resourceSetWrapper, IEdmTypeReference edmType,
- ODataDeserializerContext readContext)
+ private object ReadNestedResourceSetInline(ODataResourceSetWrapperBase resourceSetWrapper, IEdmTypeReference edmType,
+ ODataDeserializerContext nestedReadContext)
{
Contract.Assert(resourceSetWrapper != null);
Contract.Assert(edmType != null);
- Contract.Assert(readContext != null);
+ Contract.Assert(nestedReadContext != null);
ODataEdmTypeDeserializer deserializer = DeserializerProvider.GetEdmTypeDeserializer(edmType);
if (deserializer == null)
@@ -601,18 +986,13 @@ private object ReadNestedResourceSetInline(ODataResourceSetWrapper resourceSetWr
}
IEdmStructuredTypeReference structuredType = edmType.AsCollection().ElementType().AsStructured();
- var nestedReadContext = new ODataDeserializerContext
- {
- Path = readContext.Path,
- Model = readContext.Model,
- Request = readContext.Request,
- };
- if (readContext.IsUntyped)
+ if (nestedReadContext.IsUntyped)
{
if (structuredType.IsEntity())
{
- nestedReadContext.ResourceType = typeof(EdmEntityObjectCollection);
+ nestedReadContext.ResourceType = (nestedReadContext.IsDeltaOfT && resourceSetWrapper.ResourceSetType == ResourceSetType.DeltaResourceSet) ?
+ typeof(EdmChangedObjectCollection) : typeof(EdmEntityObjectCollection);
}
else
{
@@ -621,7 +1001,7 @@ private object ReadNestedResourceSetInline(ODataResourceSetWrapper resourceSetWr
}
else
{
- Type clrType = EdmLibHelpers.GetClrType(structuredType, readContext.Model);
+ Type clrType = EdmLibHelpers.GetClrType(structuredType, nestedReadContext.Model);
if (clrType == null)
{
@@ -629,7 +1009,8 @@ private object ReadNestedResourceSetInline(ODataResourceSetWrapper resourceSetWr
Error.Format(SRResources.MappingDoesNotContainResourceType, structuredType.FullName()));
}
- nestedReadContext.ResourceType = typeof(List<>).MakeGenericType(clrType);
+ nestedReadContext.ResourceType = (nestedReadContext.IsDeltaOfT && resourceSetWrapper.ResourceSetType == ResourceSetType.DeltaResourceSet)
+ ? typeof(DeltaSet<>).MakeGenericType(clrType) : typeof(List<>).MakeGenericType(clrType);
}
return deserializer.ReadInline(resourceSetWrapper, edmType, nestedReadContext);
@@ -673,4 +1054,4 @@ private static IEdmNavigationSource GetNavigationSource(IEdmStructuredTypeRefere
return navigationSource;
}
}
-}
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataResourceSetDeserializer.cs b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataResourceSetDeserializer.cs
index 1fe34b9bc0..27aa8972e8 100644
--- a/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataResourceSetDeserializer.cs
+++ b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ODataResourceSetDeserializer.cs
@@ -7,6 +7,8 @@
using System;
using System.Collections;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Reflection;
@@ -52,7 +54,7 @@ public override object Read(ODataMessageReader messageReader, Type type, ODataDe
throw Error.Argument("edmType", SRResources.ArgumentMustBeOfType, EdmTypeKind.Complex + " or " + EdmTypeKind.Entity);
}
- ODataReader resourceSetReader = messageReader.CreateODataResourceSetReader();
+ ODataReader resourceSetReader = readContext.IsChangedObjectCollection ? messageReader.CreateODataDeltaResourceSetReader() : messageReader.CreateODataResourceSetReader();
object resourceSet = resourceSetReader.ReadResourceOrResourceSet();
return ReadInline(resourceSet, edmType, readContext);
}
@@ -74,12 +76,13 @@ public override async Task
public sealed class ODataResourceWrapper : ODataItemBase
- {
+ {
///
/// Initializes a new instance of .
///
/// The wrapped item.
- public ODataResourceWrapper(ODataResource item)
+ public ODataResourceWrapper(ODataResourceBase item)
: base(item)
{
NestedResourceInfos = new List();
+ ResourceBase = item;
}
+
+ ///
+ /// Gets the wrapped .
+ ///
+ public ODataResourceBase ResourceBase {get;}
///
- /// Gets the wrapped .
+ /// Gets the wrapped . This will return null for deleted resources.
///
+ [Obsolete("Please use ResourceBase instead")]
public ODataResource Resource
{
get
diff --git a/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ResourceSetType.cs b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ResourceSetType.cs
new file mode 100644
index 0000000000..450d946f86
--- /dev/null
+++ b/src/Microsoft.AspNet.OData.Shared/Formatter/Deserialization/ResourceSetType.cs
@@ -0,0 +1,25 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (c) .NET Foundation and Contributors. All rights reserved.
+// See License.txt in the project root for license information.
+//
+//------------------------------------------------------------------------------
+
+namespace Microsoft.AspNet.OData.Formatter.Deserialization
+{
+ ///
+ /// Enum to determine the type of Resource Set
+ ///
+ internal enum ResourceSetType
+ {
+ ///
+ /// A normal ResourceSet
+ ///
+ ResourceSet,
+
+ ///
+ /// A Delta Resource Set
+ ///
+ DeltaResourceSet
+ }
+}
diff --git a/src/Microsoft.AspNet.OData.Shared/Formatter/EdmLibHelpers.cs b/src/Microsoft.AspNet.OData.Shared/Formatter/EdmLibHelpers.cs
index fb3f83110f..cdeb02d13a 100644
--- a/src/Microsoft.AspNet.OData.Shared/Formatter/EdmLibHelpers.cs
+++ b/src/Microsoft.AspNet.OData.Shared/Formatter/EdmLibHelpers.cs
@@ -6,6 +6,7 @@
//------------------------------------------------------------------------------
using System;
+using System.Collections;
using System.Collections.Generic;
#if NETFX // System.Data.Linq.Binary is only supported in the AspNet version.
using System.Data.Linq;
@@ -134,13 +135,23 @@ private static IEdmType GetEdmType(IEdmModel edmModel, Type clrType, bool testCo
{
if (testCollections)
{
+ Type entityType;
+
+ if (IsDeltaSetWrapper(clrType, out entityType))
+ {
+ IEdmType elementType = GetEdmType(edmModel, entityType, testCollections: false);
+ if (elementType != null)
+ {
+ return new EdmCollectionType(elementType.ToEdmTypeReference(IsNullable(entityType)));
+ }
+ }
+
Type enumerableOfT = ExtractGenericInterface(clrType, typeof(IEnumerable<>));
if (enumerableOfT != null)
{
Type elementClrType = enumerableOfT.GetGenericArguments()[0];
- // IEnumerable> is a collection of T.
- Type entityType;
+ // IEnumerable> is a collection of T.
if (IsSelectExpandWrapper(elementClrType, out entityType))
{
elementClrType = entityType;
@@ -977,14 +988,17 @@ internal static IEdmTypeReference GetExpectedPayloadType(Type type, ODataPath pa
{
IEdmTypeReference expectedPayloadType = null;
- if (typeof(IEdmObject).IsAssignableFrom(type))
+ if (typeof(IEdmObject).IsAssignableFrom(type) || typeof(IDeltaSet).IsAssignableFrom(type))
{
// typeless mode. figure out the expected payload type from the OData Path.
IEdmType edmType = path.EdmType;
if (edmType != null)
{
expectedPayloadType = EdmLibHelpers.ToEdmTypeReference(edmType, isNullable: false);
- if (expectedPayloadType.TypeKind() == EdmTypeKind.Collection)
+
+ //This loop should execute only if its not a type of edmchangedobjectcollection, In case of edmchangedobjectcollection,
+ //Expectedpayloadtype should not be of elementytype, but of collection.
+ if (expectedPayloadType.TypeKind() == EdmTypeKind.Collection && !(typeof(ICollection).IsAssignableFrom(type) || typeof(IDeltaSet).IsAssignableFrom(type)))
{
IEdmTypeReference elementType = expectedPayloadType.AsCollection().ElementType();
if (elementType.IsEntity())
@@ -1117,6 +1131,7 @@ private static IEdmPrimitiveType GetPrimitiveType(EdmPrimitiveTypeKind primitive
}
private static bool IsSelectExpandWrapper(Type type, out Type entityType) => IsTypeWrapper(typeof(SelectExpandWrapper<>), type, out entityType);
+ private static bool IsDeltaSetWrapper(Type type, out Type entityType) => IsTypeWrapper(typeof(DeltaSet<>), type, out entityType);
internal static bool IsComputeWrapper(Type type, out Type entityType) => IsTypeWrapper(typeof(ComputeWrapper<>), type, out entityType);
diff --git a/src/Microsoft.AspNet.OData.Shared/Formatter/ODataInputFormatterHelper.cs b/src/Microsoft.AspNet.OData.Shared/Formatter/ODataInputFormatterHelper.cs
index 8ecd8ed6ce..334056d1c6 100644
--- a/src/Microsoft.AspNet.OData.Shared/Formatter/ODataInputFormatterHelper.cs
+++ b/src/Microsoft.AspNet.OData.Shared/Formatter/ODataInputFormatterHelper.cs
@@ -80,8 +80,9 @@ internal static object ReadFromStream(
oDataReaderSettings.BaseUri = baseAddress;
oDataReaderSettings.Validations = oDataReaderSettings.Validations & ~ValidationKinds.ThrowOnUndeclaredPropertyForNonOpenType;
oDataReaderSettings.Version = version;
+ oDataReaderSettings.MaxProtocolVersion = version;
- IODataRequestMessage oDataRequestMessage = getODataRequestMessage();
+ IODataRequestMessage oDataRequestMessage = getODataRequestMessage();
string preferHeader = RequestPreferenceHelpers.GetRequestPreferHeader(internalRequest.Headers);
string annotationFilter = null;
diff --git a/src/Microsoft.AspNet.OData.Shared/Formatter/ODataModelBinderConverter.cs b/src/Microsoft.AspNet.OData.Shared/Formatter/ODataModelBinderConverter.cs
index ac70bbd32d..f7eea7088c 100644
--- a/src/Microsoft.AspNet.OData.Shared/Formatter/ODataModelBinderConverter.cs
+++ b/src/Microsoft.AspNet.OData.Shared/Formatter/ODataModelBinderConverter.cs
@@ -337,7 +337,7 @@ private static object ConvertResource(ODataMessageReader oDataMessageReader, IEd
if (edmTypeReference.IsEntity())
{
IEdmEntityTypeReference entityType = edmTypeReference.AsEntity();
- return CovertResourceId(value, topLevelResource.Resource, entityType, readContext);
+ return CovertResourceId(value, topLevelResource.ResourceBase, entityType, readContext);
}
return value;
@@ -350,14 +350,14 @@ private static IEnumerable CovertResourceSetIds(IEnumerable sources, ODataResour
int i = 0;
foreach (object item in sources)
{
- object newItem = CovertResourceId(item, resourceSet.Resources[i].Resource, entityTypeReference,
+ object newItem = CovertResourceId(item, resourceSet.Resources[i].ResourceBase, entityTypeReference,
readContext);
i++;
yield return newItem;
}
}
- private static object CovertResourceId(object source, ODataResource resource,
+ private static object CovertResourceId(object source, ODataResourceBase resource,
IEdmEntityTypeReference entityTypeReference, ODataDeserializerContext readContext)
{
Contract.Assert(resource != null);
diff --git a/src/Microsoft.AspNet.OData.Shared/Formatter/ODataOutputFormatterHelper.cs b/src/Microsoft.AspNet.OData.Shared/Formatter/ODataOutputFormatterHelper.cs
index d59728abe4..eca447f439 100644
--- a/src/Microsoft.AspNet.OData.Shared/Formatter/ODataOutputFormatterHelper.cs
+++ b/src/Microsoft.AspNet.OData.Shared/Formatter/ODataOutputFormatterHelper.cs
@@ -95,7 +95,7 @@ internal static bool CanWriteType(
ODataPayloadKind? payloadKind;
Type elementType;
- if (typeof(IEdmObject).IsAssignableFrom(type) ||
+ if (typeof(IDeltaSet).IsAssignableFrom(type) || typeof(IEdmObject).IsAssignableFrom(type) ||
(TypeHelper.IsCollection(type, out elementType) && typeof(IEdmObject).IsAssignableFrom(elementType)))
{
payloadKind = GetEdmObjectPayloadKind(type, internalRequest);
@@ -156,7 +156,16 @@ internal static void WriteToStream(
ODataMessageWriterSettings writerSettings = internalRequest.WriterSettings;
writerSettings.BaseUri = baseAddress;
- writerSettings.Version = version;
+
+ if (serializer.ODataPayloadKind == ODataPayloadKind.Delta)
+ {
+ writerSettings.Version = ODataVersion.V401;
+ }
+ else
+ {
+ writerSettings.Version = version;
+ }
+
writerSettings.Validations = writerSettings.Validations & ~ValidationKinds.ThrowOnUndeclaredPropertyForNonOpenType;
string metadataLink = internaUrlHelper.CreateODataLink(MetadataSegment.Instance);
@@ -207,6 +216,7 @@ internal static void WriteToStream(
writeContext.Path = path;
writeContext.MetadataLevel = metadataLevel;
writeContext.QueryOptions = internalRequest.Context.QueryOptions;
+ writeContext.Type = type;
//Set the SelectExpandClause on the context if it was explicitly specified.
if (selectExpandDifferentFromQueryOptions != null)
@@ -251,7 +261,7 @@ internal static void WriteToStream(
{
return ODataPayloadKind.ResourceSet;
}
- else if (typeof(IEdmChangedObject).IsAssignableFrom(elementType))
+ else if (typeof(IDeltaSetItem).IsAssignableFrom(elementType) || typeof(IEdmChangedObject).IsAssignableFrom(elementType))
{
return ODataPayloadKind.Delta;
}
diff --git a/src/Microsoft.AspNet.OData.Shared/Formatter/Serialization/DefaultODataSerializerProvider.cs b/src/Microsoft.AspNet.OData.Shared/Formatter/Serialization/DefaultODataSerializerProvider.cs
index fcefb7c6fd..f814563e25 100644
--- a/src/Microsoft.AspNet.OData.Shared/Formatter/Serialization/DefaultODataSerializerProvider.cs
+++ b/src/Microsoft.AspNet.OData.Shared/Formatter/Serialization/DefaultODataSerializerProvider.cs
@@ -112,6 +112,10 @@ internal ODataSerializer GetODataPayloadSerializerImpl(Type type, Func();
}
+ else if(TypeHelper.IsTypeAssignableFrom(typeof(IDeltaSet), type))
+ {
+ return _rootContainer.GetRequiredService();
+ }
// Get the model. Using a Func to delay evaluation of the model
// until after the above checks have passed.
diff --git a/src/Microsoft.AspNet.OData.Shared/Formatter/Serialization/ODataDeltaFeedSerializer.cs b/src/Microsoft.AspNet.OData.Shared/Formatter/Serialization/ODataDeltaFeedSerializer.cs
index d041abeb41..e0e0aafb9e 100644
--- a/src/Microsoft.AspNet.OData.Shared/Formatter/Serialization/ODataDeltaFeedSerializer.cs
+++ b/src/Microsoft.AspNet.OData.Shared/Formatter/Serialization/ODataDeltaFeedSerializer.cs
@@ -6,8 +6,11 @@
//------------------------------------------------------------------------------
using System;
+using System.CodeDom;
using System.Collections;
+using System.Collections.Generic;
using System.Diagnostics.Contracts;
+using System.Reflection;
using System.Runtime.Serialization;
using System.Threading.Tasks;
using Microsoft.AspNet.OData.Builder;
@@ -25,6 +28,7 @@ namespace Microsoft.AspNet.OData.Formatter.Serialization
public class ODataDeltaFeedSerializer : ODataEdmTypeSerializer
{
private const string DeltaFeed = "deltafeed";
+ IEdmStructuredTypeReference _elementType;
///
/// Initializes a new instance of .
@@ -60,6 +64,7 @@ public override void WriteObject(object graph, Type type, ODataMessageWriter mes
}
IEdmTypeReference feedType = writeContext.GetEdmType(graph, type);
+
Contract.Assert(feedType != null);
IEdmEntityTypeReference entityType = GetResourceType(feedType).AsEntity();
@@ -93,6 +98,7 @@ public override async Task WriteObjectAsync(object graph, Type type, ODataMessag
}
IEdmTypeReference feedType = writeContext.GetEdmType(graph, type);
+
Contract.Assert(feedType != null);
IEdmEntityTypeReference entityType = GetResourceType(feedType).AsEntity();
@@ -186,6 +192,7 @@ private void WriteFeed(IEnumerable enumerable, IEdmTypeReference feedType, OData
Contract.Assert(feedType != null);
IEdmStructuredTypeReference elementType = GetResourceType(feedType);
+ _elementType = elementType;
if (elementType.IsComplex())
{
@@ -234,13 +241,31 @@ private void WriteFeed(IEnumerable enumerable, IEdmTypeReference feedType, OData
}
lastResource = entry;
- IEdmChangedObject edmChangedObject = entry as IEdmChangedObject;
- if (edmChangedObject == null)
+
+ EdmDeltaEntityKind deltaEntityKind;
+ if (writeContext.IsUntyped)
{
- throw new SerializationException(Error.Format(SRResources.CannotWriteType, GetType().Name, enumerable.GetType().FullName));
+ IEdmChangedObject edmChangedObject = entry as IEdmChangedObject;
+ if (edmChangedObject == null)
+ {
+ throw new SerializationException(Error.Format(SRResources.CannotWriteType, GetType().Name, enumerable.GetType().FullName));
+ }
+
+ deltaEntityKind = edmChangedObject.DeltaKind;
}
+ else
+ {
+ IDeltaSetItem deltaSetItem = entry as IDeltaSetItem;
- switch (edmChangedObject.DeltaKind)
+ if (deltaSetItem == null)
+ {
+ throw new SerializationException(Error.Format(SRResources.CannotWriteType, GetType().Name, enumerable.GetType().FullName));
+ }
+
+ deltaEntityKind = deltaSetItem.DeltaKind;
+ }
+
+ switch (deltaEntityKind)
{
case EdmDeltaEntityKind.DeletedEntry:
WriteDeltaDeletedEntry(entry, writer, writeContext);
@@ -254,6 +279,7 @@ private void WriteFeed(IEnumerable enumerable, IEdmTypeReference feedType, OData
case EdmDeltaEntityKind.Entry:
{
ODataResourceSerializer entrySerializer = SerializerProvider.GetEdmTypeSerializer(elementType) as ODataResourceSerializer;
+
if (entrySerializer == null)
{
throw new SerializationException(
@@ -289,6 +315,7 @@ private async Task WriteFeedAsync(IEnumerable enumerable, IEdmTypeReference feed
Contract.Assert(feedType != null);
IEdmStructuredTypeReference elementType = GetResourceType(feedType);
+ _elementType = elementType;
if (elementType.IsComplex())
{
@@ -337,13 +364,32 @@ private async Task WriteFeedAsync(IEnumerable enumerable, IEdmTypeReference feed
}
lastResource = entry;
- IEdmChangedObject edmChangedObject = entry as IEdmChangedObject;
- if (edmChangedObject == null)
+
+ EdmDeltaEntityKind deltaEntityKind;
+ if (writeContext.IsUntyped)
+ {
+ IEdmChangedObject edmChangedObject = entry as IEdmChangedObject;
+ if (edmChangedObject == null)
+ {
+ throw new SerializationException(Error.Format(SRResources.CannotWriteType, GetType().Name, enumerable.GetType().FullName));
+ }
+
+ deltaEntityKind = edmChangedObject.DeltaKind;
+ }
+ else
{
- throw new SerializationException(Error.Format(SRResources.CannotWriteType, GetType().Name, enumerable.GetType().FullName));
+ IDeltaSetItem deltaSetItem = entry as IDeltaSetItem;
+
+ if (deltaSetItem == null)
+ {
+ throw new SerializationException(Error.Format(SRResources.CannotWriteType, GetType().Name, enumerable.GetType().FullName));
+ }
+
+ deltaEntityKind = deltaSetItem.DeltaKind;
}
+
- switch (edmChangedObject.DeltaKind)
+ switch (deltaEntityKind)
{
case EdmDeltaEntityKind.DeletedEntry:
await WriteDeltaDeletedEntryAsync(entry, writer, writeContext);
@@ -438,15 +484,23 @@ public virtual ODataDeltaResourceSet CreateODataDeltaFeed(IEnumerable feedInstan
///
/// The object to be written.
/// The to be used for writing.
- /// The .
+ /// The .
public virtual void WriteDeltaDeletedEntry(object graph, ODataWriter writer, ODataSerializerContext writeContext)
{
- ODataDeletedResource deletedResource = GetDeletedResource(graph);
-
- if (deletedResource != null)
+ ODataResourceSerializer serializer = SerializerProvider.GetEdmTypeSerializer(_elementType) as ODataResourceSerializer;
+ ResourceContext resourceContext = serializer.GetResourceContext(graph, writeContext);
+ SelectExpandNode selectExpandNode = serializer.CreateSelectExpandNode(resourceContext);
+
+ if (selectExpandNode != null)
{
- writer.WriteStart(deletedResource);
- writer.WriteEnd();
+ ODataDeletedResource deletedResource = GetDeletedResource(graph, resourceContext, serializer, selectExpandNode, writeContext.IsUntyped);
+
+ if (deletedResource != null)
+ {
+ writer.WriteStart(deletedResource);
+ serializer.WriteDeltaComplexProperties(selectExpandNode, resourceContext, writer);
+ writer.WriteEnd();
+ }
}
}
@@ -456,14 +510,22 @@ public virtual void WriteDeltaDeletedEntry(object graph, ODataWriter writer, ODa
///
/// The object to be written.
/// The to be used for writing.
- /// The .
+ /// The .
public virtual async Task WriteDeltaDeletedEntryAsync(object graph, ODataWriter writer, ODataSerializerContext writeContext)
{
- ODataDeletedResource deletedResource = GetDeletedResource(graph);
- if (deletedResource != null)
+ ODataResourceSerializer serializer = SerializerProvider.GetEdmTypeSerializer(_elementType) as ODataResourceSerializer;
+ ResourceContext resourceContext = serializer.GetResourceContext(graph, writeContext);
+ SelectExpandNode selectExpandNode = serializer.CreateSelectExpandNode(resourceContext);
+
+ if (selectExpandNode != null)
{
- await writer.WriteStartAsync(deletedResource);
- await writer.WriteEndAsync();
+ ODataDeletedResource deletedResource = GetDeletedResource(graph, resourceContext, serializer, selectExpandNode, writeContext.IsUntyped);
+
+ if (deletedResource != null)
+ {
+ await writer.WriteStartAsync(deletedResource);
+ await writer.WriteEndAsync();
+ }
}
}
@@ -473,7 +535,7 @@ public virtual async Task WriteDeltaDeletedEntryAsync(object graph, ODataWriter
///
/// The object to be written.
/// The to be used for writing.
- /// The .
+ /// The .
public virtual void WriteDeltaDeletedLink(object graph, ODataWriter writer, ODataSerializerContext writeContext)
{
ODataDeltaDeletedLink deltaDeletedLink = GetDeletedLink(graph);
@@ -489,7 +551,7 @@ public virtual void WriteDeltaDeletedLink(object graph, ODataWriter writer, ODat
///
/// The object to be written.
/// The to be used for writing.
- /// The .
+ /// The .
public virtual async Task WriteDeltaDeletedLinkAsync(object graph, ODataWriter writer, ODataSerializerContext writeContext)
{
ODataDeltaDeletedLink deltaDeletedLink = GetDeletedLink(graph);
@@ -505,7 +567,7 @@ public virtual async Task WriteDeltaDeletedLinkAsync(object graph, ODataWriter w
///
/// The object to be written.
/// The to be used for writing.
- /// The .
+ /// The .
public virtual void WriteDeltaLink(object graph, ODataWriter writer, ODataSerializerContext writeContext)
{
ODataDeltaLink deltaLink = GetDeltaLink(graph);
@@ -521,7 +583,7 @@ public virtual void WriteDeltaLink(object graph, ODataWriter writer, ODataSerial
///
/// The object to be written.
/// The to be used for writing.
- /// The .
+ /// The .
public async Task WriteDeltaLinkAsync(object graph, ODataWriter writer, ODataSerializerContext writeContext)
{
ODataDeltaLink deltaLink = GetDeltaLink(graph);
@@ -531,26 +593,46 @@ public async Task WriteDeltaLinkAsync(object graph, ODataWriter writer, ODataSer
}
}
- private ODataDeletedResource GetDeletedResource(object graph)
+
+ private ODataDeletedResource GetDeletedResource(object graph, ResourceContext resourceContext, ODataResourceSerializer serializer, SelectExpandNode selectExpandNode, bool isUntyped)
{
- EdmDeltaDeletedEntityObject edmDeltaDeletedEntity = graph as EdmDeltaDeletedEntityObject;
- if (edmDeltaDeletedEntity == null)
+ IEdmNavigationSource navigationSource;
+ ODataDeletedResource deletedResource = serializer.CreateDeletedResource(selectExpandNode, resourceContext);
+
+ if (isUntyped)
{
- throw new SerializationException(Error.Format(SRResources.CannotWriteType, GetType().Name, graph.GetType().FullName));
- }
+ EdmDeltaDeletedEntityObject edmDeltaDeletedEntity = graph as EdmDeltaDeletedEntityObject;
+ if (edmDeltaDeletedEntity == null)
+ {
+ throw new SerializationException(Error.Format(SRResources.CannotWriteType, GetType().Name, graph.GetType().FullName));
+ }
- Uri id = StringToUri(edmDeltaDeletedEntity.Id);
- ODataDeletedResource deletedResource = new ODataDeletedResource(id, edmDeltaDeletedEntity.Reason);
+ deletedResource.Id = StringToUri(edmDeltaDeletedEntity.Id??string.Empty);
+ deletedResource.Reason = edmDeltaDeletedEntity.Reason;
+ navigationSource = edmDeltaDeletedEntity.NavigationSource;
+ }
+ else
+ {
+ IDeltaDeletedEntityObject deltaDeletedEntity = graph as IDeltaDeletedEntityObject;
+ if (deltaDeletedEntity == null)
+ {
+ throw new SerializationException(Error.Format(SRResources.CannotWriteType, GetType().Name, graph.GetType().FullName));
+ }
- if (edmDeltaDeletedEntity.NavigationSource != null)
+ deletedResource.Id = deltaDeletedEntity.Id;
+ deletedResource.Reason = deltaDeletedEntity.Reason;
+ navigationSource = deltaDeletedEntity.NavigationSource;
+ }
+
+ if (navigationSource != null)
{
ODataResourceSerializationInfo serializationInfo = new ODataResourceSerializationInfo
{
- NavigationSourceName = edmDeltaDeletedEntity.NavigationSource.Name
+ NavigationSourceName = navigationSource.Name
};
deletedResource.SetSerializationInfo(serializationInfo);
}
-
+
return deletedResource;
}
diff --git a/src/Microsoft.AspNet.OData.Shared/Formatter/Serialization/ODataResourceSerializer.cs b/src/Microsoft.AspNet.OData.Shared/Formatter/Serialization/ODataResourceSerializer.cs
index 8d4c2c5a92..3d41c1a261 100644
--- a/src/Microsoft.AspNet.OData.Shared/Formatter/Serialization/ODataResourceSerializer.cs
+++ b/src/Microsoft.AspNet.OData.Shared/Formatter/Serialization/ODataResourceSerializer.cs
@@ -8,6 +8,7 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Contracts;
using System.Linq;
@@ -202,12 +203,13 @@ private void WriteDeltaResource(object graph, ODataWriter writer, ODataSerialize
{
writer.WriteStart(resource);
WriteDeltaComplexProperties(selectExpandNode, resourceContext, writer);
+ WriteDeltaNavigationProperties(selectExpandNode, resourceContext, writer);
//TODO: Need to add support to write Navigation Links, etc. using Delta Writer
//https://github.com/OData/odata.net/issues/155
//CLEANUP: merge delta logic with regular logic; requires common base between ODataWriter and ODataDeltaWriter
//WriteDynamicComplexProperties(resourceContext, writer);
//WriteNavigationLinks(selectExpandNode.SelectedNavigationProperties, resourceContext, writer);
- //WriteExpandedNavigationProperties(selectExpandNode.ExpandedNavigationProperties, resourceContext, writer);
+ //WriteExpandedNavigationProperties(selectExpandNode, resourceContext, writer);
writer.WriteEnd();
}
@@ -226,6 +228,7 @@ private async Task WriteDeltaResourceAsync(object graph, ODataWriter writer, ODa
{
await writer.WriteStartAsync(resource);
await WriteDeltaComplexPropertiesAsync(selectExpandNode, resourceContext, writer);
+ await WriteDeltaNavigationPropertiesAsync(selectExpandNode, resourceContext, writer);
//TODO: Need to add support to write Navigation Links, etc. using Delta Writer
//https://github.com/OData/odata.net/issues/155
//CLEANUP: merge delta logic with regular logic; requires common base between ODataWriter and ODataDeltaWriter
@@ -238,7 +241,7 @@ private async Task WriteDeltaResourceAsync(object graph, ODataWriter writer, ODa
}
}
- private ResourceContext GetResourceContext(object graph, ODataSerializerContext writeContext)
+ internal ResourceContext GetResourceContext(object graph, ODataSerializerContext writeContext)
{
Contract.Assert(writeContext != null);
@@ -253,7 +256,7 @@ private ResourceContext GetResourceContext(object graph, ODataSerializerContext
return resourceContext;
}
- private void WriteDeltaComplexProperties(SelectExpandNode selectExpandNode,
+ internal void WriteDeltaComplexProperties(SelectExpandNode selectExpandNode,
ResourceContext resourceContext, ODataWriter writer)
{
Contract.Assert(resourceContext != null);
@@ -275,6 +278,48 @@ private void WriteDeltaComplexProperties(SelectExpandNode selectExpandNode,
}
}
+ internal void WriteDeltaNavigationProperties(SelectExpandNode selectExpandNode, ResourceContext resourceContext, ODataWriter writer)
+ {
+ Contract.Assert(resourceContext != null);
+ Contract.Assert(writer != null);
+
+ IEnumerable> navigationProperties = GetNavigationPropertiesToWrite(selectExpandNode, resourceContext);
+
+ foreach (KeyValuePair navigationProperty in navigationProperties)
+ {
+ ODataNestedResourceInfo nestedResourceInfo = new ODataNestedResourceInfo
+ {
+ IsCollection = navigationProperty.Key.Type.IsCollection(),
+ Name = navigationProperty.Key.Name
+ };
+
+ writer.WriteStart(nestedResourceInfo);
+ WriteDeltaComplexAndExpandedNavigationProperty(navigationProperty.Key, null, resourceContext, writer, navigationProperty.Value);
+ writer.WriteEnd();
+ }
+ }
+
+ internal async Task WriteDeltaNavigationPropertiesAsync(SelectExpandNode selectExpandNode, ResourceContext resourceContext, ODataWriter writer)
+ {
+ Contract.Assert(resourceContext != null);
+ Contract.Assert(writer != null);
+
+ IEnumerable> navigationProperties = GetNavigationPropertiesToWrite(selectExpandNode, resourceContext);
+
+ foreach (KeyValuePair navigationProperty in navigationProperties)
+ {
+ ODataNestedResourceInfo nestedResourceInfo = new ODataNestedResourceInfo
+ {
+ IsCollection = navigationProperty.Key.Type.IsCollection(),
+ Name = navigationProperty.Key.Name
+ };
+
+ await writer.WriteStartAsync(nestedResourceInfo);
+ await WriteDeltaComplexAndExpandedNavigationPropertyAsync(navigationProperty.Key, null, resourceContext, writer, navigationProperty.Value);
+ await writer.WriteEndAsync();
+ }
+ }
+
private async Task WriteDeltaComplexPropertiesAsync(SelectExpandNode selectExpandNode,
ResourceContext resourceContext, ODataWriter writer)
{
@@ -298,7 +343,7 @@ private async Task WriteDeltaComplexPropertiesAsync(SelectExpandNode selectExpan
}
private void WriteDeltaComplexAndExpandedNavigationProperty(IEdmProperty edmProperty, SelectExpandClause selectExpandClause,
- ResourceContext resourceContext, ODataWriter writer)
+ ResourceContext resourceContext, ODataWriter writer, Type type = null)
{
Contract.Assert(edmProperty != null);
Contract.Assert(resourceContext != null);
@@ -331,6 +376,7 @@ private void WriteDeltaComplexAndExpandedNavigationProperty(IEdmProperty edmProp
{
// create the serializer context for the complex and expanded item.
ODataSerializerContext nestedWriteContext = new ODataSerializerContext(resourceContext, selectExpandClause, edmProperty);
+ nestedWriteContext.Type = type;
// write object.
@@ -355,7 +401,7 @@ private void WriteDeltaComplexAndExpandedNavigationProperty(IEdmProperty edmProp
}
private async Task WriteDeltaComplexAndExpandedNavigationPropertyAsync(IEdmProperty edmProperty, SelectExpandClause selectExpandClause,
- ResourceContext resourceContext, ODataWriter writer)
+ ResourceContext resourceContext, ODataWriter writer, Type type = null)
{
Contract.Assert(edmProperty != null);
Contract.Assert(resourceContext != null);
@@ -388,6 +434,7 @@ await writer.WriteStartAsync(new ODataResourceSet
{
// create the serializer context for the complex and expanded item.
ODataSerializerContext nestedWriteContext = new ODataSerializerContext(resourceContext, selectExpandClause, edmProperty);
+ nestedWriteContext.Type = type;
// write object.
@@ -601,6 +648,7 @@ private void WriteResource(object graph, ODataWriter writer, ODataSerializerCont
WriteDynamicComplexProperties(resourceContext, writer);
WriteNavigationLinks(selectExpandNode, resourceContext, writer);
WriteExpandedNavigationProperties(selectExpandNode, resourceContext, writer);
+ WriteNestedNavigationProperties(selectExpandNode, resourceContext, writer);
WriteReferencedNavigationProperties(selectExpandNode, resourceContext, writer);
writer.WriteEnd();
}
@@ -643,6 +691,7 @@ await writer.WriteEntityReferenceLinkAsync(new ODataEntityReferenceLink
await WriteDynamicComplexPropertiesAsync(resourceContext, writer);
await WriteNavigationLinksAsync(selectExpandNode, resourceContext, writer);
await WriteExpandedNavigationPropertiesAsync(selectExpandNode, resourceContext, writer);
+ await WriteNestedNavigationPropertiesAsync(selectExpandNode, resourceContext, writer);
await WriteReferencedNavigationPropertiesAsync(selectExpandNode, resourceContext, writer);
await writer.WriteEndAsync();
}
@@ -688,6 +737,14 @@ public virtual SelectExpandNode CreateSelectExpandNode(ResourceContext resourceC
/// The created .
public virtual ODataResource CreateResource(SelectExpandNode selectExpandNode, ResourceContext resourceContext)
{
+ ODataResource resource = CreateResourceBase(selectExpandNode, resourceContext, false) as ODataResource;
+ return resource;
+ }
+
+
+ private ODataResourceBase CreateResourceBase(SelectExpandNode selectExpandNode, ResourceContext resourceContext, bool isDeletedResource)
+ {
+
if (selectExpandNode == null)
{
throw Error.ArgumentNull("selectExpandNode");
@@ -700,6 +757,14 @@ public virtual ODataResource CreateResource(SelectExpandNode selectExpandNode, R
if (resourceContext.SerializerContext.ExpandReference)
{
+ if (isDeletedResource)
+ {
+ return new ODataDeletedResource
+ {
+ Id = resourceContext.GenerateSelfLink(false)
+ };
+ }
+
return new ODataResource
{
Id = resourceContext.GenerateSelfLink(false)
@@ -707,11 +772,25 @@ public virtual ODataResource CreateResource(SelectExpandNode selectExpandNode, R
}
string typeName = resourceContext.StructuredType.FullTypeName();
- ODataResource resource = new ODataResource
+ ODataResourceBase resource;
+
+ if (isDeletedResource)
{
- TypeName = typeName,
- Properties = CreateStructuralPropertyBag(selectExpandNode, resourceContext),
- };
+ resource = new ODataDeletedResource
+ {
+ TypeName = typeName,
+ Properties = CreateStructuralPropertyBag(selectExpandNode, resourceContext),
+ };
+ }
+ else
+ {
+ resource = new ODataResource
+ {
+ TypeName = typeName,
+ Properties = CreateStructuralPropertyBag(selectExpandNode, resourceContext),
+ };
+ }
+
if (resourceContext.EdmObject is EdmDeltaEntityObject && resourceContext.NavigationSource != null)
{
@@ -760,7 +839,7 @@ public virtual ODataResource CreateResource(SelectExpandNode selectExpandNode, R
AddTypeNameAnnotationAsNeeded(resource, pathType, resourceContext.SerializerContext.MetadataLevel);
}
- if (resourceContext.StructuredType.TypeKind == EdmTypeKind.Entity && resourceContext.NavigationSource != null)
+ if (!isDeletedResource && resourceContext.StructuredType.TypeKind == EdmTypeKind.Entity && resourceContext.NavigationSource != null)
{
if (!(resourceContext.NavigationSource is IEdmContainedEntitySet))
{
@@ -794,6 +873,19 @@ public virtual ODataResource CreateResource(SelectExpandNode selectExpandNode, R
return resource;
}
+ ///
+ /// Creates the to be written while writing this resource.
+ ///
+ /// The describing the response graph.
+ /// The context for the resource instance being written.
+ /// The created .
+ public virtual ODataDeletedResource CreateDeletedResource(SelectExpandNode selectExpandNode, ResourceContext resourceContext)
+ {
+ ODataDeletedResource resource = CreateResourceBase(selectExpandNode, resourceContext, true) as ODataDeletedResource;
+ return resource;
+ }
+
+
///
/// Appends the dynamic properties of primitive, enum or the collection of them into the given .
/// If the dynamic property is a property of the complex or collection of complex, it will be saved into
@@ -804,7 +896,7 @@ public virtual ODataResource CreateResource(SelectExpandNode selectExpandNode, R
/// The context for the resource instance being written.
[SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Justification = "Relies on many classes.")]
[SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Justification = "These are simple conversion function and cannot be split up.")]
- public virtual void AppendDynamicProperties(ODataResource resource, SelectExpandNode selectExpandNode,
+ public virtual void AppendDynamicProperties(ODataResourceBase resource, SelectExpandNode selectExpandNode,
ResourceContext resourceContext)
{
Contract.Assert(resource != null);
@@ -931,102 +1023,60 @@ public virtual void AppendDynamicProperties(ODataResource resource, SelectExpand
///
/// The describing the resource, which is being annotated.
/// The context for the resource instance, which is being annotated.
- public virtual void AppendInstanceAnnotations(ODataResource resource, ResourceContext resourceContext)
+ public virtual void AppendInstanceAnnotations(ODataResourceBase resource, ResourceContext resourceContext)
{
IEdmStructuredType structuredType = resourceContext.StructuredType;
IEdmStructuredObject structuredObject = resourceContext.EdmObject;
+
+ //For appending transient and persistent instance annotations for both enity object and normal resources
+
PropertyInfo instanceAnnotationInfo = EdmLibHelpers.GetInstanceAnnotationsContainer(structuredType,
resourceContext.EdmModel);
- object value;
-
- if (instanceAnnotationInfo == null || structuredObject == null ||
- !structuredObject.TryGetPropertyValue(instanceAnnotationInfo.Name, out value) || value == null)
- {
- return;
- }
+ EdmEntityObject edmEntityObject = null;
+ object instanceAnnotations = null;
+ IODataInstanceAnnotationContainer transientAnnotations = null;
- IODataInstanceAnnotationContainer instanceAnnotationContainer = value as IODataInstanceAnnotationContainer;
+ IDelta delta = null;
- if (instanceAnnotationContainer != null)
+ if (resourceContext.SerializerContext.IsDeltaOfT)
{
- IDictionary clrAnnotations = instanceAnnotationContainer.GetResourceAnnotations();
-
- if (clrAnnotations != null)
- {
- foreach (KeyValuePair annotation in clrAnnotations)
- {
- AddODataAnnotations(resource.InstanceAnnotations, resourceContext, annotation);
- }
- }
-
- foreach(ODataProperty property in resource.Properties)
- {
- string propertyName = property.Name;
-
- if (property.InstanceAnnotations == null)
- {
- property.InstanceAnnotations = new List();
- }
-
- IDictionary propertyAnnotations = instanceAnnotationContainer.GetPropertyAnnotations(propertyName);
-
- if (propertyAnnotations != null)
- {
- foreach (KeyValuePair annotation in propertyAnnotations)
- {
- AddODataAnnotations(property.InstanceAnnotations, resourceContext, annotation);
- }
- }
- }
+ delta = resourceContext.ResourceInstance as IDelta;
}
- }
- private void AddODataAnnotations(ICollection InstanceAnnotations, ResourceContext resourceContext, KeyValuePair annotation)
- {
- ODataValue annotationValue = null;
-
- if (annotation.Value != null)
+ if (delta != null)
{
- IEdmTypeReference edmTypeReference = resourceContext.SerializerContext.GetEdmType(annotation.Value,
- annotation.Value.GetType());
+ if (instanceAnnotationInfo != null)
+ {
+ delta.TryGetPropertyValue(instanceAnnotationInfo.Name, out instanceAnnotations);
- ODataEdmTypeSerializer edmTypeSerializer = GetEdmTypeSerializer(edmTypeReference);
+ }
- if (edmTypeSerializer != null)
+ IDeltaSetItem deltaitem = resourceContext.ResourceInstance as IDeltaSetItem;
+
+ if(deltaitem != null)
{
- annotationValue = edmTypeSerializer.CreateODataValue(annotation.Value, edmTypeReference, resourceContext.SerializerContext);
+ transientAnnotations = deltaitem.TransientInstanceAnnotationContainer;
}
}
else
{
- annotationValue = new ODataNullValue();
- }
-
- if (annotationValue != null)
- {
- InstanceAnnotations.Add(new ODataInstanceAnnotation(annotation.Key, annotationValue));
- }
- }
+ if (instanceAnnotationInfo == null || structuredObject == null ||
+ !structuredObject.TryGetPropertyValue(instanceAnnotationInfo.Name, out instanceAnnotations) || instanceAnnotations == null)
+ {
+ edmEntityObject = structuredObject as EdmEntityObject;
- private ODataEdmTypeSerializer GetEdmTypeSerializer(IEdmTypeReference edmTypeReference)
- {
- ODataEdmTypeSerializer edmTypeSerializer;
-
- if (edmTypeReference.IsCollection())
- {
- edmTypeSerializer = new ODataCollectionSerializer(SerializerProvider, true);
- }
- else if (edmTypeReference.IsStructured())
- {
- edmTypeSerializer = new ODataResourceValueSerializer(SerializerProvider);
- }
- else
- {
- edmTypeSerializer = SerializerProvider.GetEdmTypeSerializer(edmTypeReference);
+ if (edmEntityObject != null)
+ {
+ instanceAnnotations = edmEntityObject.PersistentInstanceAnnotationsContainer;
+ transientAnnotations = edmEntityObject.TransientInstanceAnnotationContainer;
+ }
+ }
}
- return edmTypeSerializer;
+ ODataSerializerHelper.AppendInstanceAnnotations(resource, resourceContext, instanceAnnotations, SerializerProvider);
+
+ ODataSerializerHelper.AppendInstanceAnnotations(resource, resourceContext, transientAnnotations, SerializerProvider);
}
///
@@ -1071,7 +1121,7 @@ private void WriteNavigationLinks(SelectExpandNode selectExpandNode, ResourceCon
Contract.Assert(selectExpandNode != null);
Contract.Assert(resourceContext != null);
- if (selectExpandNode.SelectedNavigationProperties == null)
+ if (selectExpandNode.SelectedNavigationProperties == null || resourceContext.IsPostRequest)
{
return;
}
@@ -1092,7 +1142,7 @@ private async Task WriteNavigationLinksAsync(SelectExpandNode selectExpandNode,
Contract.Assert(selectExpandNode != null);
Contract.Assert(resourceContext != null);
- if (selectExpandNode.SelectedNavigationProperties == null)
+ if (selectExpandNode.SelectedNavigationProperties == null || resourceContext.IsPostRequest)
{
return;
}
@@ -1329,7 +1379,8 @@ private IEnumerable> GetPro
if (complexProperties != null)
{
IEnumerable changedProperties = null;
- if (null != resourceContext.EdmObject && resourceContext.EdmObject.IsDeltaResource())
+
+ if (resourceContext.EdmObject != null && resourceContext.EdmObject.IsDeltaResource())
{
IDelta deltaObject = resourceContext.EdmObject as IDelta;
changedProperties = deltaObject.GetChangedPropertyNames();
@@ -1345,6 +1396,73 @@ private IEnumerable> GetPro
}
}
+ private IEnumerable> GetNavigationPropertiesToWrite(SelectExpandNode selectExpandNode, ResourceContext resourceContext)
+ {
+ ISet navigationProperties = selectExpandNode.SelectedNavigationProperties;
+
+ if (navigationProperties == null)
+ {
+ yield break;
+ }
+
+ if (resourceContext.EdmObject is IDelta changedObject)
+ {
+ IEnumerable changedProperties = changedObject.GetChangedPropertyNames();
+
+ foreach (IEdmNavigationProperty navigationProperty in navigationProperties)
+ {
+ if (changedProperties != null && changedProperties.Contains(navigationProperty.Name))
+ {
+ yield return new KeyValuePair(navigationProperty, typeof(IEdmChangedObject));
+ }
+ }
+ }
+ else if (resourceContext.ResourceInstance is IDelta deltaObject)
+ {
+ IEnumerable changedProperties = deltaObject.GetChangedPropertyNames();
+ dynamic delta = deltaObject;
+
+ foreach (IEdmNavigationProperty navigationProperty in navigationProperties)
+ {
+ object obj = null;
+
+ if (changedProperties != null && changedProperties.Contains(navigationProperty.Name) && delta.DeltaNestedResources.TryGetValue(navigationProperty.Name, out obj))
+ {
+ if (obj != null)
+ {
+ yield return new KeyValuePair(navigationProperty, obj.GetType());
+ }
+ }
+ }
+ }
+ // Serializing nested navigation properties from a deep insert request.
+ // We currently don't deserialize Deep insert nested resources as Delta but as T. If this was to change in the future, logic in this method will have to change.
+ else if (resourceContext.IsPostRequest)
+ {
+ object instance = resourceContext.ResourceInstance;
+ PropertyInfo[] properties = instance.GetType().GetProperties();
+ Dictionary propertyNamesAndValues = new Dictionary();
+
+ foreach (PropertyInfo propertyInfo in properties)
+ {
+ string name = propertyInfo.Name;
+ object value = propertyInfo.GetValue(instance);
+ propertyNamesAndValues.Add(name, value);
+ }
+
+ foreach (IEdmNavigationProperty navigationProperty in navigationProperties)
+ {
+ if (propertyNamesAndValues.TryGetValue(navigationProperty.Name, out object obj))
+ {
+ if (obj != null)
+ {
+ yield return new KeyValuePair(navigationProperty, obj.GetType());
+ }
+ }
+ }
+ }
+ }
+
private void WriteExpandedNavigationProperties(SelectExpandNode selectExpandNode, ResourceContext resourceContext, ODataWriter writer)
{
Contract.Assert(resourceContext != null);
@@ -1498,7 +1616,7 @@ private async Task WriteComplexAndExpandedNavigationPropertyAsync(IEdmProperty e
object propertyValue = resourceContext.GetPropertyValue(edmProperty.Name);
- if (propertyValue == null || propertyValue is NullEdmComplexObject)
+ if (propertyValue == null || propertyValue is NullEdmComplexObject || propertyValue is ODataIdContainer)
{
if (edmProperty.Type.IsCollection())
{
@@ -1535,6 +1653,58 @@ await writer.WriteStartAsync(new ODataResourceSet
}
}
+ private void WriteNestedNavigationProperties(SelectExpandNode selectExpandNode, ResourceContext resourceContext, ODataWriter writer)
+ {
+ Debug.Assert(resourceContext != null, "resourceContext != null");
+ Debug.Assert(writer != null, "writer != null");
+
+ if (!resourceContext.IsPostRequest)
+ {
+ return;
+ }
+
+ IEnumerable> navigationProperties = GetNavigationPropertiesToWrite(selectExpandNode, resourceContext);
+
+ foreach (KeyValuePair navigationProperty in navigationProperties)
+ {
+ ODataNestedResourceInfo nestedResourceInfo = new ODataNestedResourceInfo
+ {
+ IsCollection = navigationProperty.Key.Type.IsCollection(),
+ Name = navigationProperty.Key.Name
+ };
+
+ writer.WriteStart(nestedResourceInfo);
+ WriteComplexAndExpandedNavigationProperty(navigationProperty.Key, null, resourceContext, writer);
+ writer.WriteEnd();
+ }
+ }
+
+ private async Task WriteNestedNavigationPropertiesAsync(SelectExpandNode selectExpandNode, ResourceContext resourceContext, ODataWriter writer)
+ {
+ Debug.Assert(resourceContext != null, "resourceContext != null");
+ Debug.Assert(writer != null, "writer != null");
+
+ if (!resourceContext.IsPostRequest)
+ {
+ return;
+ }
+
+ IEnumerable> navigationProperties = GetNavigationPropertiesToWrite(selectExpandNode, resourceContext);
+
+ foreach (KeyValuePair navigationProperty in navigationProperties)
+ {
+ ODataNestedResourceInfo nestedResourceInfo = new ODataNestedResourceInfo
+ {
+ IsCollection = navigationProperty.Key.Type.IsCollection(),
+ Name = navigationProperty.Key.Name
+ };
+
+ await writer.WriteStartAsync(nestedResourceInfo);
+ await WriteComplexAndExpandedNavigationPropertyAsync(navigationProperty.Key, null, resourceContext, writer);
+ await writer.WriteEndAsync();
+ }
+ }
+
private IEnumerable CreateNavigationLinks(
IEnumerable navigationProperties, ResourceContext resourceContext)
{
@@ -1658,13 +1828,15 @@ private IEnumerable CreateStructuralPropertyBag(SelectExpandNode
{
IEnumerable structuralProperties = selectExpandNode.SelectedStructuralProperties;
- if (null != resourceContext.EdmObject && resourceContext.EdmObject.IsDeltaResource())
+ if (resourceContext.EdmObject != null && resourceContext.EdmObject.IsDeltaResource())
{
IDelta deltaObject = resourceContext.EdmObject as IDelta;
IEnumerable changedProperties = deltaObject.GetChangedPropertyNames();
structuralProperties = structuralProperties.Where(p => changedProperties.Contains(p.Name));
}
+ bool isDeletedEntity = resourceContext.EdmObject is EdmDeltaDeletedEntityObject;
+
foreach (IEdmStructuralProperty structuralProperty in structuralProperties)
{
if (structuralProperty.Type != null && structuralProperty.Type.IsStream())
@@ -1674,11 +1846,13 @@ private IEnumerable CreateStructuralPropertyBag(SelectExpandNode
}
ODataProperty property = CreateStructuralProperty(structuralProperty, resourceContext);
- if (property != null)
+ if (property == null || (isDeletedEntity && property.Value == null) )
{
- properties.Add(property);
+ continue;
}
- }
+
+ properties.Add(property);
+ }
}
return properties;
@@ -1996,7 +2170,7 @@ private static IEdmStructuredType GetODataPathType(ODataSerializerContext serial
}
}
- internal static void AddTypeNameAnnotationAsNeeded(ODataResource resource, IEdmStructuredType odataPathType,
+ internal static void AddTypeNameAnnotationAsNeeded(ODataResourceBase resource, IEdmStructuredType odataPathType,
ODataMetadataLevel metadataLevel)
{
// ODataLib normally has the caller decide whether or not to serialize properties by leaving properties
@@ -2021,7 +2195,7 @@ internal static void AddTypeNameAnnotationAsNeeded(ODataResource resource, IEdmS
resource.TypeAnnotation = new ODataTypeAnnotation(typeName);
}
- internal static void AddTypeNameAnnotationAsNeededForComplex(ODataResource resource, ODataMetadataLevel metadataLevel)
+ internal static void AddTypeNameAnnotationAsNeededForComplex(ODataResourceBase resource, ODataMetadataLevel metadataLevel)
{
// ODataLib normally has the caller decide whether or not to serialize properties by leaving properties
// null when values should not be serialized. The TypeName property is different and should always be
@@ -2095,7 +2269,7 @@ internal static bool ShouldOmitOperation(IEdmOperation operation, OperationLinkB
}
}
- internal static bool ShouldSuppressTypeNameSerialization(ODataResource resource, IEdmStructuredType edmType,
+ internal static bool ShouldSuppressTypeNameSerialization(ODataResourceBase resource, IEdmStructuredType edmType,
ODataMetadataLevel metadataLevel)
{
Contract.Assert(resource != null);
diff --git a/src/Microsoft.AspNet.OData.Shared/Formatter/Serialization/ODataSerializerContext.cs b/src/Microsoft.AspNet.OData.Shared/Formatter/Serialization/ODataSerializerContext.cs
index f72ee955b8..bde531d415 100644
--- a/src/Microsoft.AspNet.OData.Shared/Formatter/Serialization/ODataSerializerContext.cs
+++ b/src/Microsoft.AspNet.OData.Shared/Formatter/Serialization/ODataSerializerContext.cs
@@ -27,6 +27,8 @@ public partial class ODataSerializerContext
private ODataQueryContext _queryContext;
private SelectExpandClause _selectExpandClause;
private bool _isSelectExpandClauseSet;
+ private bool? _isUntyped;
+ private bool? _isDeltaOfT;
///
/// Initializes a new instance of the class.
@@ -163,6 +165,34 @@ internal ODataQueryContext QueryContext
///
public ODataPath Path { get; set; }
+ internal Type Type { get; set; }
+
+ internal bool IsUntyped
+ {
+ get
+ { if (_isUntyped == null)
+ {
+ _isUntyped = typeof(IEdmObject).IsAssignableFrom(Type) || typeof(EdmChangedObjectCollection).IsAssignableFrom(Type);
+ }
+
+ return _isUntyped.Value;
+ }
+ }
+
+ internal bool IsDeltaOfT
+ {
+ get
+ {
+ if (_isDeltaOfT == null)
+ {
+ _isDeltaOfT = Type != null && TypeHelper.IsGenericType(Type) && (Type.GetGenericTypeDefinition() == typeof(DeltaSet<>) ||
+ Type.GetGenericTypeDefinition() == typeof(Delta<>) || Type.GetGenericTypeDefinition() == typeof(DeltaDeletedEntityObject<>));
+ }
+
+ return _isDeltaOfT.Value;
+ }
+ }
+
///
/// Gets or sets the root element name which is used when writing primitive and enum types
///
@@ -300,6 +330,11 @@ internal IEdmTypeReference GetEdmType(object instance, Type type)
}
else
{
+ if (typeof(IDeltaSet).IsAssignableFrom(type))
+ {
+ return EdmLibHelpers.ToEdmTypeReference(Path.EdmType, isNullable: false);
+ }
+
if (Model == null)
{
throw Error.InvalidOperation(SRResources.RequestMustHaveModel);
@@ -312,7 +347,15 @@ internal IEdmTypeReference GetEdmType(object instance, Type type)
{
if (instance != null)
{
- edmType = _typeMappingCache.GetEdmType(instance.GetType(), Model);
+ TypedDelta delta = instance as TypedDelta;
+ if (delta != null)
+ {
+ edmType = _typeMappingCache.GetEdmType(delta.ExpectedClrType, Model);
+ }
+ else
+ {
+ edmType = _typeMappingCache.GetEdmType(instance.GetType(), Model);
+ }
}
if (edmType == null)
diff --git a/src/Microsoft.AspNet.OData.Shared/Formatter/Serialization/ODataSerializerHelper.cs b/src/Microsoft.AspNet.OData.Shared/Formatter/Serialization/ODataSerializerHelper.cs
new file mode 100644
index 0000000000..4c3cdecf7b
--- /dev/null
+++ b/src/Microsoft.AspNet.OData.Shared/Formatter/Serialization/ODataSerializerHelper.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 System;
+using System.Collections.Generic;
+using Microsoft.AspNet.OData.Builder;
+using Microsoft.OData;
+using Microsoft.OData.Edm;
+
+namespace Microsoft.AspNet.OData.Formatter.Serialization
+{
+ ///
+ /// Helper class for OData Serialization
+ ///
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1053:StaticHolderTypesShouldNotHaveConstructors")]
+ internal class ODataSerializerHelper
+ {
+ internal static void AppendInstanceAnnotations(ODataResourceBase resource, ResourceContext resourceContext, object value, ODataSerializerProvider SerializerProvider)
+ {
+ IODataInstanceAnnotationContainer instanceAnnotationContainer = value as IODataInstanceAnnotationContainer;
+
+ if (instanceAnnotationContainer != null)
+ {
+ IDictionary clrAnnotations = instanceAnnotationContainer.GetResourceAnnotations();
+
+ if (clrAnnotations != null)
+ {
+ foreach (KeyValuePair annotation in clrAnnotations)
+ {
+ AddODataAnnotations(resource.InstanceAnnotations, resourceContext, annotation, SerializerProvider);
+ }
+ }
+
+ if (resource.Properties != null)
+ {
+ foreach (ODataProperty property in resource.Properties)
+ {
+ string propertyName = property.Name;
+
+ if (property.InstanceAnnotations == null)
+ {
+ property.InstanceAnnotations = new List();
+ }
+
+ IDictionary propertyAnnotations = instanceAnnotationContainer.GetPropertyAnnotations(propertyName);
+
+ if (propertyAnnotations != null)
+ {
+ foreach (KeyValuePair annotation in propertyAnnotations)
+ {
+ AddODataAnnotations(property.InstanceAnnotations, resourceContext, annotation, SerializerProvider);
+ }
+ }
+ }
+ }
+ }
+ }
+
+
+ internal static void AddODataAnnotations(ICollection InstanceAnnotations, ResourceContext resourceContext, KeyValuePair annotation, ODataSerializerProvider SerializerProvider)
+ {
+ ODataValue annotationValue = null;
+
+ if (annotation.Value != null)
+ {
+ IEdmTypeReference edmTypeReference = resourceContext.SerializerContext.GetEdmType(annotation.Value,
+ annotation.Value.GetType());
+
+ ODataEdmTypeSerializer edmTypeSerializer = GetEdmTypeSerializer(edmTypeReference, SerializerProvider);
+
+ if (edmTypeSerializer != null)
+ {
+ annotationValue = edmTypeSerializer.CreateODataValue(annotation.Value, edmTypeReference, resourceContext.SerializerContext);
+ }
+ }
+ else
+ {
+ annotationValue = new ODataNullValue();
+ }
+
+ if (annotationValue != null)
+ {
+ InstanceAnnotations.Add(new ODataInstanceAnnotation(annotation.Key, annotationValue));
+ }
+ }
+
+
+ private static ODataEdmTypeSerializer GetEdmTypeSerializer(IEdmTypeReference edmTypeReference, ODataSerializerProvider SerializerProvider)
+ {
+ ODataEdmTypeSerializer edmTypeSerializer;
+
+ if (edmTypeReference.IsCollection())
+ {
+ edmTypeSerializer = new ODataCollectionSerializer(SerializerProvider, true);
+ }
+ else if (edmTypeReference.IsStructured())
+ {
+ edmTypeSerializer = new ODataResourceValueSerializer(SerializerProvider);
+ }
+ else
+ {
+ edmTypeSerializer = SerializerProvider.GetEdmTypeSerializer(edmTypeReference);
+ }
+
+ return edmTypeSerializer;
+ }
+
+ }
+}
diff --git a/src/Microsoft.AspNet.OData.Shared/IDeltaDeletedEntityObject.cs b/src/Microsoft.AspNet.OData.Shared/IDeltaDeletedEntityObject.cs
new file mode 100644
index 0000000000..02ff2441aa
--- /dev/null
+++ b/src/Microsoft.AspNet.OData.Shared/IDeltaDeletedEntityObject.cs
@@ -0,0 +1,35 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (c) .NET Foundation and Contributors. All rights reserved.
+// See License.txt in the project root for license information.
+//
+//------------------------------------------------------------------------------
+
+using System;
+using Microsoft.OData;
+using Microsoft.OData.Edm;
+
+namespace Microsoft.AspNet.OData
+{
+ ///
+ /// Base interface to represented a typed deleted entity object
+ ///
+ public interface IDeltaDeletedEntityObject
+ {
+ ///
+ /// The id of the deleted entity (same as the odata.id returned or computed when calling GET on resource), which may be absolute or relative.
+ ///
+ Uri Id { get; set; }
+
+ ///
+ /// Optional. Either deleted, if the entity was deleted (destroyed), or changed if the entity was removed from membership in the result (i.e., due to a data change).
+ ///
+ DeltaDeletedEntryReason? Reason { get; set; }
+
+ ///
+ /// The navigation source of the deleted entity. If null, then the deleted entity is from the current feed.
+ ///
+ IEdmNavigationSource NavigationSource { get; set; }
+
+ }
+}
diff --git a/src/Microsoft.AspNet.OData.Shared/IDeltaSet.cs b/src/Microsoft.AspNet.OData.Shared/IDeltaSet.cs
new file mode 100644
index 0000000000..33c7a25214
--- /dev/null
+++ b/src/Microsoft.AspNet.OData.Shared/IDeltaSet.cs
@@ -0,0 +1,20 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (c) .NET Foundation and Contributors. All rights reserved.
+// See License.txt in the project root for license information.
+//
+//------------------------------------------------------------------------------
+
+namespace Microsoft.AspNet.OData
+{
+ ///
+ /// Basic interface to reperesent a deltaset which is a collection of Deltas.
+ /// This is being implemented by DeltaSet. Since its being implementd by a generic type and
+ /// since we need to check in a few places(like deserializer) where the object is a DeltaSet and the {TStructuralType} is not available,
+ /// we need a marker interface which can be used in these checks.
+ ///
+ public interface IDeltaSet
+ {
+
+ }
+}
diff --git a/src/Microsoft.AspNet.OData.Shared/IDeltaSetItem.cs b/src/Microsoft.AspNet.OData.Shared/IDeltaSetItem.cs
new file mode 100644
index 0000000000..b8bff3f003
--- /dev/null
+++ b/src/Microsoft.AspNet.OData.Shared/IDeltaSetItem.cs
@@ -0,0 +1,42 @@
+//-----------------------------------------------------------------------------
+//
+// 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.UriParser;
+using System;
+using System.Collections.Generic;
+using System.Reflection;
+using System.Text;
+
+namespace Microsoft.AspNet.OData
+{
+ ///
+ /// Basic Interface for representing a delta item like delta, deletedentity etc
+ ///
+ public interface IDeltaSetItem
+ {
+ ///
+ /// Entry or Deleted Entry for Delta Set Item
+ ///
+ EdmDeltaEntityKind DeltaKind { get; }
+
+ ///
+ /// Annotation container to hold Transient Instance Annotations
+ ///
+ IODataInstanceAnnotationContainer TransientInstanceAnnotationContainer { get; set; }
+
+ ///
+ /// Container to hold ODataId
+ ///
+ IODataIdContainer ODataIdContainer { get; set; }
+
+ ///
+ /// OData Path for the Item
+ ///
+ ODataPath ODataPath { get; set; }
+ }
+}
diff --git a/src/Microsoft.AspNet.OData.Shared/IODataAPIHandler.cs b/src/Microsoft.AspNet.OData.Shared/IODataAPIHandler.cs
new file mode 100644
index 0000000000..b2051f4b97
--- /dev/null
+++ b/src/Microsoft.AspNet.OData.Shared/IODataAPIHandler.cs
@@ -0,0 +1,19 @@
+//-----------------------------------------------------------------------------
+//
+// 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 nestedpatch 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 d071c08f42..4b881021d9 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 @@
+
@@ -53,12 +54,20 @@
+
+
+
+
+
+
+
+
@@ -71,8 +80,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Microsoft.AspNet.OData.Shared/ODataAPIHandler.cs b/src/Microsoft.AspNet.OData.Shared/ODataAPIHandler.cs
new file mode 100644
index 0000000000..d7ee5186a7
--- /dev/null
+++ b/src/Microsoft.AspNet.OData.Shared/ODataAPIHandler.cs
@@ -0,0 +1,186 @@
+//-----------------------------------------------------------------------------
+//
+// 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
+{
+ ///
+ /// Handler Class to handle users methods for create, delete and update.
+ /// This is the handler for data modification where there is a CLR type.
+ ///
+ public abstract class ODataAPIHandler: IODataAPIHandler where TStructuralType : class
+ {
+ ///
+ /// TryCreate method to create a new object.
+ ///
+ /// TheKey value pair of the objecct to be created. Optional
+ /// The created object (CLR or Typeless)
+ /// 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);
+
+ ///
+ /// TryGet method which tries to get the Origignal object based on a keyvalues.
+ ///
+ /// 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);
+
+ ///
+ /// TryDelete Method which will delete the object based on keyvalue pairs.
+ ///
+ ///
+ ///
+ /// The status of the TryGet method
+ public abstract ODataAPIResponseStatus TryDelete(IDictionary keyValues, 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.
+ public 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(IODataIdContainer));
+ if (property != null && property.GetValue(obj) is IODataIdContainer 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..6c925271ee
--- /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 ODataAPIHandlers.
+ ///
+ 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; private set; }
+
+ ///
+ /// Get the handler depending on OData path.
+ ///
+ /// OData path corresponding to an odataid.
+ /// 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.
+ public 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..3f6f0123d7
--- /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.Shared/ODataEdmAPIHandlerFactory.cs b/src/Microsoft.AspNet.OData.Shared/ODataEdmAPIHandlerFactory.cs
new file mode 100644
index 0000000000..674525fd99
--- /dev/null
+++ b/src/Microsoft.AspNet.OData.Shared/ODataEdmAPIHandlerFactory.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 ODataAPIHandlers for typeless entities
+ ///
+ internal abstract class ODataEdmAPIHandlerFactory
+ {
+ protected ODataEdmAPIHandlerFactory(IEdmModel model)
+ {
+ Model = model;
+ }
+
+ ///
+ /// The IEdmModel for the Factory.
+ ///
+ public IEdmModel Model { get; private set; }
+
+ ///
+ /// Get the handler depending on OData path.
+ ///
+ /// OData path corresponding to an odataid.
+ /// 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.
+ public 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/ODataIdContainer.cs b/src/Microsoft.AspNet.OData.Shared/ODataIdContainer.cs
new file mode 100644
index 0000000000..6db116c571
--- /dev/null
+++ b/src/Microsoft.AspNet.OData.Shared/ODataIdContainer.cs
@@ -0,0 +1,29 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (c) .NET Foundation and Contributors. All rights reserved.
+// See License.txt in the project root for license information.
+//
+//------------------------------------------------------------------------------
+
+namespace Microsoft.AspNet.OData
+{
+ ///
+ /// Interface to hold ODataID in parsed format, it will be used by POCO objects as well as Delta{TStructuralType}
+ ///
+ public interface IODataIdContainer
+ {
+ ///
+ /// OdataId path string
+ ///
+ string ODataId { set; get; }
+ }
+
+ ///
+ /// Default implementation of IOdataIdContainer
+ ///
+ public class ODataIdContainer : IODataIdContainer
+ {
+ ///
+ public string ODataId { set; get; }
+ }
+}
diff --git a/src/Microsoft.AspNet.OData.Shared/ResourceContext.cs b/src/Microsoft.AspNet.OData.Shared/ResourceContext.cs
index 2950e17097..d0a2b504db 100644
--- a/src/Microsoft.AspNet.OData.Shared/ResourceContext.cs
+++ b/src/Microsoft.AspNet.OData.Shared/ResourceContext.cs
@@ -95,6 +95,18 @@ public IEdmModel EdmModel
}
}
+ internal bool IsPostRequest
+ {
+ get
+ {
+#if NETCORE
+ return Request == null ? false : String.Equals(Request.Method, "post", StringComparison.OrdinalIgnoreCase);
+#else
+ return Request == null ? false : String.Equals(Request.Method.ToString(), "post", StringComparison.OrdinalIgnoreCase);
+#endif
+ }
+ }
+
///
/// Gets or sets the to which this instance belongs.
///
@@ -195,12 +207,21 @@ public object GetPropertyValue(string propertyName)
}
object value;
+ if (SerializerContext.IsDeltaOfT)
+ {
+ IDelta delta = ResourceInstance as IDelta;
+ if (delta != null && delta.TryGetPropertyValue(propertyName, out value))
+ {
+ return value;
+ }
+ }
+
if (EdmObject.TryGetPropertyValue(propertyName, out value))
{
return value;
}
else
- {
+ {
IEdmTypeReference edmType = EdmObject.GetEdmType();
if (edmType == null)
{
diff --git a/src/Microsoft.AspNet.OData.Shared/Routing/Conventions/EntitySetRoutingConvention.cs b/src/Microsoft.AspNet.OData.Shared/Routing/Conventions/EntitySetRoutingConvention.cs
index 4d85011cfe..38b775a6a4 100644
--- a/src/Microsoft.AspNet.OData.Shared/Routing/Conventions/EntitySetRoutingConvention.cs
+++ b/src/Microsoft.AspNet.OData.Shared/Routing/Conventions/EntitySetRoutingConvention.cs
@@ -46,6 +46,13 @@ internal static string SelectActionImpl(ODataPath odataPath, IWebApiControllerCo
"Post" + entitySet.EntityType().Name,
"Post");
}
+ else if (ODataRequestMethod.Patch == controllerContext.Request.Method)
+ {
+ // e.g. Try PatchCustomers first, then fall back to Patch action name
+ return actionMap.FindMatchingAction(
+ "Patch" + entitySet.Name,
+ "Patch");
+ }
}
else if (odataPath.PathTemplate == "~/entityset/$count" &&
ODataRequestMethod.Get == controllerContext.Request.GetRequestMethodOrPreflightMethod())
diff --git a/src/Microsoft.AspNet.OData.Shared/Routing/Conventions/NavigationRoutingConvention.cs b/src/Microsoft.AspNet.OData.Shared/Routing/Conventions/NavigationRoutingConvention.cs
index c2273693fb..591714dd27 100644
--- a/src/Microsoft.AspNet.OData.Shared/Routing/Conventions/NavigationRoutingConvention.cs
+++ b/src/Microsoft.AspNet.OData.Shared/Routing/Conventions/NavigationRoutingConvention.cs
@@ -50,9 +50,9 @@ internal static string SelectActionImpl(ODataPath odataPath, IWebApiControllerCo
return null;
}
- // It is not valid to *Put/Patch" to any collection-valued navigation property.
+ // It is not valid to *Put" to any collection-valued navigation property.
if (navigationProperty.TargetMultiplicity() == EdmMultiplicity.Many &&
- (ODataRequestMethod.Put == method || ODataRequestMethod.Patch == method))
+ ODataRequestMethod.Put == method)
{
return null;
}
diff --git a/src/Microsoft.AspNet.OData.Shared/Routing/ODataPath.cs b/src/Microsoft.AspNet.OData.Shared/Routing/ODataPath.cs
index 7a7eb799a1..2d5ded2382 100644
--- a/src/Microsoft.AspNet.OData.Shared/Routing/ODataPath.cs
+++ b/src/Microsoft.AspNet.OData.Shared/Routing/ODataPath.cs
@@ -114,5 +114,10 @@ public override string ToString()
/// Gets the ODL path.
///
public Semantic.ODataPath Path { get; internal set; }
+
+ internal IList SegmentList
+ {
+ get { return _segments.ToList(); }
+ }
}
}
diff --git a/src/Microsoft.AspNet.OData.Shared/TransientAnnotations.cs b/src/Microsoft.AspNet.OData.Shared/TransientAnnotations.cs
new file mode 100644
index 0000000000..e8a04c79fa
--- /dev/null
+++ b/src/Microsoft.AspNet.OData.Shared/TransientAnnotations.cs
@@ -0,0 +1,17 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (c) .NET Foundation and Contributors. All rights reserved.
+// See License.txt in the project root for license information.
+//
+//------------------------------------------------------------------------------
+
+using System.Collections.Generic;
+using Microsoft.AspNet.OData.Common;
+
+namespace Microsoft.AspNet.OData
+{
+ internal static class TransientAnnotations
+ {
+ internal static HashSet TransientAnnotationTerms = new HashSet() { SRResources.ContentID, SRResources.DataModificationException };
+ }
+}
diff --git a/src/Microsoft.AspNet.OData.Shared/TypedDelta.cs b/src/Microsoft.AspNet.OData.Shared/TypedDelta.cs
index 9ee9b11a6c..ad88b237bb 100644
--- a/src/Microsoft.AspNet.OData.Shared/TypedDelta.cs
+++ b/src/Microsoft.AspNet.OData.Shared/TypedDelta.cs
@@ -32,7 +32,7 @@ public abstract class TypedDelta : Delta
/// True if it is a Delta generic type; false otherwise.
internal static bool IsDeltaOfT(Type type)
{
- return type != null && TypeHelper.IsGenericType(type) && type.GetGenericTypeDefinition() == typeof(Delta<>);
+ return type != null && TypeHelper.IsGenericType(type) && typeof(Delta<>).IsAssignableFrom(type.GetGenericTypeDefinition());
}
}
}
diff --git a/src/Microsoft.AspNet.OData/GlobalSuppressions.cs b/src/Microsoft.AspNet.OData/GlobalSuppressions.cs
index 1d8ee02d50..32e5422a99 100644
--- a/src/Microsoft.AspNet.OData/GlobalSuppressions.cs
+++ b/src/Microsoft.AspNet.OData/GlobalSuppressions.cs
@@ -65,4 +65,88 @@
[assembly: SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Scope = "member", Target = "Microsoft.AspNet.OData.Builder.IODataInstanceAnnotationContainer.#GetAllResourceAnnotations()")]
[assembly: SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Scope = "member", Target = "Microsoft.AspNet.OData.Builder.IODataInstanceAnnotationContainer.#GetResourceAnnotations()")]
[assembly: SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly", Scope = "member", Target = "Microsoft.AspNet.OData.Common.Error.#PropertyNullOrWhiteSpace()")]
+
[assembly: SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Scope = "member", Target = "Microsoft.AspNet.OData.Delta`1.#CopyChangedValues(!0)")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes", Scope = "member", Target = "Microsoft.AspNet.OData.Delta`1.#Microsoft.AspNet.OData.IEdmObject.GetEdmType()")]
+[assembly: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Upsert", Scope = "member", Target = "Microsoft.AspNet.OData.DataModificationOperationKind.#Upsert")]
+[assembly: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Upsert", Scope = "member", Target = "Org.OData.Core.V1.DataModificationOperationKind.#Upsert")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", Target = "Org.OData.Core.V1")]
+
+[assembly: SuppressMessage("Microsoft.Design", "CA1040:AvoidEmptyInterfaces", Scope = "type", Target = "Microsoft.AspNet.OData.IDeltaSet")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "2#", Scope = "member", Target = "Microsoft.AspNet.OData.ODataAPIHandler`1.#TryGet(System.Collections.Generic.IDictionary`2,!0&,System.String&)")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "1#", Scope = "member", Target = "Microsoft.AspNet.OData.ODataAPIHandler`1.#TryDelete(System.Collections.Generic.IDictionary`2,System.String&)")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "1#", Scope = "member", Target = "Microsoft.AspNet.OData.ODataAPIHandler`1.#TryGet(System.Collections.Generic.IDictionary`2,!0&,System.String&)")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1040:AvoidEmptyInterfaces", Scope = "type", Target = "Microsoft.AspNet.OData.IODataAPIHandler")]
+[assembly: SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Scope = "member", Target = "Microsoft.AspNet.OData.Formatter.Serialization.DefaultODataSerializerProvider.#GetODataPayloadSerializerImpl(System.Type,System.Func`1,Microsoft.AspNet.OData.Routing.ODataPath,System.Type)")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "1#", Scope = "member", Target = "Microsoft.AspNet.OData.EdmODataAPIHandler.#TryDelete(System.Collections.Generic.IDictionary`2,System.String&)")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "1#", Scope = "member", Target = "Microsoft.AspNet.OData.EdmODataAPIHandler.#TryGet(System.Collections.Generic.IDictionary`2,Microsoft.AspNet.OData.EdmStructuredObject&,System.String&)")]
+[assembly: SuppressMessage("Microsoft.Globalization", "CA1305:SpecifyIFormatProvider", MessageId = "System.String.Format(System.String,System.Object,System.Object)", Scope = "member", Target = "Microsoft.AspNet.OData.Formatter.ClrTypeCache+EdmTypeCacheItemComparer.#GetHashCode(Microsoft.AspNet.OData.Formatter.ClrTypeCache+EdmTypeCacheItem)")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "1#", Scope = "member", Target = "Microsoft.AspNet.OData.EdmODataAPIHandler.#TryCreate(Microsoft.AspNet.OData.IEdmChangedObject,Microsoft.AspNet.OData.IEdmStructuredObject&,System.String&)")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "2#", Scope = "member", Target = "Microsoft.AspNet.OData.EdmODataAPIHandler.#TryCreate(Microsoft.AspNet.OData.IEdmChangedObject,Microsoft.AspNet.OData.IEdmStructuredObject&,System.String&)")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "2#", Scope = "member", Target = "Microsoft.AspNet.OData.EdmODataAPIHandler.#TryGet(System.Collections.Generic.IDictionary`2,Microsoft.AspNet.OData.IEdmStructuredObject&,System.String&)")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "1#", Scope = "member", Target = "Microsoft.AspNet.OData.EdmODataAPIHandler.#TryGet(System.Collections.Generic.IDictionary`2,Microsoft.AspNet.OData.IEdmStructuredObject&,System.String&)")]
+[assembly: SuppressMessage("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields", Scope = "member", Target = "Microsoft.AspNet.OData.EdmDeltaDeletedEntityObject.#_edmType")]
+[assembly: SuppressMessage("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields", Scope = "member", Target = "Microsoft.AspNet.OData.EdmDeltaDeletedLink.#_edmType")]
+[assembly: SuppressMessage("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields", Scope = "member", Target = "Microsoft.AspNet.OData.EdmDeltaEntityObject.#_edmType")]
+[assembly: SuppressMessage("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields", Scope = "member", Target = "Microsoft.AspNet.OData.EdmDeltaLink.#_edmType")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Scope = "member", Target = "Microsoft.AspNet.OData.Formatter.Deserialization.ODataResourceDeserializer.#CreateKeyProperties(System.Uri,Microsoft.AspNet.OData.Formatter.Deserialization.ODataDeserializerContext)")]
+[assembly: SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Scope = "member", Target = "Microsoft.AspNet.OData.Formatter.Deserialization.ODataResourceDeserializer.#CreateResourceWrapper(Microsoft.OData.Edm.IEdmTypeReference,Microsoft.AspNet.OData.Formatter.Deserialization.ODataEntityReferenceLinkBase,Microsoft.AspNet.OData.Formatter.Deserialization.ODataDeserializerContext)")]
+[assembly: SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Scope = "member", Target = "Microsoft.AspNet.OData.Formatter.Deserialization.ODataResourceDeserializer.#UpdateResourceWrapper(Microsoft.AspNet.OData.Formatter.Deserialization.ODataResourceWrapper,Microsoft.AspNet.OData.Formatter.Deserialization.ODataDeserializerContext)")]
+[assembly: SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Scope = "member", Target = "Microsoft.AspNet.OData.Delta`1.#CopyChangedValues(!0,Microsoft.AspNet.OData.ODataAPIHandler`1,Microsoft.AspNet.OData.ODataAPIHandlerFactory)")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Scope = "member", Target = "Microsoft.AspNet.OData.Formatter.Deserialization.ODataResourceDeserializer.#GetODataPath(System.String,Microsoft.AspNet.OData.Formatter.Deserialization.ODataDeserializerContext)")]
+[assembly: SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "API", Scope = "type", Target = "Microsoft.AspNet.OData.ODataAPIHandler`1")]
+[assembly: SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "API", Scope = "type", Target = "Microsoft.AspNet.OData.IODataAPIHandler")]
+[assembly: SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "API", Scope = "type", Target = "Microsoft.AspNet.OData.ODataEdmAPIHandlerFactory")]
+[assembly: SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "API", Scope = "type", Target = "Microsoft.AspNet.OData.ODataAPIResponseStatus")]
+[assembly: SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "API", Scope = "type", Target = "Microsoft.AspNet.OData.EdmODataAPIHandler")]
+[assembly: SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "API", Scope = "type", Target = "Microsoft.AspNet.OData.ODataAPIHandlerFactory")]
+
+[assembly: SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Scope = "member", Target = "Microsoft.AspNet.OData.DeltaSet`1.#TryDeleteObject(System.Collections.Generic.IDictionary`2,System.String&)")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Scope = "member", Target = "Microsoft.AspNet.OData.DeltaSet`1.#TryGetObject(System.Collections.Generic.IDictionary`2,System.Object&,System.String&)")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Scope = "member", Target = "Microsoft.AspNet.OData.EdmChangedObjectCollection.#TryCreateObject(System.Object&,System.String&)")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Scope = "member", Target = "Microsoft.AspNet.OData.EdmChangedObjectCollection.#TryDeleteObject(System.Collections.Generic.IDictionary`2,System.String&)")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Scope = "member", Target = "Microsoft.AspNet.OData.EdmChangedObjectCollection.#TryGetObject(System.Collections.Generic.IDictionary`2,System.Object&,System.String&)")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible", Scope = "type", Target = "Microsoft.AspNet.OData.PatchMethodHandler+TryCreate")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible", Scope = "type", Target = "Microsoft.AspNet.OData.PatchMethodHandler+TryDelete")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible", Scope = "type", Target = "Microsoft.AspNet.OData.PatchMethodHandler+TryGet")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Scope = "member", Target = "Microsoft.AspNet.OData.DeltaSet`1.#TryCreateObject(System.Object&,System.String&)")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "0#", Scope = "member", Target = "Microsoft.AspNet.OData.PatchMethodHandler+TryCreate.#Invoke(System.Object&,System.String&)")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "1#", Scope = "member", Target = "Microsoft.AspNet.OData.PatchMethodHandler+TryCreate.#Invoke(System.Object&,System.String&)")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "1#", Scope = "member", Target = "Microsoft.AspNet.OData.PatchMethodHandler+TryDelete.#Invoke(System.Collections.Generic.IDictionary`2,System.String&)")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "2#", Scope = "member", Target = "Microsoft.AspNet.OData.PatchMethodHandler+TryGet.#Invoke(System.Collections.Generic.IDictionary`2,System.Object&,System.String&)")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "1#", Scope = "member", Target = "Microsoft.AspNet.OData.PatchMethodHandler+TryGet.#Invoke(System.Collections.Generic.IDictionary`2,System.Object&,System.String&)")]
+
+[assembly: SuppressMessage("Microsoft.Design", "CA1040:AvoidEmptyInterfaces", Scope = "type", Target = "Microsoft.AspNet.OData.IDeltaSet")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "2#", Scope = "member", Target = "Microsoft.AspNet.OData.ODataAPIHandler`1.#TryGet(System.Collections.Generic.IDictionary`2,!0&,System.String&)")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "1#", Scope = "member", Target = "Microsoft.AspNet.OData.ODataAPIHandler`1.#TryDelete(System.Collections.Generic.IDictionary`2,System.String&)")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "1#", Scope = "member", Target = "Microsoft.AspNet.OData.ODataAPIHandler`1.#TryGet(System.Collections.Generic.IDictionary`2,!0&,System.String&)")]
+[assembly: SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Scope = "member", Target = "Microsoft.AspNet.OData.ODataAPIHandler`1.#GetODataPath(System.String,Microsoft.OData.Edm.IEdmModel)")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1040:AvoidEmptyInterfaces", Scope = "type", Target = "Microsoft.AspNet.OData.IODataAPIHandler")]
+[assembly: SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Scope = "member", Target = "Microsoft.AspNet.OData.Formatter.Serialization.DefaultODataSerializerProvider.#GetODataPayloadSerializerImpl(System.Type,System.Func`1,Microsoft.AspNet.OData.Routing.ODataPath,System.Type)")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "1#", Scope = "member", Target = "Microsoft.AspNet.OData.EdmODataAPIHandler.#TryDelete(System.Collections.Generic.IDictionary`2,System.String&)")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "1#", Scope = "member", Target = "Microsoft.AspNet.OData.EdmODataAPIHandler.#TryGet(System.Collections.Generic.IDictionary`2,Microsoft.AspNet.OData.EdmStructuredObject&,System.String&)")]
+[assembly: SuppressMessage("Microsoft.Globalization", "CA1305:SpecifyIFormatProvider", MessageId = "System.String.Format(System.String,System.Object,System.Object)", Scope = "member", Target = "Microsoft.AspNet.OData.Formatter.ClrTypeCache+EdmTypeCacheItemComparer.#GetHashCode(Microsoft.AspNet.OData.Formatter.ClrTypeCache+EdmTypeCacheItem)")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "1#", Scope = "member", Target = "Microsoft.AspNet.OData.EdmODataAPIHandler.#TryCreate(Microsoft.AspNet.OData.IEdmChangedObject,Microsoft.AspNet.OData.IEdmStructuredObject&,System.String&)")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "2#", Scope = "member", Target = "Microsoft.AspNet.OData.EdmODataAPIHandler.#TryCreate(Microsoft.AspNet.OData.IEdmChangedObject,Microsoft.AspNet.OData.IEdmStructuredObject&,System.String&)")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "2#", Scope = "member", Target = "Microsoft.AspNet.OData.EdmODataAPIHandler.#TryGet(System.Collections.Generic.IDictionary`2,Microsoft.AspNet.OData.IEdmStructuredObject&,System.String&)")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "1#", Scope = "member", Target = "Microsoft.AspNet.OData.EdmODataAPIHandler.#TryGet(System.Collections.Generic.IDictionary`2,Microsoft.AspNet.OData.IEdmStructuredObject&,System.String&)")]
+[assembly: SuppressMessage("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields", Scope = "member", Target = "Microsoft.AspNet.OData.EdmDeltaDeletedEntityObject.#_edmType")]
+[assembly: SuppressMessage("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields", Scope = "member", Target = "Microsoft.AspNet.OData.EdmDeltaDeletedLink.#_edmType")]
+[assembly: SuppressMessage("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields", Scope = "member", Target = "Microsoft.AspNet.OData.EdmDeltaEntityObject.#_edmType")]
+[assembly: SuppressMessage("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields", Scope = "member", Target = "Microsoft.AspNet.OData.EdmDeltaLink.#_edmType")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Scope = "member", Target = "Microsoft.AspNet.OData.Formatter.Deserialization.ODataResourceDeserializer.#CreateKeyProperties(System.Uri,Microsoft.AspNet.OData.Formatter.Deserialization.ODataDeserializerContext)")]
+[assembly: SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Scope = "member", Target = "Microsoft.AspNet.OData.Formatter.Deserialization.ODataResourceDeserializer.#CreateResourceWrapper(Microsoft.OData.Edm.IEdmTypeReference,Microsoft.AspNet.OData.Formatter.Deserialization.ODataEntityReferenceLinkBase,Microsoft.AspNet.OData.Formatter.Deserialization.ODataDeserializerContext)")]
+[assembly: SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Scope = "member", Target = "Microsoft.AspNet.OData.Formatter.Deserialization.ODataResourceDeserializer.#UpdateResourceWrapper(Microsoft.AspNet.OData.Formatter.Deserialization.ODataResourceWrapper,Microsoft.AspNet.OData.Formatter.Deserialization.ODataDeserializerContext)")]
+[assembly: SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Scope = "member", Target = "Microsoft.AspNet.OData.Delta`1.#CopyChangedValues(!0,Microsoft.AspNet.OData.ODataAPIHandler`1,Microsoft.AspNet.OData.ODataAPIHandlerFactory)")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Scope = "member", Target = "Microsoft.AspNet.OData.Formatter.Deserialization.ODataResourceDeserializer.#GetODataPath(System.String,Microsoft.AspNet.OData.Formatter.Deserialization.ODataDeserializerContext)")]
+[assembly: SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "API", Scope = "type", Target = "Microsoft.AspNet.OData.ODataAPIHandler`1")]
+[assembly: SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "API", Scope = "type", Target = "Microsoft.AspNet.OData.IODataAPIHandler")]
+[assembly: SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "API", Scope = "type", Target = "Microsoft.AspNet.OData.ODataEdmAPIHandlerFactory")]
+[assembly: SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "API", Scope = "type", Target = "Microsoft.AspNet.OData.ODataAPIResponseStatus")]
+[assembly: SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "API", Scope = "type", Target = "Microsoft.AspNet.OData.EdmODataAPIHandler")]
+[assembly: SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "API", Scope = "type", Target = "Microsoft.AspNet.OData.ODataAPIHandlerFactory")]
+[assembly: SuppressMessage("Microsoft.Globalization", "CA1309:UseOrdinalStringComparison", MessageId = "System.String.Equals(System.String,System.String,System.StringComparison)", Scope = "member", Target = "Microsoft.AspNet.OData.Formatter.Deserialization.ODataResourceDeserializer.#CreateResourceId(Microsoft.AspNet.OData.Formatter.Deserialization.ODataResourceWrapper,Microsoft.AspNet.OData.Formatter.Deserialization.ODataDeserializerContext,Microsoft.OData.UriParser.ODataPathSegment,Microsoft.OData.Edm.IEdmEntityType)")]
+[assembly: SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix", Scope = "type", Target = "Microsoft.AspNet.OData.NavigationPath")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Scope = "member", Target = "Microsoft.AspNet.OData.Formatter.Deserialization.ODataResourceDeserializer.#GenerateNestedReadContext(Microsoft.AspNet.OData.Formatter.Deserialization.ODataNestedResourceInfoWrapper,Microsoft.AspNet.OData.Formatter.Deserialization.ODataDeserializerContext,Microsoft.OData.Edm.IEdmProperty)")]
+[assembly: SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Scope = "member", Target = "Microsoft.AspNet.OData.Formatter.Deserialization.ODataResourceDeserializer.#ApplyIdToPath(Microsoft.AspNet.OData.Formatter.Deserialization.ODataDeserializerContext,Microsoft.AspNet.OData.Formatter.Deserialization.ODataResourceWrapper)")]
+[assembly: SuppressMessage("Microsoft.Globalization", "CA1309:UseOrdinalStringComparison", MessageId = "System.String.Equals(System.String,System.String,System.StringComparison)", Scope = "member", Target = "Microsoft.AspNet.OData.Formatter.Deserialization.ODataResourceDeserializer.#ApplyIdToPath(Microsoft.AspNet.OData.Formatter.Deserialization.ODataDeserializerContext,Microsoft.AspNet.OData.Formatter.Deserialization.ODataResourceWrapper)")]
diff --git a/src/Microsoft.AspNet.OData/Properties/AssemblyInfo.cs b/src/Microsoft.AspNet.OData/Properties/AssemblyInfo.cs
index 4bd46c88f7..6db41a4b01 100644
--- a/src/Microsoft.AspNet.OData/Properties/AssemblyInfo.cs
+++ b/src/Microsoft.AspNet.OData/Properties/AssemblyInfo.cs
@@ -12,3 +12,6 @@
[assembly: AssemblyDescription("")]
[assembly: InternalsVisibleTo("Microsoft.AspNet.OData.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")]
+[assembly: InternalsVisibleTo("Microsoft.Test.E2E.AspNet.OData, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")]
+[assembly: InternalsVisibleTo("Microsoft.Test.E2E.AspNetCore.OData, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")]
+[assembly: InternalsVisibleTo("Microsoft.Test.E2E.AspNetCore3x.OData, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")]
\ No newline at end of file
diff --git a/src/Microsoft.AspNetCore.OData/Properties/AssemblyInfo.cs b/src/Microsoft.AspNetCore.OData/Properties/AssemblyInfo.cs
index e09883cfac..cf56834d77 100644
--- a/src/Microsoft.AspNetCore.OData/Properties/AssemblyInfo.cs
+++ b/src/Microsoft.AspNetCore.OData/Properties/AssemblyInfo.cs
@@ -12,3 +12,6 @@
[assembly: AssemblyDescription("")]
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.OData.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")]
+[assembly: InternalsVisibleTo("Microsoft.Test.E2E.AspNet.OData, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")]
+[assembly: InternalsVisibleTo("Microsoft.Test.E2E.AspNetCore.OData, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")]
+[assembly: InternalsVisibleTo("Microsoft.Test.E2E.AspNetCore3x.OData, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")]
\ No newline at end of file
diff --git a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNet/Formatter/ODataEntityReferenceLinkE2ETests.cs b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNet/Formatter/ODataEntityReferenceLinkE2ETests.cs
new file mode 100644
index 0000000000..40ff2933b3
--- /dev/null
+++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNet/Formatter/ODataEntityReferenceLinkE2ETests.cs
@@ -0,0 +1,276 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Data.Entity;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Threading.Tasks;
+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 Microsoft.Test.E2E.AspNet.OData.Common.Execution;
+using Newtonsoft.Json.Linq;
+using Xunit;
+
+namespace Microsoft.Test.E2E.AspNet.OData.Formatter
+{
+ public class ODataEntityReferenceLinkE2ETests : WebHostTestBase
+ {
+ public ODataEntityReferenceLinkE2ETests(WebHostTestFixture fixture)
+ : base(fixture)
+ {
+ }
+ protected override void UpdateConfiguration(WebRouteConfiguration configuration)
+ {
+ var controllers = new[] { typeof(BooksController)};
+ configuration.AddControllers(controllers);
+ configuration.Count().Filter().OrderBy().Expand().MaxTop(null).Select();
+ configuration.MapODataServiceRoute("odata", "odata", BuildEdmModel(configuration));
+ }
+ private static IEdmModel BuildEdmModel(WebRouteConfiguration configuration)
+ {
+ var builder = configuration.CreateConventionModelBuilder();
+ builder.EntitySet("Books");
+ builder.EntitySet("Authors");
+ builder.Action("ResetDataSource");
+ builder.Action("RelateToExistingEntityAndUpdate").ReturnsFromEntitySet("Books").EntityParameter("book");
+
+ return builder.GetEdmModel();
+ }
+
+ [Fact]
+ public async Task CanCreate_ANewEntityAndRelateToAnExistingEntity_UsingODataBind()
+ {
+ await ResetDataSource();
+ // Arrange
+ const string Payload = "{" +
+ "\"Id\":\"1\"," +
+ "\"Name\":\"BookA\"," +
+ "\"Author@odata.bind\":\"Authors(1)\"}";
+
+ string Uri = BaseAddress + "/odata/Books";
+ HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, Uri);
+
+ request.Content = new StringContent(Payload);
+ request.Content.Headers.ContentType = MediaTypeWithQualityHeaderValue.Parse("application/json");
+
+ // Act
+ HttpResponseMessage response = await Client.SendAsync(request);
+
+ // Assert
+ Assert.True(response.IsSuccessStatusCode);
+ Assert.Equal(HttpStatusCode.Created, response.StatusCode);
+ Assert.NotNull(response.Content);
+
+ //Get the above saved entity from the database
+ //and expand the navigation property to see if
+ //it was correctly created with the existing entity
+ //attached to it.
+ string query = string.Format("{0}/odata/Books?$expand=Author", BaseAddress);
+ HttpRequestMessage requestMessage = new HttpRequestMessage(HttpMethod.Get, query);
+ request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json"));
+
+ // Act
+ HttpResponseMessage res = await Client.SendAsync(requestMessage);
+
+ // Assert
+ Assert.True(res.IsSuccessStatusCode);
+ var responseObject = JObject.Parse(await res.Content.ReadAsStringAsync());
+ var result = responseObject["value"] as JArray;
+ var expandProp = result[0]["Author"] as JObject;
+ Assert.Equal(1, expandProp["Id"]);
+
+ }
+
+ [Fact]
+ public async Task CanUpdate_TheRelatedEntitiesProperties()
+ {
+ await ResetDataSource();
+ // Arrange
+ const string Payload = "{" +
+ "\"book\":{" +
+ "\"Id\":\"1\"," +
+ "\"Name\":\"BookA\"," +
+ "\"Author\":{" +
+ "\"@odata.id\":\"Authors(1)\"," +
+ "\"Name\":\"UpdatedAuthor\"}}}";
+
+ string Uri = BaseAddress + "/odata/RelateToExistingEntityAndUpdate";
+ HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, Uri);
+
+ request.Content = new StringContent(Payload);
+ request.Content.Headers.ContentType = MediaTypeWithQualityHeaderValue.Parse("application/json");
+
+ // Act
+ HttpResponseMessage response = await Client.SendAsync(request);
+
+ // Assert
+ Assert.True(response.IsSuccessStatusCode);
+ Assert.Equal(HttpStatusCode.Created, response.StatusCode);
+ Assert.NotNull(response.Content);
+
+ //Get the above saved entity from the database
+ //and expand its navigation property to see if it was created with
+ //the existing entity correctly.
+ //Also note that we were able to update the name property
+ //of the existing entity
+ string query = string.Format("{0}/odata/Books?$expand=Author", BaseAddress);
+ HttpRequestMessage requestMessage = new HttpRequestMessage(HttpMethod.Get, query);
+ request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json"));
+
+ // Act
+ HttpResponseMessage res = await Client.SendAsync(requestMessage);
+
+ // Assert
+ Assert.True(res.IsSuccessStatusCode);
+ var responseObject = JObject.Parse(await res.Content.ReadAsStringAsync());
+ var result = responseObject["value"] as JArray;
+ var expandProp = result[0]["Author"] as JObject;
+ Assert.Equal(1, expandProp["Id"]);
+ Assert.Equal("UpdatedAuthor", expandProp["Name"]);
+ }
+
+ private async Task ResetDataSource()
+ {
+ string requestUri = BaseAddress + "/odata/ResetDataSource";
+ HttpClient client = new HttpClient();
+ HttpResponseMessage response = await client.GetAsync(requestUri);
+ response.EnsureSuccessStatusCode();
+ }
+ }
+
+ public class BooksController : TestODataController
+#if NETCORE
+ , IDisposable
+#endif
+ {
+ private ODataEntityReferenceLinkContext db = new ODataEntityReferenceLinkContext();
+
+ [EnableQuery]
+ public ITestActionResult Get()
+ {
+ return Ok(db.Books);
+ }
+ public ITestActionResult Post([FromBody] Book book)
+ {
+ if (!ModelState.IsValid)
+ {
+ return BadRequest();
+ }
+
+ db.Authors.Attach(book.Author);
+ db.Books.Add(book);
+ db.SaveChanges();
+
+ return Created(book);
+ }
+
+ [HttpPost]
+ [ODataRoute("RelateToExistingEntityAndUpdate")]
+ public ITestActionResult RelateToExistingEntityAndUpdate(ODataActionParameters odataActionParameters)
+ {
+ if (!ModelState.IsValid)
+ {
+ return BadRequest();
+ }
+
+ Book book = (Book)odataActionParameters["book"];
+ string authorName = book.Author.Name;
+ Author author = new Author()
+ {
+ Id = book.Author.Id
+ };
+ db.Authors.Attach(author);
+ book.Author = author;
+ book.Author.Name = authorName;
+ db.Books.Add(book);
+
+ db.SaveChanges();
+ return Created(book);
+ }
+
+ [HttpGet]
+ [ODataRoute("ResetDataSource")]
+ public ITestActionResult ResetDataSource()
+ {
+ db.Database.Delete(); // Start from scratch so that tests aren't broken by schema changes.
+ CreateDatabase();
+ return Ok();
+ }
+
+ private static void CreateDatabase()
+ {
+ using (ODataEntityReferenceLinkContext db = new ODataEntityReferenceLinkContext())
+ {
+ if (!db.Authors.Any())
+ {
+ IList authors = new List()
+ {
+ new Author()
+ {
+ Id = 1,
+ Name = "AuthorA"
+ },
+ new Author()
+ {
+ Id = 2,
+ Name = "AuthorB"
+ },
+ new Author()
+ {
+ Id = 3,
+ Name = "AuthorC"
+ }
+ };
+
+ foreach (var author in authors)
+ {
+ db.Authors.Add(author);
+ }
+ db.SaveChanges();
+ }
+ }
+ }
+
+#if NETCORE
+ public void Dispose()
+ {
+ //_db.Dispose();
+ }
+#endif
+
+ }
+
+ public class Book
+ {
+ [Key]
+ public int Id { get; set; }
+ public string Name { get; set; }
+ public Author Author { get; set; }
+ public IList AuthorList { get; set; }
+ }
+
+ public class Author
+ {
+ [Key]
+ public int Id { get; set; }
+ public string Name { get; set; }
+ }
+
+ public class ODataEntityReferenceLinkContext : DbContext
+ {
+ public static string ConnectionString = @"Data Source=(LocalDb)\MSSQLLocalDB;Integrated Security=True;Initial Catalog=ODataEntityReferenceLinkContext";
+ public ODataEntityReferenceLinkContext()
+ : base(ConnectionString)
+ {
+ }
+ public DbSet Books { get; set; }
+ public DbSet Authors { get; set; }
+ }
+}
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 272f5397ff..3aa60bfcee 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
@@ -57,6 +57,9 @@
..\..\..\..\sln\packages\Microsoft.Owin.4.2.2\lib\net45\Microsoft.Owin.dll
+
+
+ ..\..\..\..\sln\packages\Microsoft.Owin.2.0.2\lib\net45\Microsoft.Owin.dll
True
@@ -163,6 +166,21 @@
Aggregation\AggregationTests.cs
+
+ BulkOperation\BulkOperationTest.cs
+
+
+ BulkOperation\BulkOperationDataModel.cs
+
+
+ BulkOperation\BulkOperationEdmModel.cs
+
+
+ BulkOperation\BulkOperationController.cs
+
+
+ BulkOperation\BulkOperationPatchHandlers.cs
+
EntitySetAggregation\EntitySetAggregationController.cs
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 40c41bbdde..6909cd715d 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
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..a09f22adeb
--- /dev/null
+++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Build.AspNetCore3x/BulkOperation/EFTests/BulkOperationPatchHandlersEF.cs
@@ -0,0 +1,410 @@
+//-----------------------------------------------------------------------------
+//
+// 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;
+ }
+ }
+ }
+
+ 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);
+ }
+ }
+
+ 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;
+ }
+ }
+
+ 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();
+ }
+ }
+ }
+}
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..eed66da892
--- /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_WithMultipleUpdatesinOrder1()
+ {
+ //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_WithMultipleUpdatesinOrder2()
+ {
+ //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..85570102b4 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
@@ -27,6 +27,7 @@
+
@@ -1664,6 +1665,21 @@
Validation\DeltaOfTValidationTests.cs
+
+ BulkOperation\BulkOperationTest.cs
+
+
+ BulkOperation\BulkOperationDataModel.cs
+
+
+ BulkOperation\BulkOperationEdmModel.cs
+
+
+ BulkOperation\BulkOperationController.cs
+
+
+ BulkOperation\BulkOperationPatchHandlers.cs
+
@@ -1677,4 +1693,5 @@
+
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..bcc043edd6
--- /dev/null
+++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkOperationController.cs
@@ -0,0 +1,517 @@
+//-----------------------------------------------------------------------------
+//
+// 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();
+
+ 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);
+ }
+
+ 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..39aeb11bd5
--- /dev/null
+++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkOperationDataModel.cs
@@ -0,0 +1,236 @@
+//-----------------------------------------------------------------------------
+//
+// 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 IODataIdContainer Container { get; set; }
+ }
+
+ public class NewOrder
+ {
+ [Key]
+ public int Id { get; set; }
+
+ public int Price { get; set; }
+
+ public int Quantity { get; set; }
+
+ public IODataIdContainer 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..8670cce221
--- /dev/null
+++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkOperationPatchHandlers.cs
@@ -0,0 +1,1211 @@
+//-----------------------------------------------------------------------------
+//
+// 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;
+
+ default:
+ return null;
+ }
+ }
+ return null;
+ }
+ }
+
+ internal class TypelessAPIHandlerFactory : ODataEdmAPIHandlerFactory
+ {
+ 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;
+ }
+ }
+ }
+
+ internal class OverdueOrderAPIHandler : ODataAPIHandler
+ {
+ Company parent;
+
+ public OverdueOrderAPIHandler(Company parent)
+ {
+ this.parent = parent;
+ }
+
+ public override ODataAPIResponseStatus TryCreate(IDictionary keyValues, out NewOrder createdObject, out string errorMessage)
+ {
+ createdObject = null;
+ errorMessage = string.Empty;
+
+ try
+ {
+ createdObject = new NewOrder();
+ parent.OverdueOrders.Add(createdObject);
+
+ return ODataAPIResponseStatus.Success;
+ }
+ catch (Exception ex)
+ {
+ errorMessage = ex.Message;
+
+ return ODataAPIResponseStatus.Failure;
+ }
+ }
+
+ public override ODataAPIResponseStatus TryDelete(IDictionary keyValues, out string errorMessage)
+ {
+ errorMessage = string.Empty;
+
+ try
+ {
+ var id = keyValues.First().Value.ToString();
+ var newOrders = CompanyController.OverdueOrders.First(x => x.Id == Int32.Parse(id));
+
+ parent.OverdueOrders.Remove(newOrders);
+
+ return ODataAPIResponseStatus.Success;
+ }
+ catch (Exception ex)
+ {
+ errorMessage = ex.Message;
+
+ return ODataAPIResponseStatus.Failure;
+ }
+ }
+
+ public override ODataAPIResponseStatus TryGet(IDictionary keyValues, out NewOrder originalObject, out string errorMessage)
+ {
+ ODataAPIResponseStatus status = ODataAPIResponseStatus.Success;
+ errorMessage = string.Empty;
+ originalObject = null;
+
+ try
+ {
+ var id = keyValues["Id"].ToString();
+ originalObject = parent.OverdueOrders.FirstOrDefault(x => x.Id == Int32.Parse(id));
+
+ if (originalObject == null)
+ {
+ status = ODataAPIResponseStatus.NotFound;
+ }
+ }
+ catch (Exception ex)
+ {
+ status = ODataAPIResponseStatus.Failure;
+ errorMessage = ex.Message;
+ }
+
+ return status;
+ }
+
+ public override IODataAPIHandler GetNestedHandler(NewOrder parent, string navigationPropertyName)
+ {
+ switch (navigationPropertyName)
+ {
+ default:
+ return null;
+ }
+ }
+ }
+
+ internal class MyOverdueOrderAPIHandler : ODataAPIHandler
+ {
+ Company parent;
+
+ public MyOverdueOrderAPIHandler(Company parent)
+ {
+ this.parent = parent;
+ }
+
+ public override ODataAPIResponseStatus TryCreate(IDictionary keyValues, out MyNewOrder createdObject, out string errorMessage)
+ {
+ createdObject = null;
+ errorMessage = string.Empty;
+
+ try
+ {
+ createdObject = new MyNewOrder();
+ parent.MyOverdueOrders.Add(createdObject);
+
+ return ODataAPIResponseStatus.Success;
+ }
+ catch (Exception ex)
+ {
+ errorMessage = ex.Message;
+
+ return ODataAPIResponseStatus.Failure;
+ }
+ }
+
+ public override ODataAPIResponseStatus TryDelete(IDictionary keyValues, out string errorMessage)
+ {
+ errorMessage = string.Empty;
+
+ try
+ {
+ var id = keyValues.First().Value.ToString();
+ var newOrders = CompanyController.MyOverdueOrders.First(x => x.Id == Int32.Parse(id));
+
+ parent.MyOverdueOrders.Remove(newOrders);
+
+ return ODataAPIResponseStatus.Success;
+ }
+ catch (Exception ex)
+ {
+ errorMessage = ex.Message;
+
+ return ODataAPIResponseStatus.Failure;
+ }
+ }
+
+ public override ODataAPIResponseStatus TryGet(IDictionary keyValues, out MyNewOrder originalObject, out string errorMessage)
+ {
+ ODataAPIResponseStatus status = ODataAPIResponseStatus.Success;
+ errorMessage = string.Empty;
+ originalObject = null;
+
+ try
+ {
+ var id = keyValues["Id"].ToString();
+ originalObject = parent.MyOverdueOrders.FirstOrDefault(x => x.Id == Int32.Parse(id));
+
+
+ if (originalObject == null)
+ {
+ status = ODataAPIResponseStatus.NotFound;
+ }
+ }
+ catch (Exception ex)
+ {
+ status = ODataAPIResponseStatus.Failure;
+ errorMessage = ex.Message;
+ }
+
+ return status;
+ }
+
+ public override IODataAPIHandler GetNestedHandler(MyNewOrder parent, string navigationPropertyName)
+ {
+ switch (navigationPropertyName)
+ {
+
+ default:
+ return null;
+ }
+ }
+ }
+
+ internal class EmployeeAPIHandler : ODataAPIHandler
+ {
+ public override ODataAPIResponseStatus TryCreate(IDictionary keyValues, out Employee createdObject, out string errorMessage)
+ {
+ createdObject = null;
+ errorMessage = null;
+
+ try
+ {
+ createdObject = new Employee();
+ EmployeesController.Employees.Add(createdObject);
+
+ return ODataAPIResponseStatus.Success;
+ }
+ catch (Exception ex)
+ {
+ errorMessage = ex.Message;
+
+ return ODataAPIResponseStatus.Failure;
+ }
+ }
+
+ public override ODataAPIResponseStatus TryDelete(IDictionary keyValues, out string errorMessage)
+ {
+ errorMessage = null;
+
+ try
+ {
+ var id = keyValues.First().Value.ToString();
+ var employee = EmployeesController.Employees.First(x => x.ID == Int32.Parse(id));
+
+ EmployeesController.Employees.Remove(employee);
+
+ return ODataAPIResponseStatus.Success;
+ }
+ catch (Exception ex)
+ {
+ errorMessage = ex.Message;
+
+ return ODataAPIResponseStatus.Failure;
+ }
+ }
+
+ public override ODataAPIResponseStatus TryGet(IDictionary keyValues, out Employee originalObject, out string errorMessage)
+ {
+ ODataAPIResponseStatus status = ODataAPIResponseStatus.Success;
+ errorMessage = null;
+ originalObject = null;
+
+ try
+ {
+ var id = keyValues["ID"].ToString();
+ originalObject = EmployeesController.Employees.First(x => x.ID == Int32.Parse(id));
+
+ if (originalObject == null)
+ {
+ status = ODataAPIResponseStatus.NotFound;
+ }
+ }
+ catch (Exception ex)
+ {
+ status = ODataAPIResponseStatus.Failure;
+ errorMessage = ex.Message;
+ }
+
+ return status;
+ }
+
+ public override IODataAPIHandler GetNestedHandler(Employee parent, string navigationPropertyName)
+ {
+ switch (navigationPropertyName)
+ {
+ case "Friends":
+ return new FriendAPIHandler(parent);
+ case "NewFriends":
+ return new NewFriendAPIHandler(parent);
+ default:
+ return null;
+ }
+ }
+ }
+
+ internal class FriendAPIHandler : ODataAPIHandler
+ {
+ Employee employee;
+ public FriendAPIHandler(Employee employee)
+ {
+ this.employee = employee;
+ }
+
+ public override ODataAPIResponseStatus TryCreate(IDictionary keyValues, out Friend createdObject, out string errorMessage)
+ {
+ createdObject = null;
+ errorMessage = string.Empty;
+
+ try
+ {
+ createdObject = new Friend();
+ employee.Friends.Add(createdObject);
+
+ return ODataAPIResponseStatus.Success;
+ }
+ catch (Exception ex)
+ {
+ errorMessage = ex.Message;
+
+ return ODataAPIResponseStatus.Failure;
+ }
+ }
+
+ public override ODataAPIResponseStatus TryDelete(IDictionary keyValues, out string errorMessage)
+ {
+ errorMessage = string.Empty;
+
+ try
+ {
+ var id = keyValues.First().Value.ToString();
+ var friend = employee.Friends.FirstOrDefault(x => x.Id == Int32.Parse(id));
+
+ employee.Friends.Remove(friend);
+
+ return ODataAPIResponseStatus.Success;
+ }
+ catch (Exception ex)
+ {
+ errorMessage = ex.Message;
+
+ return ODataAPIResponseStatus.Failure;
+ }
+ }
+
+ public override ODataAPIResponseStatus TryGet(IDictionary keyValues, out Friend originalObject, out string errorMessage)
+ {
+ ODataAPIResponseStatus status = ODataAPIResponseStatus.Success;
+ errorMessage = string.Empty;
+ originalObject = null;
+
+ try
+ {
+ var id = keyValues["Id"].ToString();
+ originalObject = employee.Friends.FirstOrDefault(x => x.Id == Int32.Parse(id));
+
+
+ if (originalObject == null)
+ {
+ status = ODataAPIResponseStatus.NotFound;
+ }
+ }
+ catch (Exception ex)
+ {
+ status = ODataAPIResponseStatus.Failure;
+ errorMessage = ex.Message;
+ }
+
+ return status;
+ }
+
+ public override IODataAPIHandler GetNestedHandler(Friend parent, string navigationPropertyName)
+ {
+ switch (navigationPropertyName)
+ {
+ case "Orders":
+ return new OrderAPIHandler(parent);
+ default:
+ return null;
+ }
+ }
+ }
+
+ internal class NewOrderAPIHandler : ODataAPIHandler
+ {
+ NewFriend friend;
+ public NewOrderAPIHandler(NewFriend friend)
+ {
+ this.friend = friend;
+ }
+
+ public override ODataAPIResponseStatus TryCreate(IDictionary keyValues, out NewOrder createdObject, out string errorMessage)
+ {
+ createdObject = null;
+ errorMessage = string.Empty;
+
+ try
+ {
+ createdObject = new NewOrder();
+
+ if (friend.NewOrders == null)
+ {
+ friend.NewOrders = new List();
+ }
+
+ friend.NewOrders.Add(createdObject);
+
+ return ODataAPIResponseStatus.Success;
+ }
+ catch (Exception ex)
+ {
+ errorMessage = ex.Message;
+
+ return ODataAPIResponseStatus.Failure;
+ }
+ }
+
+ public override ODataAPIResponseStatus TryDelete(IDictionary keyValues, out string errorMessage)
+ {
+ errorMessage = string.Empty;
+
+ try
+ {
+ var id = keyValues.First().Value.ToString();
+ var friend = this.friend.NewOrders.FirstOrDefault(x => x.Id == int.Parse(id));
+
+ this.friend.NewOrders.Remove(friend);
+
+ return ODataAPIResponseStatus.Success;
+ }
+ catch (Exception ex)
+ {
+ errorMessage = ex.Message;
+
+ return ODataAPIResponseStatus.Failure;
+ }
+ }
+
+ public override ODataAPIResponseStatus TryGet(IDictionary keyValues, out NewOrder originalObject, out string errorMessage)
+ {
+ ODataAPIResponseStatus status = ODataAPIResponseStatus.Success;
+ errorMessage = string.Empty;
+ originalObject = null;
+
+ try
+ {
+ if (friend.NewOrders != null)
+ {
+ var id = keyValues["Id"].ToString();
+ originalObject = friend.NewOrders.FirstOrDefault(x => x.Id == Int32.Parse(id));
+ }
+
+ if (originalObject == null)
+ {
+ status = ODataAPIResponseStatus.NotFound;
+ }
+ }
+ catch (Exception ex)
+ {
+ status = ODataAPIResponseStatus.Failure;
+ errorMessage = ex.Message;
+ }
+
+ return status;
+ }
+
+ public override IODataAPIHandler GetNestedHandler(NewOrder parent, string navigationPropertyName)
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ internal class MyNewOrderAPIHandler : ODataAPIHandler
+ {
+ MyNewFriend friend;
+ public MyNewOrderAPIHandler(MyNewFriend friend)
+ {
+ this.friend = friend;
+ }
+
+ public override ODataAPIResponseStatus TryCreate(IDictionary keyValues, out MyNewOrder createdObject, out string errorMessage)
+ {
+ createdObject = null;
+ errorMessage = string.Empty;
+
+ try
+ {
+ createdObject = new MyNewOrder();
+
+ if (friend.MyNewOrders == null)
+ {
+ friend.MyNewOrders = new List();
+ }
+
+ friend.MyNewOrders.Add(createdObject);
+
+ return ODataAPIResponseStatus.Success;
+ }
+ catch (Exception ex)
+ {
+ errorMessage = ex.Message;
+
+ return ODataAPIResponseStatus.Failure;
+ }
+ }
+
+ public override ODataAPIResponseStatus TryDelete(IDictionary keyValues, out string errorMessage)
+ {
+ errorMessage = string.Empty;
+
+ try
+ {
+ var id = keyValues.First().Value.ToString();
+ var friend = this.friend.MyNewOrders.FirstOrDefault(x => x.Id == int.Parse(id));
+
+ this.friend.MyNewOrders.Remove(friend);
+
+ return ODataAPIResponseStatus.Success;
+ }
+ catch (Exception ex)
+ {
+ errorMessage = ex.Message;
+
+ return ODataAPIResponseStatus.Failure;
+ }
+ }
+
+ public override ODataAPIResponseStatus TryGet(IDictionary keyValues, out MyNewOrder originalObject, out string errorMessage)
+ {
+ ODataAPIResponseStatus status = ODataAPIResponseStatus.Success;
+ errorMessage = string.Empty;
+ originalObject = null;
+
+ try
+ {
+ if (friend.MyNewOrders != null)
+ {
+ var id = keyValues["Id"].ToString();
+ originalObject = friend.MyNewOrders.FirstOrDefault(x => x.Id == Int32.Parse(id));
+ }
+
+ if (originalObject == null)
+ {
+ status = ODataAPIResponseStatus.NotFound;
+ }
+ }
+ catch (Exception ex)
+ {
+ status = ODataAPIResponseStatus.Failure;
+ errorMessage = ex.Message;
+ }
+
+ return status;
+ }
+
+ public override IODataAPIHandler GetNestedHandler(MyNewOrder parent, string navigationPropertyName)
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ internal class OrderAPIHandler : ODataAPIHandler
+ {
+ Friend friend;
+ public OrderAPIHandler(Friend friend)
+ {
+ this.friend = friend;
+ }
+
+ public override ODataAPIResponseStatus TryCreate(IDictionary keyValues, out Order createdObject, out string errorMessage)
+ {
+ createdObject = null;
+ errorMessage = string.Empty;
+
+ try
+ {
+ createdObject = new Order();
+
+ if (friend.Orders == null)
+ {
+ friend.Orders = new List();
+ }
+
+ friend.Orders.Add(createdObject);
+
+ return ODataAPIResponseStatus.Success;
+ }
+ catch (Exception ex)
+ {
+ errorMessage = ex.Message;
+
+ return ODataAPIResponseStatus.Failure;
+ }
+ }
+
+ public override ODataAPIResponseStatus TryDelete(IDictionary keyValues, out string errorMessage)
+ {
+ errorMessage = string.Empty;
+
+ try
+ {
+ var id = keyValues.First().Value.ToString();
+ var friend = this.friend.Orders.FirstOrDefault(x => x.Id == int.Parse(id));
+
+ this.friend.Orders.Remove(friend);
+
+ return ODataAPIResponseStatus.Success;
+ }
+ catch (Exception ex)
+ {
+ errorMessage = ex.Message;
+
+ return ODataAPIResponseStatus.Failure;
+ }
+ }
+
+ public override ODataAPIResponseStatus TryGet(IDictionary keyValues, out Order originalObject, out string errorMessage)
+ {
+ ODataAPIResponseStatus status = ODataAPIResponseStatus.Success;
+ errorMessage = string.Empty;
+ originalObject = null;
+
+ try
+ {
+ if (friend.Orders != null)
+ {
+ var id = keyValues["Id"].ToString();
+ originalObject = friend.Orders.FirstOrDefault(x => x.Id == Int32.Parse(id));
+ }
+
+ if (originalObject == null)
+ {
+ status = ODataAPIResponseStatus.NotFound;
+ }
+ }
+ catch (Exception ex)
+ {
+ status = ODataAPIResponseStatus.Failure;
+ errorMessage = ex.Message;
+ }
+
+ return status;
+ }
+
+ public override IODataAPIHandler GetNestedHandler(Order parent, string navigationPropertyName)
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ internal class NewFriendAPIHandler : ODataAPIHandler
+ {
+ Employee employee;
+ public NewFriendAPIHandler(Employee employee)
+ {
+ this.employee = employee;
+ }
+
+ public override ODataAPIResponseStatus TryCreate(IDictionary keyValues, out NewFriend createdObject, out string errorMessage)
+ {
+ createdObject = null;
+ errorMessage = string.Empty;
+
+ try
+ {
+ createdObject = new NewFriend();
+
+ if (employee.NewFriends == null)
+ {
+ employee.NewFriends = new List();
+ }
+
+ employee.NewFriends.Add(createdObject);
+
+ return ODataAPIResponseStatus.Success;
+ }
+ catch (Exception ex)
+ {
+ errorMessage = ex.Message;
+
+ return ODataAPIResponseStatus.Failure;
+ }
+ }
+
+ public override ODataAPIResponseStatus TryDelete(IDictionary keyValues, out string errorMessage)
+ {
+ errorMessage = string.Empty;
+
+ try
+ {
+ var id = keyValues.First().Value.ToString();
+ var friend = employee.NewFriends.First(x => x.Id == Int32.Parse(id));
+
+ employee.NewFriends.Remove(friend);
+
+ return ODataAPIResponseStatus.Success;
+ }
+ catch (Exception ex)
+ {
+ errorMessage = ex.Message;
+
+ return ODataAPIResponseStatus.Failure;
+ }
+ }
+
+ public override ODataAPIResponseStatus TryGet(IDictionary keyValues, out NewFriend originalObject, out string errorMessage)
+ {
+ ODataAPIResponseStatus status = ODataAPIResponseStatus.Success;
+ errorMessage = string.Empty;
+ originalObject = null;
+
+ try
+ {
+ var id = keyValues["Id"].ToString();
+
+ if (employee.NewFriends == null)
+ {
+ return ODataAPIResponseStatus.NotFound;
+ }
+
+ originalObject = employee.NewFriends.FirstOrDefault(x => x.Id == Int32.Parse(id));
+
+ if (originalObject == null)
+ {
+ status = ODataAPIResponseStatus.NotFound;
+ }
+ }
+ catch (Exception ex)
+ {
+ status = ODataAPIResponseStatus.Failure;
+ errorMessage = ex.Message;
+ }
+
+ return status;
+ }
+
+ public override IODataAPIHandler GetNestedHandler(NewFriend parent, string navigationPropertyName)
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ internal class EmployeeEdmAPIHandler : EdmODataAPIHandler
+ {
+ IEdmEntityType entityType;
+ public EmployeeEdmAPIHandler(IEdmEntityType entityType)
+ {
+ this.entityType = entityType;
+ }
+
+ public override ODataAPIResponseStatus TryCreate(IEdmChangedObject changedObject, out IEdmStructuredObject createdObject, out string errorMessage)
+ {
+ createdObject = null;
+ errorMessage = string.Empty;
+
+ try
+ {
+ createdObject = new EdmEntityObject(entityType);
+ EmployeesController.EmployeesTypeless.Add(createdObject as EdmStructuredObject);
+
+ return ODataAPIResponseStatus.Success;
+ }
+ catch (Exception ex)
+ {
+ errorMessage = ex.Message;
+
+ return ODataAPIResponseStatus.Failure;
+ }
+ }
+
+ public override ODataAPIResponseStatus TryDelete(IDictionary keyValues, out string errorMessage)
+ {
+ errorMessage = string.Empty;
+
+ try
+ {
+ var id = keyValues.First().Value.ToString();
+ foreach (var emp in EmployeesController.EmployeesTypeless)
+ {
+ object id1;
+ emp.TryGetPropertyValue("ID", out id1);
+
+ if (id == id1.ToString())
+ {
+ EmployeesController.EmployeesTypeless.Remove(emp);
+ break;
+ }
+ }
+
+ return ODataAPIResponseStatus.Success;
+ }
+ catch (Exception ex)
+ {
+ errorMessage = ex.Message;
+
+ return ODataAPIResponseStatus.Failure;
+ }
+ }
+
+ public override ODataAPIResponseStatus TryGet(IDictionary keyValues, out IEdmStructuredObject originalObject, out string errorMessage)
+ {
+ ODataAPIResponseStatus status = ODataAPIResponseStatus.Success;
+ errorMessage = string.Empty;
+ originalObject = null;
+
+ try
+ {
+ var id = keyValues["ID"].ToString();
+ foreach (var emp in EmployeesController.EmployeesTypeless)
+ {
+ object id1;
+ emp.TryGetPropertyValue("ID", out id1);
+
+ if (id == id1.ToString())
+ {
+ originalObject = emp;
+ break;
+ }
+ }
+
+ if (originalObject == null)
+ {
+ status = ODataAPIResponseStatus.NotFound;
+ }
+ }
+ catch (Exception ex)
+ {
+ status = ODataAPIResponseStatus.Failure;
+ errorMessage = ex.Message;
+ }
+
+ return status;
+ }
+
+ public override EdmODataAPIHandler GetNestedHandler(IEdmStructuredObject parent, string navigationPropertyName)
+ {
+ switch (navigationPropertyName)
+ {
+ case "UnTypedFriends":
+ return new FriendTypelessAPIHandler(parent, entityType.DeclaredNavigationProperties().First().Type.Definition.AsElementType() as IEdmEntityType);
+
+ default:
+ return null;
+ }
+ }
+ }
+
+ internal class FriendTypelessAPIHandler : EdmODataAPIHandler
+ {
+ IEdmEntityType entityType;
+ EdmStructuredObject employee;
+
+ public FriendTypelessAPIHandler(IEdmStructuredObject employee, IEdmEntityType entityType)
+ {
+ this.employee = employee as EdmStructuredObject;
+ this.entityType = entityType;
+ }
+
+ public override ODataAPIResponseStatus TryCreate(IEdmChangedObject changedObject, out IEdmStructuredObject createdObject, out string errorMessage)
+ {
+ createdObject = null;
+ errorMessage = string.Empty;
+
+ try
+ {
+ object empid;
+ if (employee.TryGetPropertyValue("ID", out empid) && empid as int? == 3)
+ {
+ throw new Exception("Testing Error");
+ }
+
+ createdObject = new EdmEntityObject(entityType);
+ object obj;
+ employee.TryGetPropertyValue("UnTypedFriends", out obj);
+
+ var friends = obj as ICollection;
+
+ if (friends == null)
+ {
+ friends = new List();
+ }
+
+ friends.Add(createdObject);
+
+ employee.TrySetPropertyValue("UnTypedFriends", friends);
+
+ return ODataAPIResponseStatus.Success;
+ }
+ catch (Exception ex)
+ {
+ errorMessage = ex.Message;
+
+ return ODataAPIResponseStatus.Failure;
+ }
+ }
+
+ public override ODataAPIResponseStatus TryDelete(IDictionary keyValues, out string errorMessage)
+ {
+ errorMessage = string.Empty;
+
+ try
+ {
+ var id = keyValues.First().Value.ToString();
+ if (id == "5")
+ {
+ throw new Exception("Testing Error");
+ }
+ foreach (var emp in EmployeesController.EmployeesTypeless)
+ {
+ object id1;
+ emp.TryGetPropertyValue("ID", out id1);
+
+ if (id == id1.ToString())
+ {
+ object obj;
+ employee.TryGetPropertyValue("UnTypedFriends", out obj);
+
+ var friends = obj as IList;
+
+ friends.Remove(emp);
+
+ employee.TrySetPropertyValue("UnTypedFriends", friends);
+ break;
+ }
+ }
+
+ return ODataAPIResponseStatus.Success;
+ }
+ catch (Exception ex)
+ {
+ errorMessage = ex.Message;
+
+ return ODataAPIResponseStatus.Failure;
+ }
+ }
+
+ public override ODataAPIResponseStatus TryGet(IDictionary keyValues, out IEdmStructuredObject originalObject, out string errorMessage)
+ {
+ ODataAPIResponseStatus status = ODataAPIResponseStatus.Success;
+ errorMessage = string.Empty;
+ originalObject = null;
+
+ try
+ {
+ var id = keyValues["Id"].ToString();
+ object obj;
+ employee.TryGetPropertyValue("UnTypedFriends", out obj);
+
+ var friends = obj as IList;
+
+ if (friends == null)
+ {
+ return ODataAPIResponseStatus.NotFound;
+ }
+
+ foreach (var friend in friends)
+ {
+ object id1;
+ friend.TryGetPropertyValue("Id", out id1);
+
+ if (id == id1.ToString())
+ {
+ originalObject = friend;
+ break;
+ }
+ }
+
+ if (originalObject == null)
+ {
+ status = ODataAPIResponseStatus.NotFound;
+ }
+ }
+ catch (Exception ex)
+ {
+ status = ODataAPIResponseStatus.Failure;
+ errorMessage = ex.Message;
+ }
+
+ return status;
+ }
+
+ public override EdmODataAPIHandler GetNestedHandler(IEdmStructuredObject parent, string navigationPropertyName)
+ {
+ return null;
+ }
+ }
+}
diff --git a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkOperationTest.cs b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkOperationTest.cs
new file mode 100644
index 0000000000..224bd6df49
--- /dev/null
+++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/BulkOperation/BulkOperationTest.cs
@@ -0,0 +1,1124 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (c) .NET Foundation and Contributors. All rights reserved.
+// See License.txt in the project root for license information.
+//
+//------------------------------------------------------------------------------
+
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.AspNet.OData;
+using Microsoft.AspNet.OData.Extensions;
+using Microsoft.AspNet.OData.Routing;
+using Microsoft.AspNet.OData.Routing.Conventions;
+using Microsoft.Test.E2E.AspNet.OData.Common.Execution;
+using Microsoft.Test.E2E.AspNet.OData.Common.Extensions;
+using Newtonsoft.Json.Linq;
+using Xunit;
+
+namespace Microsoft.Test.E2E.AspNet.OData.BulkOperation
+{
+ public class BulkOperationTest : WebHostTestBase
+ {
+ public BulkOperationTest(WebHostTestFixture fixture)
+ :base(fixture)
+ {
+ }
+
+ protected override void UpdateConfiguration(WebRouteConfiguration configuration)
+ {
+ var controllers = new[] { typeof(EmployeesController), typeof(CompanyController), typeof(MetadataController) };
+ configuration.AddControllers(controllers);
+
+ configuration.Routes.Clear();
+ configuration.Count().Filter().OrderBy().Expand().MaxTop(null).Select();
+ configuration.MapODataServiceRoute("convention", "convention", BulkOperationEdmModel.GetConventionModel(configuration));
+ configuration.MapODataServiceRoute("explicit", "explicit", BulkOperationEdmModel.GetExplicitModel(configuration), new DefaultODataPathHandler(), ODataRoutingConventions.CreateDefault());
+ configuration.EnsureInitialized();
+ }
+
+
+ #region Update
+
+ [Fact]
+ public async Task PatchEmployee_WithUpdates()
+ {
+ //Arrange
+
+ string requestUri = this.BaseAddress + "/convention/Employees(1)";
+
+ var content = @"{
+ 'Name':'Sql' , 'FavoriteSports' :{'Sport': 'Cricket'},
+ 'Friends@odata.delta':[{'Id':1,'Name':'Test2'},{'Id':2,'Name':'Test3'}]
+ }";
+
+ var requestForPost = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri);
+
+ StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json");
+ requestForPost.Content = stringContent;
+
+ //Act
+ using (HttpResponseMessage response = await this.Client.SendAsync(requestForPost))
+ {
+ var json = response.Content.ReadAsStringAsync().Result;
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ }
+
+ //Assert
+ requestUri = this.BaseAddress + "/convention/Employees(1)/Friends";
+ using (HttpResponseMessage response = await this.Client.GetAsync(requestUri))
+ {
+ response.EnsureSuccessStatusCode();
+
+ var json = await response.Content.ReadAsObject();
+ var result = json.GetValue("value") as JArray;
+
+ Assert.Equal(2, result.Count);
+ Assert.Contains("Test2", result.ToString());
+ }
+ }
+
+ [Fact]
+ public async Task PatchEmployee_WithUpdates_WithEmployees()
+ {
+ //Arrange
+
+ string requestUri = this.BaseAddress + "/convention/Employees(1)";
+
+ var content = @"{
+ 'Name':'Sql' ,
+ 'Friends':[{'Id':345,'Name':'Test2'},{'Id':400,'Name':'Test3'},{'Id':900,'Name':'Test93'}]
+ }";
+
+ var requestForPost = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri);
+
+ StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json");
+ requestForPost.Content = stringContent;
+
+ using (HttpResponseMessage response = await this.Client.SendAsync(requestForPost))
+ {
+ var json = response.Content.ReadAsStringAsync().Result;
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ }
+
+ //Assert
+ requestUri = this.BaseAddress + "/convention/Employees(1)/Friends";
+ using (HttpResponseMessage response = await this.Client.GetAsync(requestUri))
+ {
+ response.EnsureSuccessStatusCode();
+
+ var json = await response.Content.ReadAsObject();
+ var result = json.GetValue("value") as JArray;
+
+ Assert.Equal(3, result.Count);
+ Assert.Contains("345", result.ToString());
+ Assert.Contains("400", result.ToString());
+ Assert.Contains("900", result.ToString());
+ }
+ }
+
+ [Fact]
+ public async Task PatchEmployee_WithUpdates_Friends()
+ {
+ //Arrange
+
+ string requestUri = this.BaseAddress + "/convention/Employees(1)/Friends";
+
+ var content = @"{'@odata.type': '#Microsoft.Test.E2E.AspNet.OData.BulkOperation.Friend',
+ '@odata.context':'" + this.BaseAddress + @"/convention/$metadata#Employees(1)/Friends/$delta',
+ 'value':[{ 'Id':1,'Name':'Friend1'}, { 'Id':2,'Name':'Friend2'}]
+ }";
+
+ var requestForPost = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri);
+ requestForPost.Headers.Add("OData-Version", "4.01");
+
+ StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json");
+ requestForPost.Content = stringContent;
+ Client.DefaultRequestHeaders.Add("Prefer", @"odata.include-annotations=""*""");
+
+ var expected = "$delta\",\"value\":[{\"Id\":1,\"Name\":\"Friend1\",\"Age\":0}," +
+ "{\"Id\":2,\"Name\":\"Friend2\",\"Age\":0}]}";
+
+ using (HttpResponseMessage response = await this.Client.SendAsync(requestForPost))
+ {
+ var json = response.Content.ReadAsStringAsync().Result;
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.Contains(expected, json.ToString());
+ }
+
+ //Assert
+ requestUri = this.BaseAddress + "/convention/Employees(1)/Friends";
+ using (HttpResponseMessage response = await this.Client.GetAsync(requestUri))
+ {
+ response.EnsureSuccessStatusCode();
+
+ var json = await response.Content.ReadAsObject();
+ var result = json.GetValue("value") as JArray;
+
+ Assert.Equal(2, result.Count);
+ Assert.Contains("Friend1", result.ToString());
+ Assert.Contains("Friend2", result.ToString());
+ }
+ }
+
+ [Fact]
+ public async Task PatchEmployee_WithDeletes_Friends()
+ {
+ //Arrange
+
+ string requestUri = this.BaseAddress + "/convention/Employees(1)/Friends";
+
+ var content = @"{'@odata.context':'" + this.BaseAddress + @"/convention/$metadata#Employees(1)/Friends/$delta',
+ 'value':[{ '@odata.removed' : {'reason':'changed'}, 'Id':1},{ 'Id':2,'Name':'Friend2'}]
+ }";
+
+ var requestForPost = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri);
+ requestForPost.Headers.Add("OData-Version", "4.01");
+
+ StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json");
+ requestForPost.Content = stringContent;
+
+ var expected = "$delta\",\"value\":[{\"@removed\":{\"reason\":\"changed\"}," +
+ "\"@id\":\""+this.BaseAddress+"/convention/Friends(1)\",\"Id\":1,\"Name\":null,\"Age\":0},{\"Id\":2,\"Name\":\"Friend2\",\"Age\":0}]}";
+
+ using (HttpResponseMessage response = await this.Client.SendAsync(requestForPost))
+ {
+ var json = response.Content.ReadAsStringAsync().Result;
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.Contains(expected.ToLower(), json.ToString().ToLower());
+ }
+
+ //Assert
+ requestUri = this.BaseAddress + "/convention/Employees(1)/Friends";
+ using (HttpResponseMessage response = await this.Client.GetAsync(requestUri))
+ {
+ response.EnsureSuccessStatusCode();
+
+ var json = await response.Content.ReadAsObject();
+ var result = json.GetValue("value") as JArray;
+
+ Assert.Single(result);
+ Assert.Contains("Friend2", result.ToString());
+ }
+ }
+
+ [Fact]
+ public async Task PatchEmployee_WithDeletes_Friends_WithNestedTypes()
+ {
+ //Arrange
+
+ string requestUri = this.BaseAddress + "/convention/Employees(1)/Friends";
+
+ var content = @"{'@odata.context':'" + this.BaseAddress + @"/convention/$metadata#Employees(1)/Friends/$delta', '@odata.type': '#Microsoft.Test.E2E.AspNet.OData.BulkOperation.Friend',
+ 'value':[{ '@odata.removed' : {'reason':'changed'}, 'Id':1, 'Orders@odata.delta' :[{'Id':1,'Price': 10}, {'Id':2,'Price': 20} ] },{ 'Id':2,'Name':'Friend2'}]
+ }";
+
+ var requestForPost = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri);
+ requestForPost.Headers.Add("OData-Version", "4.01");
+
+ StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json");
+ requestForPost.Content = stringContent;
+
+ var expected = "$delta\",\"value\":[{\"@removed\":{\"reason\":\"changed\"}," +
+ "\"@id\":\""+this.BaseAddress+"/convention/Friends(1)\",\"Id\":1,\"Name\":null,\"Age\":0},{\"Id\":2,\"Name\":\"Friend2\",\"Age\":0}]}";
+
+ using (HttpResponseMessage response = await this.Client.SendAsync(requestForPost))
+ {
+ var json = response.Content.ReadAsStringAsync().Result;
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.Contains(expected.ToLower(), json.ToString().ToLower());
+ }
+
+ //Assert
+ requestUri = this.BaseAddress + "/convention/Employees(1)/Friends";
+ using (HttpResponseMessage response = await this.Client.GetAsync(requestUri))
+ {
+ response.EnsureSuccessStatusCode();
+
+ var json = await response.Content.ReadAsObject();
+ var result = json.GetValue("value") as JArray;
+
+ Assert.Single(result);
+ Assert.Contains("Friend2", result.ToString());
+ }
+ }
+
+ [Fact]
+ public async Task PatchEmployee_WithDeletes_Friends_WithNestedDeletes()
+ {
+ //Arrange
+
+ string requestUri = this.BaseAddress + "/convention/Employees(1)/Friends";
+
+ var content = @"{'@odata.context':'" + this.BaseAddress + @"/convention/$metadata#Employees(1)/Friends/$delta', '@odata.type': '#Microsoft.Test.E2E.AspNet.OData.BulkOperation.Friend',
+ 'value':[{ '@odata.removed' : {'reason':'changed'}, 'Id':1, 'Orders@odata.delta' :[{'@odata.removed' : {'reason':'changed'}, 'Id':1,'Price': 10}, {'Id':2,'Price': 20} ] },{ 'Id':2,'Name':'Friend2'}]
+ }";
+
+ var requestForPost = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri);
+ requestForPost.Headers.Add("OData-Version", "4.01");
+
+ StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json");
+ requestForPost.Content = stringContent;
+
+ //Act & Assert
+ var expected = "$delta\",\"value\":[{\"@removed\":{\"reason\":\"changed\"}," +
+ "\"@id\":\""+ this.BaseAddress +"/convention/Friends(1)\",\"Id\":1,\"Name\":null,\"Age\":0},{\"Id\":2,\"Name\":\"Friend2\",\"Age\":0}]}";
+
+ using (HttpResponseMessage response = await this.Client.SendAsync(requestForPost))
+ {
+ var json = response.Content.ReadAsStringAsync().Result;
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.Contains(expected.ToLower(), json.ToString().ToLower());
+ }
+
+ requestUri = this.BaseAddress + "/convention/Employees(1)/Friends";
+ using (HttpResponseMessage response = await this.Client.GetAsync(requestUri))
+ {
+ response.EnsureSuccessStatusCode();
+
+ var json = await response.Content.ReadAsObject();
+ var result = json.GetValue("value") as JArray;
+
+ Assert.Contains("Friend2", result.ToString());
+ }
+ }
+
+ [Fact]
+ public async Task PatchEmployee_WithAdds_Friends_WithAnnotations()
+ {
+ //Arrange
+
+ string requestUri = this.BaseAddress + "/convention/Employees(1)/NewFriends";
+ var content = @"{'@odata.context':'" + this.BaseAddress + @"/convention/$metadata#Employees(1)/NewFriends/$delta',
+ 'value':[{ 'Id':3, 'Age':35, '@NS.Test':1}]
+ }";
+
+ var requestForPost = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri);
+ requestForPost.Headers.Add("OData-Version", "4.01");
+ requestForPost.Content = new StringContent(content);
+
+ requestForPost.Content.Headers.ContentType= MediaTypeWithQualityHeaderValue.Parse("application/json");
+
+ Client.DefaultRequestHeaders.Add("Prefer", @"odata.include-annotations=""*""");
+
+ var expected = "$delta\",\"value\":[{\"@NS.Test\":1,\"Id\":3,\"Name\":null,\"Age\":35}]}";
+
+ using (HttpResponseMessage response = await this.Client.SendAsync(requestForPost))
+ {
+ var json = response.Content.ReadAsStringAsync().Result;
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.Contains(expected, json.ToString());
+ }
+ }
+
+ [Fact]
+ public async Task PatchEmployee_WithFailedAdds_Friends()
+ {
+ //Arrange
+
+ string requestUri = this.BaseAddress + "/convention/Employees(1)/NewFriends";
+ var content = @"{'@odata.context':'" + this.BaseAddress + @"/convention/$metadata#Employees(1)/NewFriends/$delta',
+ 'value':[{ 'Id':3, 'Age':3, '@NS.Test':1}]
+ }";
+
+ var requestForPost = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri);
+ requestForPost.Headers.Add("OData-Version", "4.01");
+
+ StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json");
+ requestForPost.Content = stringContent;
+ Client.DefaultRequestHeaders.Add("Prefer", @"odata.include-annotations=""*""");
+
+ var expected = "$delta\",\"value\":[{\"@NS.Test\":1,\"Id\":3,\"Name\":null,\"Age\":3}]}";
+
+ using (HttpResponseMessage response = await this.Client.SendAsync(requestForPost))
+ {
+ var json = response.Content.ReadAsStringAsync().Result;
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.Contains(expected, json.ToString());
+ }
+ }
+
+ [Fact]
+ public async Task PatchEmployee_WithFailedDeletes_Friends()
+ {
+ //Arrange
+ string requestUri = this.BaseAddress + "/convention/Employees(2)/NewFriends";
+ var content = @"{'@odata.context':'" + this.BaseAddress + @"/convention/$metadata#Employees(1)/NewFriends/$delta',
+ 'value':[{ '@odata.removed' : {'reason':'changed'}, 'Id':2, '@NS.Test':1}]
+ }";
+
+ var requestForPost = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri);
+ requestForPost.Headers.Add("OData-Version", "4.01");
+
+ StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json");
+ requestForPost.Content = stringContent;
+ Client.DefaultRequestHeaders.Add("Prefer", @"odata.include-annotations=""*""");
+
+ var expected = "$delta\",\"value\":[{\"@NS.Test\":1,\"@Core.DataModificationException\":" +
+ "{\"@type\":\"#Org.OData.Core.V1.DataModificationExceptionType\"},\"Id\":2,\"Name\":null,\"Age\":15}]}";
+
+ using (HttpResponseMessage response = await this.Client.SendAsync(requestForPost))
+ {
+ var json = response.Content.ReadAsStringAsync().Result;
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.Contains("$delta", json);
+ Assert.Contains(expected, json.ToString());
+ }
+ }
+
+ [Fact]
+ public async Task PatchEmployee_WithFailedOperation_WithAnnotations()
+ {
+ //Arrange
+ string requestUri = this.BaseAddress + "/convention/Employees(2)/NewFriends";
+ var content = @"{'@odata.context':'" + this.BaseAddress + @"/convention/$metadata#Employees(2)/NewFriends/$delta',
+ 'value':[{ '@odata.removed' : {'reason':'changed'}, 'Id':2, '@Core.ContentID':3, '@NS.Test2':'testing'}]
+ }";
+
+ var requestForPost = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri);
+ requestForPost.Headers.Add("OData-Version", "4.01");
+
+ StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json");
+ requestForPost.Content = stringContent;
+ Client.DefaultRequestHeaders.Add("Prefer", @"odata.include-annotations=""*""");
+
+ var expected = "/convention/$metadata#NewFriends/$delta\",\"value\":[{\"@NS.Test2\":\"testing\",\"@Core.ContentID\":3," +
+ "\"@Core.DataModificationException\":{\"@type\":\"#Org.OData.Core.V1.DataModificationExceptionType\"},\"Id\":2,\"Name\":null,\"Age\":15}]}";
+
+ using (HttpResponseMessage response = await this.Client.SendAsync(requestForPost))
+ {
+ var json = response.Content.ReadAsStringAsync().Result;
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ var str = json.ToString();
+ Assert.Contains("$delta",str);
+ Assert.Contains("NS.Test2", str);
+ Assert.Contains("Core.DataModificationException", str);
+ Assert.Contains(expected, str);
+ }
+ }
+
+ [Fact]
+ public async Task PatchUntypedEmployee_WithAdds_Friends_Untyped()
+ {
+ //Arrange
+
+ string requestUri = this.BaseAddress + "/convention/UnTypedEmployees";
+ var content = @"{'@odata.context':'" + this.BaseAddress + @"/convention/$metadata#Employees(2)/UnTypedFriends/$delta',
+ 'value':[{ 'Id':3, 'Age':35,}]
+ }";
+
+ content = @"{'@odata.context':'" + this.BaseAddress + @"/convention/$metadata#UnTypedEmployees/$delta',
+ 'value':[{ 'ID':1,'Name':'Employee1',
+ 'UnTypedFriends@odata.delta':[{'Id':1,'Name':'Friend1'},{'Id':2,'Name':'Friend2'}]
+ },
+ { 'ID':2,'Name':'Employee2',
+ 'UnTypedFriends@odata.delta':[{'Id':3,'Name':'Friend3'},{'Id':4,'Name':'Friend4'}]
+ }]
+ }";
+
+ var requestForPost = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri);
+ requestForPost.Headers.Add("OData-Version", "4.01");
+
+ StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json");
+ requestForPost.Content = stringContent;
+ Client.DefaultRequestHeaders.Add("Prefer", @"odata.include-annotations=""*""");
+
+ var expected = "/convention/$metadata#UnTypedEmployees/$delta\",\"value\":[{\"ID\":1,\"Name\":\"Employee1\",\"UnTypedFriends@delta\":" +
+ "[{\"Id\":1,\"Name\":\"Friend1\",\"Age\":0},{\"Id\":2,\"Name\":\"Friend2\",\"Age\":0}]},{\"ID\":2,\"Name\":\"Employee2\",\"UnTypedFriends@delta\":" +
+ "[{\"Id\":3,\"Name\":\"Friend3\",\"Age\":0},{\"Id\":4,\"Name\":\"Friend4\",\"Age\":0}]}]}";
+
+ using (HttpResponseMessage response = await this.Client.SendAsync(requestForPost))
+ {
+ var json = response.Content.ReadAsStringAsync().Result;
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.Contains(expected, json.ToString());
+ }
+ }
+
+ [Fact]
+ public async Task PatchEmployee_WithAdds_Friends_WithNested_Untyped()
+ {
+ //Arrange
+
+ string requestUri = this.BaseAddress + "/convention/Employees(1)/UnTypedFriends";
+ var content = @"{'@odata.context':'" + this.BaseAddress + @"/convention/$metadata#Employees(1)/UnTypedFriends/$delta',
+ 'value':[{ 'Id':2, 'Name': 'Friend007', 'Age':35,'Address@odata.delta':{'Id':1, 'Street' : 'Abc 123'}, '@NS.Test':1}]
+ }";
+
+ var requestForPost = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri);
+
+ StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json");
+ requestForPost.Content = stringContent;
+ Client.DefaultRequestHeaders.Add("Prefer", @"odata.include-annotations=""*""");
+
+ using (HttpResponseMessage response = await this.Client.SendAsync(requestForPost))
+ {
+ var json = response.Content.ReadAsStringAsync().Result;
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ json.ToString().Contains("$delta");
+ json.ToString().Contains("@NS.Test");
+ }
+ }
+
+ [Fact]
+ public async Task PatchEmployee_WithAdds_Friends_WithAnnotations_Untyped()
+ {
+ //Arrange
+
+ string requestUri = this.BaseAddress + "/convention/Employees(2)/UnTypedFriends";
+ var content = @"{'@odata.context':'" + this.BaseAddress + @"/convention/$metadata#Employees(2)/UnTypedFriends/$delta',
+ 'value':[{ 'Id':2, 'Age':35, '@NS.Test':1}]
+ }";
+
+ var requestForPost = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri);
+
+ StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json");
+ requestForPost.Content = stringContent;
+ Client.DefaultRequestHeaders.Add("Prefer", @"odata.include-annotations=""*""");
+
+ using (HttpResponseMessage response = await this.Client.SendAsync(requestForPost))
+ {
+ var json = response.Content.ReadAsStringAsync().Result;
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ json.ToString().Contains("$delta");
+ json.ToString().Contains("@NS.Test");
+ }
+ }
+
+ [Fact]
+ public async Task PatchEmployee_WithFailedAdds_Friends_Untyped()
+ {
+ //Arrange
+
+ string requestUri = this.BaseAddress + "/convention/Employees(3)/UnTypedFriends";
+ var content = @"{'@odata.context':'" + this.BaseAddress + @"/convention/$metadata#Employees(3)/UnTypedFriends/$delta',
+ 'value':[{ 'Id':3, 'Age':3, '@NS.Test':1}]
+ }";
+
+ var requestForPost = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri);
+
+ StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json");
+ requestForPost.Content = stringContent;
+ Client.DefaultRequestHeaders.Add("Prefer", @"odata.include-annotations=""*""");
+
+ using (HttpResponseMessage response = await this.Client.SendAsync(requestForPost))
+ {
+ var json = response.Content.ReadAsStringAsync().Result;
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ json.ToString().Contains("$deletedEntity");
+ }
+ }
+
+ [Fact]
+ public async Task PatchEmployee_WithFailedDeletes_Friends_Untyped()
+ {
+ //Arrange
+ string requestUri = this.BaseAddress + "/convention/Employees(3)/UnTypedFriends";
+ //{ '@odata.removed' : {'reason':'changed'}, 'Id':1},{ '@odata.removed' : {'reason':'deleted'}, 'Id':2},
+ var content = @"{'@odata.context':'" + this.BaseAddress + @"/convention/$metadata#Employees(3)/UnTypedFriends/$delta',
+ 'value':[{ '@odata.removed' : {'reason':'changed'}, 'Id':5, '@NS.Test':1}]
+ }";
+
+ var requestForPost = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri);
+
+ StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json");
+ requestForPost.Content = stringContent;
+ Client.DefaultRequestHeaders.Add("Prefer", @"odata.include-annotations=""*""");
+
+ using (HttpResponseMessage response = await this.Client.SendAsync(requestForPost))
+ {
+ var json = response.Content.ReadAsStringAsync().Result;
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.Contains("@Core.DataModificationException", json.ToString());
+ Assert.Contains("@NS.Test", json.ToString());
+ }
+ }
+
+ [Fact]
+ public async Task PatchEmployee_WithFailedOperation_WithAnnotations_Untyped()
+ {
+ //Arrange
+ string requestUri = this.BaseAddress + "/convention/Employees(3)/UnTypedFriends";
+ var content = @"{'@odata.context':'" + this.BaseAddress + @"/convention/$metadata#Employees(3)/UnTypedFriends/$delta',
+ 'value':[{ '@odata.removed' : {'reason':'changed'}, 'Id':5, '@Core.ContentID':3, '@NS.Test2':'testing'}]
+ }";
+
+ var requestForPost = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri);
+
+ StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json");
+ requestForPost.Content = stringContent;
+ Client.DefaultRequestHeaders.Add("Prefer", @"odata.include-annotations=""*""");
+
+ using (HttpResponseMessage response = await this.Client.SendAsync(requestForPost))
+ {
+ var json = response.Content.ReadAsStringAsync().Result;
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ var str = json.ToString();
+ Assert.Contains("$delta", str);
+ Assert.Contains("NS.Test2", str);
+ Assert.Contains("Core.DataModificationException", str);
+ Assert.Contains("Core.ContentID", str);
+ }
+ }
+
+ [Fact]
+ public async Task PatchEmployee_WithUnchanged_Employee()
+ {
+ //Arrange
+
+ string requestUri = this.BaseAddress + "/convention/Employees";
+
+ var content = @"{'@odata.context':'" + this.BaseAddress + @"/convention/$metadata#Employees/$delta',
+ 'value':[{ '@odata.type': '#Microsoft.Test.E2E.AspNet.OData.BulkOperation.Employee', 'ID':1,'Name':'Name1',
+ 'Friends@odata.delta':[{'Id':1,'Name':'Test0','Age':33},{'Id':2,'Name':'Test1'}]
+ }]
+ }";
+
+ var requestForPost = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri);
+ requestForPost.Headers.Add("OData-Version", "4.01");
+
+ StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json");
+ requestForPost.Content = stringContent;
+
+ var expected = "\"value\":[{\"ID\":1,\"Name\":\"Name1\",\"SkillSet\":[],\"Gender\":\"0\",\"AccessLevel\":\"0\",\"FavoriteSports\":null," +
+ "\"Friends@delta\":[{\"Id\":1,\"Name\":\"Test0\",\"Age\":33}," +
+ "{\"Id\":2,\"Name\":\"Test1\",\"Age\":0}]}]}";
+
+ using (HttpResponseMessage response = await this.Client.SendAsync(requestForPost))
+ {
+ var json = response.Content.ReadAsStringAsync().Result;
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.Contains(expected, json.ToString());
+
+ // Navigation properties absent from the payload are not serialized in the response.
+ Assert.DoesNotContain("NewFriends@delta", json.ToString());
+ Assert.DoesNotContain("UntypedFriends@delta", json.ToString());
+ }
+ }
+
+ [Fact]
+ public async Task PatchEmployee_WithUpdates_Employees()
+ {
+ //Arrange
+
+ string requestUri = this.BaseAddress + "/convention/Employees";
+
+ var content = @"{'@odata.context':'"+ this.BaseAddress + @"/convention/$metadata#Employees/$delta',
+ 'value':[{ '@odata.type': '#Microsoft.Test.E2E.AspNet.OData.BulkOperation.Employee', 'ID':1,'Name':'Employee1',
+ 'Friends@odata.delta':[{'Id':1,'Name':'Friend1',
+ 'Orders@odata.delta' :[{'Id':1,'Price': 10}, {'Id':2,'Price': 20} ] },{'Id':2,'Name':'Friend2'}]
+ },
+ { '@odata.type': '#Microsoft.Test.E2E.AspNet.OData.BulkOperation.Employee', 'ID':2,'Name':'Employee2',
+ 'Friends@odata.delta':[{'Id':3,'Name':'Friend3',
+ 'Orders@odata.delta' :[{'Id':3,'Price': 30}, {'Id':4,'Price': 40} ]},{'Id':4,'Name':'Friend4'}]
+ }]
+ }";
+
+ var requestForPost = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri);
+ requestForPost.Headers.Add("OData-Version", "4.01");
+
+ StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json");
+ requestForPost.Content = stringContent;
+
+ var expected = "\"value\":[{\"ID\":1,\"Name\":\"Employee1\",\"SkillSet\":[],\"Gender\":\"0\",\"AccessLevel\":\"0\",\"FavoriteSports\":null," +
+ "\"Friends@delta\":[{\"Id\":1,\"Name\":\"Friend1\",\"Age\":0,\"Orders@delta\":[{\"Id\":1,\"Price\":10},{\"Id\":2,\"Price\":20}]}," +
+ "{\"Id\":2,\"Name\":\"Friend2\",\"Age\":0}]},{\"ID\":2,\"Name\":\"Employee2\",\"SkillSet\":[],\"Gender\":\"0\",\"AccessLevel\":\"0\",\"FavoriteSports\":null," +
+ "\"Friends@delta\":[{\"Id\":3,\"Name\":\"Friend3\",\"Age\":0,\"Orders@delta\":[{\"Id\":3,\"Price\":30},{\"Id\":4,\"Price\":40}]},{\"Id\":4,\"Name\":\"Friend4\",\"Age\":0}]}]}";
+
+ using (HttpResponseMessage response = await this.Client.SendAsync(requestForPost))
+ {
+ var json = response.Content.ReadAsStringAsync().Result;
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.Contains(expected, json.ToString());
+ Assert.DoesNotContain("NewFriends@delta", json.ToString());
+ Assert.DoesNotContain("UntypedFriends@delta", json.ToString());
+ }
+
+ //Assert
+ requestUri = this.BaseAddress + "/convention/Employees(1)/Friends";
+ using (HttpResponseMessage response = await this.Client.GetAsync(requestUri))
+ {
+ response.EnsureSuccessStatusCode();
+
+ var json = response.Content.ReadAsStringAsync().Result;
+
+ Assert.Contains("Friend1", json.ToString());
+ Assert.Contains("Friend2", json.ToString());
+ }
+
+ requestUri = this.BaseAddress + "/convention/Employees(2)?$expand=Friends";
+ using (HttpResponseMessage response = await this.Client.GetAsync(requestUri))
+ {
+ response.EnsureSuccessStatusCode();
+
+ var json = response.Content.ReadAsStringAsync().Result;
+
+ Assert.Contains("Friend3", json.ToString());
+ Assert.Contains("Friend4", json.ToString());
+ }
+ }
+
+ [Fact]
+ public async Task PatchEmployee_WithDelete()
+ {
+ //Arrange
+
+ string requestUri = this.BaseAddress + "/convention/Employees(1)";
+
+ var content = @"{
+ 'Name':'Sql' ,
+ 'Friends@odata.delta':[{ '@odata.removed' : {'reason':'changed'}, 'Id':1}]
+ }";
+
+ var requestForPost = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri);
+
+ StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json");
+ requestForPost.Content = stringContent;
+
+ var expected = "/convention/$metadata#Employees/$entity\",\"ID\":1,\"Name\":\"Sql\"," +
+ "\"SkillSet\":[\"CSharp\",\"Sql\"],\"Gender\":\"Female\",\"AccessLevel\":\"Execute\",\"FavoriteSports\":{\"Sport\":\"Football\"}}";
+
+ using (HttpResponseMessage response = await this.Client.SendAsync(requestForPost))
+ {
+ var json = response.Content.ReadAsStringAsync().Result;
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.Contains(expected, json.ToString());
+ }
+
+ //Assert
+ requestUri = this.BaseAddress + "/convention/Employees(1)/Friends";
+ using (HttpResponseMessage response = await this.Client.GetAsync(requestUri))
+ {
+ response.EnsureSuccessStatusCode();
+
+ var json = await response.Content.ReadAsObject();
+ var result = json.GetValue("value") as JArray;
+
+ Assert.Single(result);
+ Assert.DoesNotContain("Test0", result.ToString());
+ }
+ }
+
+ [Fact]
+ public async Task PatchEmployee_WithODataBind()
+ {
+ //Arrange
+
+ string requestUri = this.BaseAddress + "/convention/Employees(1)?$expand=Friends";
+
+ var content = @"{
+ 'Name':'Bind1' ,
+ 'Friends@odata.bind':['Friends(3)']
+ }";
+
+ var requestForPost = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri);
+
+ StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json");
+ requestForPost.Content = stringContent;
+
+ var expected = "/convention/$metadata#Employees(Friends())/$entity\",\"ID\":1,\"Name\":\"Bind1\"," +
+ "\"SkillSet\":[\"CSharp\",\"Sql\"],\"Gender\":\"Female\",\"AccessLevel\":\"Execute\",\"FavoriteSports\":{\"Sport\":\"Football\"},\"Friends\":[{\"Id\":3,\"Name\":null,\"Age\":0}]}";
+
+ using (HttpResponseMessage response = await this.Client.SendAsync(requestForPost))
+ {
+ var json = response.Content.ReadAsStringAsync().Result;
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.Contains(expected, json.ToString());
+ }
+ }
+
+ [Fact]
+ public async Task PatchEmployee_WithAddUpdateAndDelete()
+ {
+ //Arrange
+
+ string requestUri = this.BaseAddress + "/convention/Employees(1)";
+
+ var content = @"{
+ 'Name':'Sql' ,
+ 'Friends@odata.delta':[{ '@odata.removed' : {'reason':'changed'}, 'Id':1},{'Id':2,'Name':'Test3'},{'Id':3,'Name':'Test4'}]
+ }";
+
+ var requestForPost = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri);
+
+ StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json");
+ requestForPost.Content = stringContent;
+
+ using (HttpResponseMessage response = await this.Client.SendAsync(requestForPost))
+ {
+ var json = response.Content.ReadAsStringAsync().Result;
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ }
+
+ //Assert
+ requestUri = this.BaseAddress + "/convention/Employees(1)/Friends";
+ using (HttpResponseMessage response = await this.Client.GetAsync(requestUri))
+ {
+ response.EnsureSuccessStatusCode();
+
+ var json = await response.Content.ReadAsObject();
+ var result = json.GetValue("value") as JArray;
+
+ Assert.Equal(2, result.Count);
+ Assert.DoesNotContain("Test0", result.ToString());
+ Assert.Contains("Test3", result.ToString());
+ Assert.Contains("Test4", result.ToString());
+ }
+ }
+
+ [Fact]
+ public async Task PatchEmployee_WithMultipleUpdatesinOrder1()
+ {
+ //Arrange
+
+ string requestUri = this.BaseAddress + "/convention/Employees(1)";
+
+ var content = @"{
+ 'Name':'Sql' ,
+ 'Friends@odata.delta':[{ '@odata.removed' : {'reason':'changed'}, 'Id':1},{'Id':1,'Name':'Test_1'},{'Id':2,'Name':'Test3'},{'Id':3,'Name':'Test4'}]
+ }";
+
+ var requestForPost = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri);
+
+ StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json");
+ requestForPost.Content = stringContent;
+
+ using (HttpResponseMessage response = await this.Client.SendAsync(requestForPost))
+ {
+ var json = response.Content.ReadAsStringAsync().Result;
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ }
+
+ //Assert
+ requestUri = this.BaseAddress + "/convention/Employees(1)/Friends";
+ using (HttpResponseMessage response = await this.Client.GetAsync(requestUri))
+ {
+ response.EnsureSuccessStatusCode();
+
+ var json = await response.Content.ReadAsObject();
+ var result = json.GetValue("value") as JArray;
+
+ Assert.Equal(3, result.Count);
+ Assert.DoesNotContain("Test0", result.ToString());
+ Assert.Contains("Test_1", result.ToString());
+ Assert.Contains("Test3", result.ToString());
+ Assert.Contains("Test4", result.ToString());
+ }
+ }
+
+ [Fact]
+ public async Task PatchEmployee_WithMultipleUpdatesinOrder2()
+ {
+ //Arrange
+
+ string requestUri = this.BaseAddress + "/convention/Employees(1)";
+
+ var content = @"{
+ 'Name':'Sql' ,
+ 'Friends@odata.delta':[{ '@odata.removed' : {'reason':'changed'}, 'Id':1},{'Id':1,'Name':'Test_1'},{'Id':2,'Name':'Test3'},{'Id':3,'Name':'Test4'},{ '@odata.removed' : {'reason':'changed'}, 'Id':1}]
+ }";
+
+ var requestForPost = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri);
+
+ StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json");
+ requestForPost.Content = stringContent;
+
+ using (HttpResponseMessage response = await this.Client.SendAsync(requestForPost))
+ {
+ var json = response.Content.ReadAsStringAsync().Result;
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ }
+
+ //Assert
+ requestUri = this.BaseAddress + "/convention/Employees(1)/Friends";
+ using (HttpResponseMessage response = await this.Client.GetAsync(requestUri))
+ {
+ response.EnsureSuccessStatusCode();
+
+ var json = await response.Content.ReadAsObject();
+ var result = json.GetValue("value") as JArray;
+
+ Assert.Equal(2, result.Count);
+ Assert.DoesNotContain("Test0", result.ToString());
+ Assert.DoesNotContain("Test_1", result.ToString());
+ Assert.Contains("Test3", result.ToString());
+ Assert.Contains("Test4", result.ToString());
+ }
+ }
+
+ [Fact]
+ public async Task PatchCompanies_WithUpdates_ODataId()
+ {
+ //Arrange
+
+ string requestUri = this.BaseAddress + "/convention/Companies";
+
+ var content = @"{'@odata.context':'" + this.BaseAddress + @"/convention/$metadata#Companies/$delta',
+ 'value':[{ '@odata.type': '#Microsoft.Test.E2E.AspNet.OData.BulkOperation.Company', 'Id':1,'Name':'Company01',
+ 'OverdueOrders@odata.delta':[{'@odata.id':'Employees(1)/NewFriends(1)/NewOrders(1)', 'Quantity': 9}]
+ }]
+ }";
+
+ var requestForPost = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri);
+ requestForPost.Headers.Add("OData-Version", "4.01");
+
+ StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json");
+ requestForPost.Content = stringContent;
+
+ //Act & Assert
+ var expected = "/convention/$metadata#Companies/$delta\",\"value\":[{\"Id\":1,\"Name\":\"Company01\",\"OverdueOrders@delta\":" +
+ "[{\"Id\":0,\"Price\":0,\"Quantity\":9,\"Container\":null}]}]}";
+
+ using (HttpResponseMessage response = await this.Client.SendAsync(requestForPost))
+ {
+ var json = response.Content.ReadAsStringAsync().Result;
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.Contains(expected, json.ToString());
+ }
+ }
+
+ [Fact]
+ public async Task PatchCompanies_WithUpdates_ODataId_WithCast()
+ {
+ //Arrange
+
+ string requestUri = this.BaseAddress + "/convention/Companies";
+
+ var content = @"{'@odata.context':'" + this.BaseAddress + @"/convention/$metadata#Companies/$delta',
+ 'value':[{ '@odata.type': '#Microsoft.Test.E2E.AspNet.OData.BulkOperation.Company', 'Id':1,'Name':'Company02',
+ 'MyOverdueOrders@odata.delta':[{'@odata.id':'Employees(2)/NewFriends(2)/Microsoft.Test.E2E.AspNet.OData.BulkOperation.MyNewFriend/MyNewOrders(2)', 'Quantity': 9}]
+ }]
+ }";
+
+ var requestForPost = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri);
+ requestForPost.Headers.Add("OData-Version", "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\":\"Company02\",\"MyOverdueOrders@delta\":" +
+ "[{\"Id\":0,\"Price\":0,\"Quantity\":9,\"Container\":null}]}]}";
+
+ using (HttpResponseMessage response = await this.Client.SendAsync(requestForPost))
+ {
+ var json = response.Content.ReadAsStringAsync().Result;
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.Contains(expected, json.ToString());
+ }
+ }
+
+ [Fact]
+ public async Task PatchUntypedEmployee_WithOdataId()
+ {
+ //Arrange
+
+ string requestUri = this.BaseAddress + "/convention/UnTypedEmployees";
+
+ var content = @"{'@odata.context':'" + this.BaseAddress + @"/convention/$metadata#UnTypedEmployees/$delta',
+ 'value':[{ 'ID':1,'Name':'Employeeabcd',
+ 'UnTypedFriends@odata.delta':[{'@odata.id':'UnTypedEmployees(1)/UnTypedFriends(1)', 'Name':'abcd'}]
+ }]
+ }";
+
+ var requestForPost = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri);
+ requestForPost.Headers.Add("OData-Version", "4.01");
+
+ StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json");
+ requestForPost.Content = stringContent;
+ Client.DefaultRequestHeaders.Add("Prefer", @"odata.include-annotations=""*""");
+
+ //Act & Assert
+ var expected = "/convention/$metadata#UnTypedEmployees/$delta\",\"value\":[{\"ID\":1,\"Name\":\"Employeeabcd\"," +
+ "\"UnTypedFriends@delta\":[{\"Id\":0,\"Name\":\"abcd\",\"Age\":0}]}]}";
+
+ using (HttpResponseMessage response = await this.Client.SendAsync(requestForPost))
+ {
+ var json = response.Content.ReadAsStringAsync().Result;
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.Contains(expected, json.ToString());
+ }
+ }
+
+ #endregion
+
+ #region Post
+ [Fact]
+ public async Task PostCompany_WithODataId()
+ {
+ //Arrange
+
+ string requestUri = this.BaseAddress + "/convention/Companies";
+
+ var content = @"{'Id':3,'Name':'Company03',
+ 'OverdueOrders':[{'@odata.id':'Employees(1)/NewFriends(1)/NewOrders(1)'}]
+ }";
+
+ var requestForPost = new HttpRequestMessage(new HttpMethod("POST"), requestUri);
+
+ StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json");
+ requestForPost.Content = stringContent;
+
+ //Act & Assert
+ using (HttpResponseMessage response = await this.Client.SendAsync(requestForPost))
+ {
+ var json = response.Content.ReadAsStringAsync().Result;
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ }
+ }
+
+ [Fact]
+ public async Task PostCompany_WithODataId_AndWithout()
+ {
+ //Arrange
+
+ string requestUri = this.BaseAddress + "/convention/Companies";
+
+ var content = @"{'Id':4,'Name':'Company04',
+ 'OverdueOrders':[{'@odata.id':'Employees(1)/NewFriends(1)/NewOrders(1)'},{Price:30}]
+ }";
+
+ var requestForPost = new HttpRequestMessage(new HttpMethod("POST"), requestUri);
+
+ StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json");
+ requestForPost.Content = stringContent;
+
+ //Act & Assert
+ using (HttpResponseMessage response = await this.Client.SendAsync(requestForPost))
+ {
+ var json = response.Content.ReadAsStringAsync().Result;
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ }
+ }
+
+ [Fact]
+ public async Task PostEmployee_WithCreateFriends()
+ {
+ //Arrange
+
+ string requestUri = this.BaseAddress + "/convention/Employees";
+
+ var content = @"{
+ 'Name':'SqlUD',
+ 'Friends':[{ 'Id':1001, 'Name' : 'Friend 1001', 'Age': 31},{ 'Id':1002, 'Name' : 'Friend 1002', 'Age': 32},{ 'Id':1003, 'Name' : 'Friend 1003', 'Age': 33}]
+ }";
+
+ var requestForPatch = new HttpRequestMessage(new HttpMethod("POST"), requestUri);
+
+ StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json");
+ requestForPatch.Content = stringContent;
+
+ var expected = "Friends\":[{\"Id\":1001,\"Name\":\"Friend 1001\",\"Age\":31},{\"Id\":1002,\"Name\":\"Friend 1002\",\"Age\":32},{\"Id\":1003,\"Name\":\"Friend 1003\",\"Age\":33}]";
+
+ //Act & Assert
+ using (HttpResponseMessage response = await this.Client.SendAsync(requestForPatch))
+ {
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ var json = response.Content.ReadAsStringAsync().Result;
+ Assert.Contains("SqlUD", json);
+ Assert.Contains(expected, json);
+ }
+ }
+
+ [Fact]
+ public async Task PostEmployee_WithCreateFriendsFullMetadata()
+ {
+ //Arrange
+
+ string requestUri = this.BaseAddress + "/convention/Employees?$format=application/json;odata.metadata=full";
+
+ string content = @"{
+ 'Name':'SqlUD',
+ 'Friends':[{ 'Id':1001, 'Name' : 'Friend 1001', 'Age': 31},{ 'Id':1002, 'Name' : 'Friend 1002', 'Age': 32},{ 'Id':1003, 'Name' : 'Friend 1003', 'Age': 33}]
+ }";
+
+ var requestForPatch = new HttpRequestMessage(new HttpMethod("POST"), requestUri);
+
+ StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json");
+ requestForPatch.Content = stringContent;
+
+ string friendsNavigationLink = "Friends@odata.navigationLink";
+ string newFriendsNavigationLink = "NewFriends@odata.navigationLink";
+ string untypedFriendsNavigationLink = "UnTypedFriends@odata.navigationLink";
+
+ string expected = "Friends\":[{\"@odata.type\":\"#Microsoft.Test.E2E.AspNet.OData.BulkOperation.Friend\"";
+
+ //Act & Assert
+ using (HttpResponseMessage response = await this.Client.SendAsync(requestForPatch))
+ {
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ var json = response.Content.ReadAsStringAsync().Result;
+ Assert.Contains("SqlUD", json);
+ Assert.Contains(expected, json);
+ Assert.Contains(friendsNavigationLink, json);
+ Assert.Contains(newFriendsNavigationLink, json);
+ Assert.Contains(untypedFriendsNavigationLink, json);
+ }
+ }
+
+ [Fact]
+ public async Task PostEmployee_WithFullMetadata()
+ {
+ //Arrange
+
+ string requestUri = this.BaseAddress + "/convention/Employees?$format=application/json;odata.metadata=full";
+
+ var content = @"{
+ 'Name':'SqlUD'
+ }";
+
+ var requestForPatch = new HttpRequestMessage(new HttpMethod("POST"), requestUri);
+
+ StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json");
+ requestForPatch.Content = stringContent;
+
+ string friendsNavigationLink = "Friends@odata.navigationLink";
+ string newFriendsNavigationLink = "NewFriends@odata.navigationLink";
+ string untypedFriendsNavigationLink = "UnTypedFriends@odata.navigationLink";
+
+ //Act & Assert
+ using (HttpResponseMessage response = await this.Client.SendAsync(requestForPatch))
+ {
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ var json = response.Content.ReadAsStringAsync().Result;
+ Assert.Contains("SqlUD", json);
+ Assert.Contains(friendsNavigationLink, json);
+ Assert.Contains(newFriendsNavigationLink, json);
+ Assert.Contains(untypedFriendsNavigationLink, json);
+ }
+ }
+
+ [Fact]
+ public async Task GetEmployee_WithFullMetadata()
+ {
+ //Arrange
+
+ string requestUri = this.BaseAddress + "/convention/Employees(1)?$format=application/json;odata.metadata=full";
+
+ var requestForPatch = new HttpRequestMessage(new HttpMethod("GET"), requestUri);
+
+ string friendsNavigationLink = "Friends@odata.navigationLink";
+ string newFriendsNavigationLink = "NewFriends@odata.navigationLink";
+ string untypedFriendsNavigationLink = "UnTypedFriends@odata.navigationLink";
+
+ string notexpected = "Friends\":[{\"@odata.type\":\"#Microsoft.Test.E2E.AspNet.OData.BulkOperation.Friend\"";
+
+ //Act & Assert
+ using (HttpResponseMessage response = await this.Client.SendAsync(requestForPatch))
+ {
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ var json = response.Content.ReadAsStringAsync().Result;
+ Assert.DoesNotContain(notexpected, json);
+ Assert.Contains(friendsNavigationLink, json);
+ Assert.Contains(newFriendsNavigationLink, json);
+ Assert.Contains(untypedFriendsNavigationLink, json);
+ }
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Common/Execution/WebHostTestBase.cs b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Common/Execution/WebHostTestBase.cs
index 994308b7bd..f9f8a59ab2 100644
--- a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Common/Execution/WebHostTestBase.cs
+++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Common/Execution/WebHostTestBase.cs
@@ -43,10 +43,10 @@ protected WebHostTestBase(WebHostTestFixture fixture)
///
///
protected abstract void UpdateConfiguration(WebRouteConfiguration configuration);
-
+
public void Dispose()
{
- if(Client != null)
+ if (Client != null)
{
Client.Dispose();
}
diff --git a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Common/Execution/WebHostTestFixture.cs b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Common/Execution/WebHostTestFixture.cs
index 276033a607..5b071bc841 100644
--- a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Common/Execution/WebHostTestFixture.cs
+++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Common/Execution/WebHostTestFixture.cs
@@ -65,7 +65,7 @@ namespace Microsoft.Test.E2E.AspNet.OData.Common.Execution
public class WebHostTestFixture : IDisposable
{
private static readonly string NormalBaseAddressTemplate = "http://{0}:{1}";
-
+
private int _port;
private bool disposedValue = false;
private Object thisLock = new Object();
@@ -161,7 +161,7 @@ public bool Initialize(Action testConfigurationAction)
.Build();
_selfHostServer.Start();
-#else
+#else
_selfHostServer = WebApp.Start(this.BaseAddress, DefaultKatanaConfigure);
#endif
}
diff --git a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/ComplexTypeInheritance/ComplexTypeInheritanceControllers.cs b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/ComplexTypeInheritance/ComplexTypeInheritanceControllers.cs
index 967e0c9841..2d88199bf6 100644
--- a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/ComplexTypeInheritance/ComplexTypeInheritanceControllers.cs
+++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/ComplexTypeInheritance/ComplexTypeInheritanceControllers.cs
@@ -79,12 +79,12 @@ public TestSingleResult GetWindow([FromODataUri] int key)
return TestSingleResult.Create(_windows.Where(w => w.Id == key).AsQueryable());
}
- public ITestActionResult Post([FromBody]Window window)
+ public ITestActionResult Post([FromBody] Window window)
{
_windows.Add(window);
window.Id = _windows.Count + 1;
Rectangle rectangle = window.CurrentShape as Rectangle;
- if(rectangle!=null)
+ if (rectangle != null)
{
rectangle.Fill();
}
@@ -93,7 +93,7 @@ public ITestActionResult Post([FromBody]Window window)
}
[ODataRoute("Windows({key})")]
- public ITestActionResult Patch(int key, [FromBody]Delta delta)
+ public ITestActionResult Patch(int key, [FromBody] Delta delta)
{
delta.TrySetPropertyValue("Id", key); // It is the key property, and should not be updated.
@@ -117,26 +117,7 @@ public ITestActionResult Patch(int key, [FromBody]Delta delta)
return Ok(window);
}
- [ODataRoute("Windows({key})/CurrentShape")]
- public ITestActionResult PatchShape(int key, [FromBody] Delta delta)
- {
- Window window = _windows.First(e => e.Id == key);
- var currShape = window.CurrentShape;
- Shape newcurrShape = null;
-
- try
- {
- newcurrShape = delta.Patch(currShape);
- }
- catch (ArgumentException ae)
- {
- return BadRequest(ae.Message);
- }
-
- return Ok(newcurrShape);
- }
-
- public ITestActionResult Put(int key, [FromBody]Window window)
+ public ITestActionResult Put(int key, [FromBody] Window window)
{
if (key != window.Id)
{
@@ -246,7 +227,7 @@ public ITestActionResult GetOptionalShapesOfCircle(int key)
}
[HttpPut]
- public ITestActionResult PutToCurrentShapeOfCircle(int key, [FromBody]Delta shape)
+ public ITestActionResult PutToCurrentShapeOfCircle(int key, [FromBody] Delta shape)
{
Window window = _windows.FirstOrDefault(e => e.Id == key);
if (window == null)
@@ -280,7 +261,7 @@ public ITestActionResult ReplaceOptionalShapes(int key, IEnumerable shape
}
[HttpPost]
- public ITestActionResult PostToOptionalShapes(int key, [FromBody]Shape newShape)
+ public ITestActionResult PostToOptionalShapes(int key, [FromBody] Shape newShape)
{
Window window = _windows.FirstOrDefault(w => w.Id == key);
if (window == null)
@@ -293,7 +274,7 @@ public ITestActionResult PostToOptionalShapes(int key, [FromBody]Shape newShape)
}
[HttpPost]
- public ITestActionResult PostToPolygonalShapes(int key, [FromBody]Polygon newPolygon)
+ public ITestActionResult PostToPolygonalShapes(int key, [FromBody] Polygon newPolygon)
{
Window window = _windows.FirstOrDefault(w => w.Id == key);
if (window == null)
@@ -306,13 +287,13 @@ public ITestActionResult PostToPolygonalShapes(int key, [FromBody]Polygon newPol
}
[HttpPatch]
- public ITestActionResult PatchToOptionalShapes(int key, [FromBody]Delta shapes)
+ public ITestActionResult PatchToOptionalShapes(int key, [FromBody] Delta shapes)
{
return Ok("Not Supported");
}
[HttpPatch]
- public ITestActionResult PatchToCurrentShapeOfCircle(int key, [FromBody]Delta shape)
+ public ITestActionResult PatchToCurrentShapeOfCircle(int key, [FromBody] Delta shape)
{
Window window = _windows.FirstOrDefault(e => e.Id == key);
if (window == null)
diff --git a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/ComplexTypeInheritance/ComplexTypeInheritanceTests.cs b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/ComplexTypeInheritance/ComplexTypeInheritanceTests.cs
index 44863061cc..f8e214a2a2 100644
--- a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/ComplexTypeInheritance/ComplexTypeInheritanceTests.cs
+++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/ComplexTypeInheritance/ComplexTypeInheritanceTests.cs
@@ -302,7 +302,13 @@ public async Task PatchContainingEntity(string modelMode)
'Center':{'X':1,'Y':2},
'HasBorder':true
},
- 'OptionalShapes': [ ]
+ 'OptionalShapes': [
+ {
+ '@odata.type':'#Microsoft.Test.E2E.AspNet.OData.ComplexTypeInheritance.Circle',
+ 'Radius':1,
+ 'Center':{'X':1,'Y':2},
+ 'HasBorder':true
+ }]
}";
StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json");
request.Content = stringContent;
@@ -324,7 +330,7 @@ public async Task PatchContainingEntity(string modelMode)
String.Format("\nExpected that Radius: 2, but actually: {0},\n request uri: {1},\n response payload: {2}", radius, requestUri, contentOfString));
JArray windows = contentOfJObject["OptionalShapes"] as JArray;
- Assert.True(0 == windows.Count,
+ Assert.True(1 == windows.Count,
String.Format("\nExpected count: {0},\n actual: {1},\n request uri: {2},\n response payload: {3}", 1, windows.Count, requestUri, contentOfString));
}
diff --git a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/DeltaQueryTests/DeltaQueryTests.cs b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/DeltaQueryTests/DeltaQueryTests.cs
index 7b84f34710..2a509e9b6c 100644
--- a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/DeltaQueryTests/DeltaQueryTests.cs
+++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/DeltaQueryTests/DeltaQueryTests.cs
@@ -51,6 +51,7 @@ public async Task DeltaVerifyReslt()
{
HttpRequestMessage get = new HttpRequestMessage(HttpMethod.Get, BaseAddress + "/odata/TestCustomers?$deltaToken=abc");
get.Headers.Add("Accept", "application/json;odata.metadata=minimal");
+ get.Headers.Add("OData-Version", "4.01");
HttpResponseMessage response = await Client.SendAsync(get);
Assert.True(response.IsSuccessStatusCode);
dynamic results = await response.Content.ReadAsObject();
@@ -59,7 +60,7 @@ public async Task DeltaVerifyReslt()
var changeEntity = results.value[0];
Assert.True(((JToken)changeEntity).Count() == 7, "The changed customer should have 6 properties plus type written.");
- string changeEntityType = changeEntity["@odata.type"].Value as string;
+ string changeEntityType = changeEntity["@type"].Value as string;
Assert.True(changeEntityType != null, "The changed customer should have type written");
Assert.True(changeEntityType.Contains("#Microsoft.Test.E2E.AspNet.OData.TestCustomerWithAddress"), "The changed order should be a TestCustomerWithAddress");
Assert.True(changeEntity.Id.Value == 1, "The ID Of changed customer should be 1.");
@@ -98,22 +99,22 @@ public async Task DeltaVerifyReslt()
var newOrder = results.value[2];
Assert.True(((JToken)newOrder).Count() == 3, "The new order should have 2 properties plus context written");
- string newOrderContext = newOrder["@odata.context"].Value as string;
+ string newOrderContext = newOrder["@context"].Value as string;
Assert.True(newOrderContext != null, "The new order should have a context written");
Assert.True(newOrderContext.Contains("$metadata#TestOrders"), "The new order should come from the TestOrders entity set");
Assert.True(newOrder.Id.Value == 27, "The ID of the new order should be 27");
Assert.True(newOrder.Amount.Value == 100, "The amount of the new order should be 100");
var deletedEntity = results.value[3];
- Assert.True(deletedEntity.id.Value == "7", "The ID of the deleted customer should be 7");
- Assert.True(deletedEntity.reason.Value == "changed", "The reason for the deleted customer should be 'changed'");
+ Assert.True(deletedEntity["@id"].Value == "7", "The ID of the deleted customer should be 7");
+ Assert.True(deletedEntity["@removed"].reason.Value == "changed", "The reason for the deleted customer should be 'changed'");
var deletedOrder = results.value[4];
- string deletedOrderContext = deletedOrder["@odata.context"].Value as string;
+ string deletedOrderContext = deletedOrder["@context"].Value as string;
Assert.True(deletedOrderContext != null, "The deleted order should have a context written");
Assert.True(deletedOrderContext.Contains("$metadata#TestOrders"), "The deleted order should come from the TestOrders entity set");
- Assert.True(deletedOrder.id.Value == "12", "The ID of the deleted order should be 12");
- Assert.True(deletedOrder.reason.Value == "deleted", "The reason for the deleted order should be 'deleted'");
+ Assert.True(deletedOrder["@id"].Value == "12", "The ID of the deleted order should be 12");
+ Assert.True(deletedOrder["@removed"].reason.Value == "deleted", "The reason for the deleted order should be 'deleted'");
var deletedLink = results.value[5];
Assert.True(deletedLink.source.Value == "http://localhost/odata/DeltaCustomers(1)", "The source of the deleted link should be 'http://localhost/odata/DeltaCustomers(1)'");
diff --git a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/DollarLevels/DollarLevelsController.cs b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/DollarLevels/DollarLevelsController.cs
index e0a530fd51..efbb31c121 100644
--- a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/DollarLevels/DollarLevelsController.cs
+++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/DollarLevels/DollarLevelsController.cs
@@ -144,7 +144,7 @@ public ITestActionResult Get(int key, ODataQueryOptions queryOptions
return BadRequest(responseMessage);
}
- var employee = _DLEmployees.Single(e=>e.ID == key);
+ var employee = _DLEmployees.Single(e => e.ID == key);
var appliedEmployee = queryOptions.ApplyTo(employee, new ODataQuerySettings());
return Ok(appliedEmployee, appliedEmployee.GetType());
}
diff --git a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/DollarLevels/DollarLevelsDataModel.cs b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/DollarLevels/DollarLevelsDataModel.cs
index 7b8432ed87..ab0b9c5aaf 100644
--- a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/DollarLevels/DollarLevelsDataModel.cs
+++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/DollarLevels/DollarLevelsDataModel.cs
@@ -6,6 +6,7 @@
//------------------------------------------------------------------------------
using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
namespace Microsoft.Test.E2E.AspNet.OData.DollarLevels
{
@@ -26,4 +27,5 @@ public class DLEmployee
public DLEmployee Friend { get; set; }
}
+
}
diff --git a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Formatter/DeltaTests.cs b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Formatter/DeltaTests.cs
index 26ad72d42f..b438fd4f8d 100644
--- a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Formatter/DeltaTests.cs
+++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Formatter/DeltaTests.cs
@@ -45,6 +45,7 @@
using Microsoft.AspNet.OData.Extensions;
using Microsoft.AspNet.OData.Routing;
using Microsoft.AspNet.OData.Routing.Conventions;
+using Microsoft.AspNet.OData.Test.Builder.TestModels.Recursive;
using Microsoft.OData.Client;
using Microsoft.OData.Edm;
using Microsoft.Test.E2E.AspNet.OData.Common;
@@ -355,7 +356,8 @@ private static IEdmModel GetModel(WebRouteConfiguration config)
}
[Fact]
- public async Task PutShouldntOverrideNavigationProperties()
+ //Changing the test from shouldnt to should as it override navigation properties with bulk operations
+ public async Task PutShouldOverrideNavigationProperties()
{
string putUri = BaseAddress + "/odata/DeltaCustomers(5)";
ExpandoObject data = new ExpandoObject();
@@ -366,12 +368,13 @@ public async Task PutShouldntOverrideNavigationProperties()
response = await Client.SendAsync(get);
Assert.True(response.IsSuccessStatusCode);
dynamic query = await response.Content.ReadAsObject();
- Assert.Equal(3, query.Orders.Count);
+ Assert.Equal(0, query.Orders.Count);
}
}
public class PatchtDeltaOfTTests : WebHostTestBase
{
+ static IEdmModel model;
public PatchtDeltaOfTTests(WebHostTestFixture fixture)
:base(fixture)
{
@@ -389,15 +392,31 @@ private static IEdmModel GetModel(WebRouteConfiguration config)
ODataModelBuilder builder = config.CreateConventionModelBuilder();
builder.EntitySet("DeltaCustomers");
builder.EntitySet("DeltaOrders");
- return builder.GetEdmModel();
+ model = builder.GetEdmModel();
+ return model;
}
+
[Fact]
public async Task PatchShouldSupportNonSettableCollectionProperties()
{
+ var changedEntity = new EdmDeltaEntityObject(model.FindDeclaredType("Microsoft.Test.E2E.AspNet.OData.Formatter.DeltaCustomer") as IEdmEntityType);
+ changedEntity.TrySetPropertyValue("Id", 1);
+ changedEntity.TrySetPropertyValue("FathersAge", 3);
+
HttpRequestMessage patch = new HttpRequestMessage(new HttpMethod("PATCH"), BaseAddress + "/odata/DeltaCustomers(6)");
- dynamic data = new ExpandoObject();
- data.Addresses = Enumerable.Range(10, 3).Select(i => new DeltaAddress { ZipCode = i });
+ var data = new ExpandoObject() as IDictionary;
+
+ foreach(var prop in changedEntity.GetChangedPropertyNames())
+ {
+ object val;
+ if(changedEntity.TryGetPropertyValue(prop, out val))
+ {
+ data.Add(prop, val);
+ }
+
+ }
+
string content = JsonConvert.SerializeObject(data);
patch.Content = new StringContent(content);
patch.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json");
@@ -405,35 +424,35 @@ public async Task PatchShouldSupportNonSettableCollectionProperties()
Assert.True(response.IsSuccessStatusCode);
- HttpRequestMessage get = new HttpRequestMessage(HttpMethod.Get, BaseAddress + "/odata/DeltaCustomers(6)?$expand=Orders");
+ HttpRequestMessage get = new HttpRequestMessage(HttpMethod.Get, BaseAddress + "/odata/DeltaCustomers(1)?$expand=Orders");
response = await Client.SendAsync(get);
Assert.True(response.IsSuccessStatusCode);
dynamic query = await response.Content.ReadAsObject();
- Assert.Equal(3, query.Addresses.Count);
+ Assert.Equal(2, query.Addresses.Count);
Assert.Equal(3, query.Orders.Count);
}
[Fact]
public async Task PatchShouldSupportComplexDerivedTypeTransform()
{
- HttpRequestMessage patch = new HttpRequestMessage(new HttpMethod("MERGE"), BaseAddress + "/odata/DeltaCustomers(6)");
+ HttpRequestMessage patch = new HttpRequestMessage(new HttpMethod("PATCH"), BaseAddress + "/odata/DeltaCustomers(7)");
dynamic data = new ExpandoObject();
- data.Addresses = Enumerable.Range(10, 3).Select(i => new DeltaAddress { ZipCode = i });
+ data.MyAddress = new PersonalAddress { Street = "abc" };
- string content = JsonConvert.SerializeObject(data);
- content = @"{'MyAddress':{'@odata.type': 'Microsoft.Test.E2E.AspNet.OData.Formatter.PersonalAddress','Street': 'abc'}}";
+ string content = JsonConvert.SerializeObject(data);
patch.Content = new StringContent(content);
patch.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json");
HttpResponseMessage response = await Client.SendAsync(patch);
Assert.True(response.IsSuccessStatusCode);
- HttpRequestMessage get = new HttpRequestMessage(HttpMethod.Get, BaseAddress + "/odata/DeltaCustomers(6)?$expand=Orders");
+ HttpRequestMessage get = new HttpRequestMessage(HttpMethod.Get, BaseAddress + "/odata/DeltaCustomers(7)?$expand=Orders");
response = await Client.SendAsync(get);
Assert.True(response.IsSuccessStatusCode);
dynamic query = await response.Content.ReadAsObject();
Assert.Equal("abc", query.MyAddress.Street.ToString());
}
+
}
public class DeltaCustomersController : TestODataController
@@ -450,12 +469,19 @@ static DeltaCustomersController()
customer.Id = 5;
customers.Add(customer);
- customer = new DeltaCustomer("Original name",
+ var customer1 = new DeltaCustomer("Original name",
Enumerable.Range(0, 2).Select(i => new DeltaAddress { ZipCode = i }),
Enumerable.Range(0, 3).Select(i => new DeltaOrder { Details = i.ToString() }));
- customer.Id = 6;
- customer.MyAddress = new OfficeAddress { Street = "Microsot" };
- customers.Add(customer);
+ customer1.Id = 6;
+ customer1.MyAddress = new OfficeAddress { Street = "Microsot" };
+ customers.Add(customer1);
+
+ var customer2 = new DeltaCustomer("Original name",
+ Enumerable.Range(0, 2).Select(i => new DeltaAddress { ZipCode = i }),
+ Enumerable.Range(0, 3).Select(i => new DeltaOrder { Details = i.ToString() }));
+ customer2.Id = 7;
+ customer2.MyAddress = new OfficeAddress { Street = "Microsot" };
+ customers.Add(customer2);
}
[EnableQuery(PageSize = 10, MaxExpansionDepth = 2)]
@@ -480,6 +506,7 @@ public ITestActionResult Put([FromODataUri] int key, [FromBody] Delta c.Id == key).FirstOrDefault();
entity.Put(customer);
return Ok(customer);
+
}
[AcceptVerbs("PATCH", "MERGE")]
@@ -490,9 +517,11 @@ public ITestActionResult Patch([FromODataUri] int key, Delta patc
return BadRequest(ModelState);
}
var customer = customers.Where(c => c.Id == key).FirstOrDefault();
- var newCustomer = patch.Patch(customer);
- Assert.True(newCustomer == customer);
- return Ok(newCustomer);
+ if (customer == null)
+ return Ok();
+
+ patch.Patch(customer);
+ return Ok(customer);
}
}
@@ -510,6 +539,7 @@ public DeltaCustomer(string name, IEnumerable addresses, IEnumerab
_addresses = addresses.ToList();
Orders = orders.ToList();
}
+
public int Id { get; set; }
private string _name = null;
diff --git a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Formatter/Untyped/UntypedDeltaSerializationTests.cs b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Formatter/Untyped/UntypedDeltaSerializationTests.cs
index 85704de575..db31d195af 100644
--- a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Formatter/Untyped/UntypedDeltaSerializationTests.cs
+++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/Formatter/Untyped/UntypedDeltaSerializationTests.cs
@@ -55,6 +55,7 @@ public async Task UntypedDeltaWorksInAllFormats(string acceptHeader)
string url = "/untyped/UntypedDeltaCustomers?$deltatoken=abc";
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, BaseAddress + url);
request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(acceptHeader));
+ request.Headers.Add("OData-Version", "4.01");
HttpResponseMessage response = await Client.SendAsync(request);
Assert.True(response.IsSuccessStatusCode);
Assert.NotNull(response.Content);
@@ -71,8 +72,7 @@ public async Task UntypedDeltaWorksInAllFormats(string acceptHeader)
for (int i=10 ; i < 15 ; i++)
{
string contextUrl = BaseAddress.ToLowerInvariant() + "/untyped/$metadata#UntypedDeltaCustomers/$deletedEntity";
- Assert.True(contextUrl.Equals(((dynamic)returnedObject).value[i]["@odata.context"].Value));
- Assert.True(i.ToString().Equals(((dynamic)returnedObject).value[i]["id"].Value));
+ Assert.True(i.ToString().Equals(((dynamic)returnedObject).value[i]["@id"].Value));
}
}
}
diff --git a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/OpenType/OpenTypeControllers.cs b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/OpenType/OpenTypeControllers.cs
index 6d196e01f5..74d4090d01 100644
--- a/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/OpenType/OpenTypeControllers.cs
+++ b/test/E2ETest/Microsoft.Test.E2E.AspNet.OData/OpenType/OpenTypeControllers.cs
@@ -56,7 +56,7 @@ public ITestActionResult Get(int key)
}
[EnableQuery]
- public ITestActionResult Post([FromBody]Employee employee)
+ public ITestActionResult Post([FromBody] Employee employee)
{
employee.Id = Employees.Count + 1;
Employees.Add(employee);
@@ -64,7 +64,7 @@ public ITestActionResult Post([FromBody]Employee employee)
}
[EnableQuery]
- public ITestActionResult Put(int key, [FromBody]Employee employee)
+ public ITestActionResult Put(int key, [FromBody] Employee employee)
{
Employee originalEmployee = Employees.Single(e => e.Id == key);
employee.Id = key;
@@ -74,7 +74,7 @@ public ITestActionResult Put(int key, [FromBody]Employee employee)
}
[EnableQuery]
- public ITestActionResult Patch(int key, [FromBody]Delta employee)
+ public ITestActionResult Patch(int key, [FromBody] Delta employee)
{
Employee originalEmployee = Employees.Single(e => e.Id == key);
employee.Patch(originalEmployee);
@@ -98,7 +98,7 @@ public ITestActionResult GetManager(int key)
}
[EnableQuery]
- public ITestActionResult PutManager(int key, [FromBody]Manager employee)
+ public ITestActionResult PutManager(int key, [FromBody] Manager employee)
{
Manager originalEmployee = Employees.OfType().Single(e => e.Id == key);
employee.Id = key;
@@ -109,7 +109,7 @@ public ITestActionResult PutManager(int key, [FromBody]Manager employee)
}
[EnableQuery]
- public ITestActionResult PatchManager(int key, [FromBody]Delta employee)
+ public ITestActionResult PatchManager(int key, [FromBody] Delta employee)
{
Manager originalEmployee = Employees.OfType().Single(e => e.Id == key);
employee.Patch(originalEmployee);
@@ -143,7 +143,7 @@ private static void InitAccounts()
{
Accounts = new List
{
-
+
new PremiumAccount()
{
Id = 1,
@@ -183,7 +183,7 @@ private static void InitAccounts()
AccountInfo = new AccountInfo()
{
NickName = "NickName3"
-
+
},
Address = new Address()
{
@@ -342,7 +342,7 @@ public ITestActionResult GetTagsAttributeRouting(int key)
}
[HttpPatch]
- public ITestActionResult Patch(int key, [FromBody]Delta patch, ODataQueryOptions queryOptions)
+ public ITestActionResult Patch(int key, [FromBody] Delta patch, ODataQueryOptions queryOptions)
{
IEnumerable appliedAccounts = Accounts.Where(a => a.Id == key);
@@ -369,7 +369,7 @@ public ITestActionResult Patch(int key, [FromBody]Delta patch, ODataQue
[HttpPatch]
[ODataRoute("Accounts({key})")]
- public ITestActionResult PatchAttributeRouting(int key, [FromBody]Delta patch, ODataQueryOptions queryOptions)
+ public ITestActionResult PatchAttributeRouting(int key, [FromBody] Delta patch, ODataQueryOptions queryOptions)
{
IEnumerable appliedAccounts = Accounts.Where(a => a.Id == key);
@@ -395,7 +395,7 @@ public ITestActionResult PatchAttributeRouting(int key, [FromBody]Delta
}
[HttpPut]
- public ITestActionResult Put(int key, [FromBody]Account account)
+ public ITestActionResult Put(int key, [FromBody] Account account)
{
if (key != account.Id)
{
@@ -410,7 +410,7 @@ public ITestActionResult Put(int key, [FromBody]Account account)
[HttpPut]
[ODataRoute("Accounts({key})")]
- public ITestActionResult PutAttributeRouting(int key, [FromBody]Account account)
+ public ITestActionResult PutAttributeRouting(int key, [FromBody] Account account)
{
if (key != account.Id)
{
@@ -424,7 +424,7 @@ public ITestActionResult PutAttributeRouting(int key, [FromBody]Account account)
}
[HttpPost]
- public ITestActionResult Post([FromBody]Account account)
+ public ITestActionResult Post([FromBody] Account account)
{
account.Id = Accounts.Count + 1;
account.DynamicProperties["OwnerGender"] = Gender.Male;// Defect 2371564 odata.type is missed in client payload for dynamic enum type
@@ -435,7 +435,7 @@ public ITestActionResult Post([FromBody]Account account)
[HttpPost]
[ODataRoute("Accounts")]
- public ITestActionResult PostAttributeRouting([FromBody]Account account)
+ public ITestActionResult PostAttributeRouting([FromBody] Account account)
{
account.Id = Accounts.Count + 1;
Accounts.Add(account);
@@ -475,7 +475,7 @@ public ITestActionResult DeleteAttributeRouting(int key)
}
[HttpPatch]
- public ITestActionResult PatchToAddress(int key, [FromBody]Delta address)
+ public ITestActionResult PatchToAddress(int key, [FromBody] Delta