From e972bdebfd88f440ab7a958b99ab997e90e3a561 Mon Sep 17 00:00:00 2001 From: Sam Xu Date: Mon, 10 Jun 2024 15:16:47 -0700 Subject: [PATCH 1/3] Enable Instance annotation serialization Enable Instance annotation deserialization Enable property without value --- .../Deltas/DeltaOfT.cs | 77 +++ .../Edm/EdmModelAnnotationExtensions.cs | 28 + .../Edm/EdmModelExtensions.cs | 69 ++- .../Edm/EdmUntypedHelpers.cs | 5 + .../Extensions/HttpRequestExtensions.cs | 38 ++ .../Deserialization/DeserializationHelper.cs | 121 ++++- .../ODataCollectionDeserializer.cs | 35 +- .../ODataDeserializerContext.cs | 77 +++ .../ODataDeserializerProvider.cs | 12 +- .../ODataResourceDeserializer.cs | 151 +++++- .../ODataResourceSetDeserializer.cs | 2 +- .../Formatter/ODataInputFormatter.cs | 18 +- .../Formatter/ODataOutputFormatterHelper.cs | 3 + .../Formatter/ResourceContext.cs | 43 ++ .../ODataCollectionSerializer.cs | 3 +- .../Serialization/ODataPrimitiveSerializer.cs | 18 +- .../Serialization/ODataResourceSerializer.cs | 144 ++++- .../ODataResourceSetSerializer.cs | 25 +- .../Serialization/ODataSerializerContext.cs | 5 + .../Serialization/ODataSerializerHelper.cs | 73 +++ .../Serialization/ODataSerializerProvider.cs | 17 +- .../Wrapper/ODataReaderExtensions.cs | 6 + .../Formatter/Wrapper/ODataResourceWrapper.cs | 37 +- .../Microsoft.AspNetCore.OData.csproj | 6 +- .../Microsoft.AspNetCore.OData.xml | 133 ++++- .../Properties/SRResources.Designer.cs | 9 + .../Properties/SRResources.resx | 3 + .../PublicAPI.Unshipped.txt | 14 + .../InstanceAnnotationsController.cs | 273 ++++++++++ .../InstanceAnnotationsDataModel.cs | 60 ++ .../InstanceAnnotationsDataSource.cs | 161 ++++++ .../InstanceAnnotationsEdmModel.cs | 32 ++ .../InstanceAnnotationsTests.cs | 513 ++++++++++++++++++ .../Deltas/DeltaTests.cs | 27 + .../Edm/EdmModelAnnotationExtensionsTests.cs | 53 +- .../Edm/EdmModelExtensionsTests.cs | 52 +- .../Extensions/HttpRequestExtensionsTests.cs | 23 + .../DeserializationHelpersTest.cs | 68 +++ .../ODataCollectionDeserializerTests.cs | 108 ++++ .../ODataDeserializerContextTest.cs | 72 +++ .../ODataDeserializerProviderTests.cs | 16 +- .../ODataResourceDeserializerTests.cs | 313 ++++++++++- .../ODataResourceSerializerTests.cs | 169 ++++++ .../ODataResourceSetSerializerTests.cs | 60 +- .../ODataSerializerHelperTests.cs | 104 ++++ .../ODataSerializerProviderTests.cs | 29 + .../Wrapper/ODataReaderExtensionsTests.cs | 53 +- .../Wrapper/ODataResourceWrapperTests.cs | 87 +++ 48 files changed, 3326 insertions(+), 119 deletions(-) create mode 100644 src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataSerializerHelper.cs create mode 100644 test/Microsoft.AspNetCore.OData.E2E.Tests/InstanceAnnotations/InstanceAnnotationsController.cs create mode 100644 test/Microsoft.AspNetCore.OData.E2E.Tests/InstanceAnnotations/InstanceAnnotationsDataModel.cs create mode 100644 test/Microsoft.AspNetCore.OData.E2E.Tests/InstanceAnnotations/InstanceAnnotationsDataSource.cs create mode 100644 test/Microsoft.AspNetCore.OData.E2E.Tests/InstanceAnnotations/InstanceAnnotationsEdmModel.cs create mode 100644 test/Microsoft.AspNetCore.OData.E2E.Tests/InstanceAnnotations/InstanceAnnotationsTests.cs create mode 100644 test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataSerializerHelperTests.cs create mode 100644 test/Microsoft.AspNetCore.OData.Tests/Formatter/Wrapper/ODataResourceWrapperTests.cs diff --git a/src/Microsoft.AspNetCore.OData/Deltas/DeltaOfT.cs b/src/Microsoft.AspNetCore.OData/Deltas/DeltaOfT.cs index bbde3507d..85ae87a86 100644 --- a/src/Microsoft.AspNetCore.OData/Deltas/DeltaOfT.cs +++ b/src/Microsoft.AspNetCore.OData/Deltas/DeltaOfT.cs @@ -17,6 +17,7 @@ using System.Runtime.Serialization; using Microsoft.AspNetCore.OData.Abstracts; using Microsoft.AspNetCore.OData.Common; +using Microsoft.OData.ModelBuilder; namespace Microsoft.AspNetCore.OData.Deltas { @@ -47,6 +48,9 @@ private static readonly ConcurrentDictionary _changedDynamicProperties; private IDictionary _dynamicDictionaryCache; + private PropertyInfo _instanceAnnotationContainerPropertyInfo; + private IODataInstanceAnnotationContainer _instanceAnnotationContainer; + /// /// Initializes a new instance of . /// @@ -150,6 +154,20 @@ public override bool TrySetPropertyValue(string name, object value) throw Error.ArgumentNull(nameof(name)); } + if (IsInstanceAnnotation(name, out PropertyInfo annotationContainerPropertyInfo)) + { + IODataInstanceAnnotationContainer annotationContainer = value as IODataInstanceAnnotationContainer; + if (value != null && annotationContainer == null) + { + return false; + } + + annotationContainerPropertyInfo.SetValue(_instance, annotationContainer); + _instanceAnnotationContainer = annotationContainer; + _instanceAnnotationContainerPropertyInfo = annotationContainerPropertyInfo; + return true; + } + if (_dynamicDictionaryPropertyinfo != null) { // Dynamic property can have the same name as the dynamic property dictionary. @@ -374,6 +392,8 @@ public void CopyChangedValues(T original) CopyChangedDynamicValues(original); + CopyInstanceAnnotations(original); + // For nested resources. foreach (string nestedResourceName in _deltaNestedResources.Keys) { @@ -685,6 +705,44 @@ private static bool IsIgnoredProperty(bool isTypeDataContract, PropertyInfo prop return propertyInfo.GetCustomAttributes(typeof(IgnoreDataMemberAttribute), inherit: true).Any(); } + private void CopyInstanceAnnotations(T targetEntity) + { + if (_instanceAnnotationContainerPropertyInfo == null) + { + return; + } + + IODataInstanceAnnotationContainer sourceContainer = + _instanceAnnotationContainerPropertyInfo.GetValue(_instance) as IODataInstanceAnnotationContainer; + if (sourceContainer == null) + { + return; + } + + IODataInstanceAnnotationContainer desContainer = + _instanceAnnotationContainerPropertyInfo.GetValue(targetEntity) as IODataInstanceAnnotationContainer; + if (desContainer == null) + { + _instanceAnnotationContainerPropertyInfo.SetValue(targetEntity, sourceContainer); + return; + } + + foreach (var item in sourceContainer.InstanceAnnotations) + { + foreach (var annotation in item.Value) + { + if (item.Key == null || item.Key == string.Empty) + { + desContainer.AddResourceAnnotation(annotation.Key, annotation.Value); + } + else + { + desContainer.AddPropertyAnnotation(item.Key, annotation.Key, annotation.Value); + } + } + } + } + // Copy changed dynamic properties and leave the unchanged dynamic properties private void CopyChangedDynamicValues(T targetEntity) { @@ -842,5 +900,24 @@ private bool TrySetNestedResourceInternal(string name, object deltaNestedResourc return true; } + + private bool IsInstanceAnnotation(string name, out PropertyInfo propertyInfo) + { + propertyInfo = null; + if (!_allProperties.TryGetValue(name, out PropertyAccessor propertyAccessor)) + { + return false; + } + + propertyInfo = propertyAccessor.Property; + + if (propertyInfo.PropertyType == typeof(ODataInstanceAnnotationContainer) || + typeof(IODataInstanceAnnotationContainer).IsAssignableFrom(propertyInfo.PropertyType)) + { + return true; + } + + return false; + } } } diff --git a/src/Microsoft.AspNetCore.OData/Edm/EdmModelAnnotationExtensions.cs b/src/Microsoft.AspNetCore.OData/Edm/EdmModelAnnotationExtensions.cs index b452e079c..c17b5b64b 100644 --- a/src/Microsoft.AspNetCore.OData/Edm/EdmModelAnnotationExtensions.cs +++ b/src/Microsoft.AspNetCore.OData/Edm/EdmModelAnnotationExtensions.cs @@ -217,6 +217,34 @@ public static PropertyInfo GetDynamicPropertyDictionary(this IEdmModel edmModel, return null; } + /// + /// Gets the instance annotation container property info. + /// + /// The Edm model. + /// The Edm type. + /// The instance annotation container property info. + public static PropertyInfo GetInstanceAnnotationsContainer(this IEdmModel edmModel, IEdmStructuredType edmType) + { + if (edmModel == null) + { + throw Error.ArgumentNull(nameof(edmModel)); + } + + if (edmType == null) + { + throw Error.ArgumentNull(nameof(edmType)); + } + + InstanceAnnotationContainerAnnotation annotation = + edmModel.GetAnnotationValue(edmType); + if (annotation != null) + { + return annotation.PropertyInfo; + } + + return null; + } + /// /// Gets the model name. /// diff --git a/src/Microsoft.AspNetCore.OData/Edm/EdmModelExtensions.cs b/src/Microsoft.AspNetCore.OData/Edm/EdmModelExtensions.cs index 833e2e6b8..a44f6c315 100644 --- a/src/Microsoft.AspNetCore.OData/Edm/EdmModelExtensions.cs +++ b/src/Microsoft.AspNetCore.OData/Edm/EdmModelExtensions.cs @@ -8,11 +8,15 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Security.AccessControl; using Microsoft.AspNetCore.OData.Formatter.Deserialization; +using Microsoft.AspNetCore.OData.Formatter.Wrapper; using Microsoft.AspNetCore.OData.Routing.Template; using Microsoft.OData; using Microsoft.OData.Edm; +using Microsoft.OData.Edm.Csdl; using Microsoft.OData.Edm.Validation; +using Microsoft.OData.Edm.Vocabularies; using Microsoft.OData.UriParser; namespace Microsoft.AspNetCore.OData.Edm @@ -61,32 +65,36 @@ public static IEdmCollectionTypeReference ResolveResourceSetType(this IEdmModel /// Resolve the type reference from the type name of /// /// The Edm model. - /// The given resource. + /// The given resource wrapper. /// The resolved type. - public static IEdmStructuredTypeReference ResolveResourceType(this IEdmModel model, ODataResourceBase resource) + public static IEdmStructuredTypeReference ResolveResourceType(this IEdmModel model, ODataResourceWrapper resourceWrapper) { if (model == null) { throw Error.ArgumentNull(nameof(model)); } - if (resource == null) + if (resourceWrapper == null) { - throw Error.ArgumentNull(nameof(resource)); + throw Error.ArgumentNull(nameof(resourceWrapper)); } + string typeName = resourceWrapper.IsResourceValue ? + resourceWrapper.ResourceValue.TypeName : + resourceWrapper.Resource.TypeName; + IEdmStructuredTypeReference resourceType; - if (string.IsNullOrEmpty(resource.TypeName) || - string.Equals(resource.TypeName, "Edm.Untyped", StringComparison.OrdinalIgnoreCase)) + if (string.IsNullOrEmpty(typeName) || + string.Equals(typeName, "Edm.Untyped", StringComparison.OrdinalIgnoreCase)) { resourceType = EdmUntypedStructuredTypeReference.NullableTypeReference; } else { - IEdmStructuredType actualType = model.FindType(resource.TypeName) as IEdmStructuredType; + IEdmStructuredType actualType = model.FindType(typeName) as IEdmStructuredType; if (actualType == null) { - throw new ODataException(Error.Format(SRResources.ResourceTypeNotInModel, resource.TypeName)); + throw new ODataException(Error.Format(SRResources.ResourceTypeNotInModel, typeName)); } if (actualType is IEdmEntityType actualEntityType) @@ -102,6 +110,51 @@ public static IEdmStructuredTypeReference ResolveResourceType(this IEdmModel mod return resourceType; } + /// + /// Resolve the term using the annotation identifier. + /// + /// The Edm model. + /// It consists of the namespace or alias of the schema that defines the term, followed by a dot (.), + /// followed by the name of the term, optionally followed by a hash (#) and a qualifier. + /// The resolved term or null if not found. + public static IEdmTerm ResolveTerm(this IEdmModel model, string annotationIdentifier) + { + if (model == null) + { + throw Error.ArgumentNull(nameof(model)); + } + + string[] identifier = annotationIdentifier.Split('#'); + + IEdmTerm term = model.FindTerm(identifier[0]); + if (term != null) + { + return term; + } + + // TODO: Let's support namespace alias when we get requirement and ODL publics 'ReplaceAlias' extension method. + // identifier = model.ReplaceAlias(identifier); + + string termName = identifier[0]; + var terms = model.SchemaElements.OfType() + .Where(e => string.Equals(termName, e.FullName(), StringComparison.OrdinalIgnoreCase)); + + foreach (var refModels in model.ReferencedModels) + { + var refedTerms = refModels.SchemaElements.OfType() + .Where(e => string.Equals(termName, e.FullName(), StringComparison.OrdinalIgnoreCase)); + + terms = terms.Concat(refedTerms); + } + + if (terms.Count() > 1) + { + throw new ODataException(Error.Format(SRResources.AmbiguousTypeNameFound, termName)); + } + + return terms.SingleOrDefault(); + } + /// /// Get all property names for the given structured type. /// diff --git a/src/Microsoft.AspNetCore.OData/Edm/EdmUntypedHelpers.cs b/src/Microsoft.AspNetCore.OData/Edm/EdmUntypedHelpers.cs index 03039f2d7..f940a0182 100644 --- a/src/Microsoft.AspNetCore.OData/Edm/EdmUntypedHelpers.cs +++ b/src/Microsoft.AspNetCore.OData/Edm/EdmUntypedHelpers.cs @@ -11,8 +11,13 @@ namespace Microsoft.AspNetCore.OData.Edm { internal class EdmUntypedHelpers { + // Collection(Edm.Untyped) for resource set public readonly static EdmCollectionTypeReference NullableUntypedCollectionReference = new EdmCollectionTypeReference( new EdmCollectionType(EdmUntypedStructuredTypeReference.NullableTypeReference)); + + // Collection(Edm.Untyped) for collection of (Primitive, enum) + public readonly static EdmCollectionTypeReference NullablePrimitiveUntypedCollectionReference + = new EdmCollectionTypeReference(new EdmCollectionType(EdmCoreModel.Instance.GetUntyped())); } } diff --git a/src/Microsoft.AspNetCore.OData/Extensions/HttpRequestExtensions.cs b/src/Microsoft.AspNetCore.OData/Extensions/HttpRequestExtensions.cs index 83beb1bc1..6e39bdce7 100644 --- a/src/Microsoft.AspNetCore.OData/Extensions/HttpRequestExtensions.cs +++ b/src/Microsoft.AspNetCore.OData/Extensions/HttpRequestExtensions.cs @@ -26,6 +26,8 @@ namespace Microsoft.AspNetCore.OData.Extensions /// public static class HttpRequestExtensions { + private static readonly string ODataInstanceAnnotationContainerKey = "odataInstanceAnnotation_14802D58-69EF-4B28-9BDC-963D3648F06A"; + /// /// Returns the from the DI container. /// @@ -86,6 +88,42 @@ public static IEdmModel GetModel(this HttpRequest request) return request.ODataFeature().Model; } + /// + /// Set the top-level instance annotations for the request. + /// + /// The instance to extend. + /// The instance annotations + public static HttpRequest SetInstanceAnnotations(this HttpRequest request, IDictionary instanceAnnotations) + { + IODataFeature odataFeature = request.ODataFeature(); + + // The last wins. + odataFeature.RoutingConventionsStore[ODataInstanceAnnotationContainerKey] = instanceAnnotations; + + return request; + } + + /// + /// Get the top-level instance annotations for the request. + /// + /// The instance to extend. + /// null or top-level instance annotations. + public static IDictionary GetInstanceAnnotations(this HttpRequest request) + { + if (request == null) + { + return null; + } + + IODataFeature odataFeature = request.ODataFeature(); + if (!odataFeature.RoutingConventionsStore.TryGetValue(ODataInstanceAnnotationContainerKey, out object annotations)) + { + return null; + } + + return annotations as IDictionary; + } + /// /// Gets the setting. /// diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/DeserializationHelper.cs b/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/DeserializationHelper.cs index bb5c29dbf..6552f21d2 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/DeserializationHelper.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/DeserializationHelper.cs @@ -18,6 +18,7 @@ using Microsoft.AspNetCore.OData.Formatter.Value; using Microsoft.OData; using Microsoft.OData.Edm; +using Microsoft.OData.ModelBuilder; namespace Microsoft.AspNetCore.OData.Formatter.Deserialization { @@ -276,28 +277,30 @@ internal static void SetDynamicProperty(object resource, string propertyName, ob internal static object ConvertValue(object oDataValue, ref IEdmTypeReference propertyType, IODataDeserializerProvider deserializerProvider, ODataDeserializerContext readContext, out EdmTypeKind typeKind) { - if (oDataValue == null) + if (oDataValue == null || oDataValue is ODataNullValue) { typeKind = EdmTypeKind.None; return null; } - ODataEnumValue enumValue = oDataValue as ODataEnumValue; - if (enumValue != null) + if (oDataValue is ODataEnumValue enumValue) { typeKind = EdmTypeKind.Enum; return ConvertEnumValue(enumValue, ref propertyType, deserializerProvider, readContext); } - ODataCollectionValue collection = oDataValue as ODataCollectionValue; - if (collection != null) + if (oDataValue is ODataCollectionValue collectionValue) { typeKind = EdmTypeKind.Collection; - return ConvertCollectionValue(collection, ref propertyType, deserializerProvider, readContext); + return ConvertCollectionValue(collectionValue, ref propertyType, deserializerProvider, readContext); } - ODataUntypedValue untypedValue = oDataValue as ODataUntypedValue; - if (untypedValue != null) + if (oDataValue is ODataResourceValue resourceValue) + { + return ConvertResourceValue(resourceValue, ref propertyType, deserializerProvider, readContext, out typeKind); + } + + if (oDataValue is ODataUntypedValue untypedValue) { Contract.Assert(!String.IsNullOrEmpty(untypedValue.RawValue)); @@ -310,6 +313,12 @@ internal static object ConvertValue(object oDataValue, ref IEdmTypeReference pro oDataValue = ConvertPrimitiveValue(untypedValue.RawValue); } + if (oDataValue is ODataPrimitiveValue primitiveValue) + { + typeKind = EdmTypeKind.Primitive; + return EdmPrimitiveHelper.ConvertPrimitiveValue(primitiveValue.Value, primitiveValue.Value.GetType()); + } + typeKind = EdmTypeKind.Primitive; return oDataValue; } @@ -365,33 +374,43 @@ private static object GetProperty(object resource, string propertyName) } private static object ConvertCollectionValue(ODataCollectionValue collection, - ref IEdmTypeReference propertyType, IODataDeserializerProvider deserializerProvider, + ref IEdmTypeReference valueType, IODataDeserializerProvider deserializerProvider, ODataDeserializerContext readContext) { // Be noted: If a declared property (propertyType != null) is untyped (or collection), // It should be never come here. Because for collection untyped, it goes to nested resource set. // ODL reads the value as ODataResourceSet in a ODataNestedResourceInfo. // So, if it comes here, the untyped value is odata.type annotated. for example, create a ODataProperty using ODataCollectionValue + + // 05/01/2024: new scenario, if we specify an instance annotation using the collection value as: + // ""Magics@NS.StringCollection"":[""Skyline"",7,""Beaver""], + // ODL generates 'ODataCollectionValue' without providing the 'TypeName' on ODataCollectionValue. + // So the above assumption could not be correct again. IEdmCollectionTypeReference collectionType; - if (propertyType == null || propertyType.IsUntyped()) + if (valueType == null || valueType.IsUntyped()) { - // dynamic collection property or untyped value @odata.type annotated - Contract.Assert(!String.IsNullOrEmpty(collection.TypeName), - "ODataLib should have verified that dynamic collection value has a type name " + - "since we provided metadata."); - string elementTypeName = GetCollectionElementTypeName(collection.TypeName, isNested: false); - IEdmModel model = readContext.Model; - IEdmSchemaType elementType = model.FindType(elementTypeName); - Contract.Assert(elementType != null); - collectionType = - new EdmCollectionTypeReference( - new EdmCollectionType(elementType.ToEdmTypeReference(isNullable: false))); - propertyType = collectionType; + if (elementTypeName != null) + { + IEdmModel model = readContext.Model; + IEdmSchemaType elementType = model.FindType(elementTypeName); + Contract.Assert(elementType != null); + collectionType = + new EdmCollectionTypeReference( + new EdmCollectionType(elementType.ToEdmTypeReference(isNullable: false))); + } + else + { + // 05/01/2024: If we don't have the property type info meanwhile we don't have 'TypeName' on ODataCollectionValue, + // Let's use the "Collection(Edm.Untyped)" as the collection type. + collectionType = EdmUntypedHelpers.NullablePrimitiveUntypedCollectionReference; + } + + valueType = collectionType; } else { - collectionType = propertyType as IEdmCollectionTypeReference; + collectionType = valueType as IEdmCollectionTypeReference; Contract.Assert(collectionType != null, "The type for collection must be a IEdmCollectionType."); } @@ -399,6 +418,62 @@ private static object ConvertCollectionValue(ODataCollectionValue collection, return deserializer.ReadInline(collection, collectionType, readContext); } + private static object ConvertResourceValue(ODataResourceValue resourceValue, + ref IEdmTypeReference valueType, IODataDeserializerProvider deserializerProvider, + ODataDeserializerContext readContext, out EdmTypeKind typeKind) + { + ODataDeserializerContext nestedReadContext = new ODataDeserializerContext + { + Path = readContext.Path, + Model = readContext.Model, + Request = readContext.Request, + TimeZone = readContext.TimeZone + }; + + IODataEdmTypeDeserializer deserializer; + if (string.IsNullOrEmpty(resourceValue.TypeName)) + { + // If we don't have the type name, treat it as untyped. + valueType = EdmUntypedStructuredTypeReference.NullableTypeReference; + nestedReadContext.ResourceType = typeof(EdmUntypedObject); + deserializer = deserializerProvider.GetEdmTypeDeserializer(valueType); + typeKind = EdmTypeKind.Complex; + return deserializer.ReadInline(resourceValue, valueType, nestedReadContext); + } + + // If we do have the type name, make sure we can resolve the type using type name + IEdmType edmType = readContext.Model.FindType(resourceValue.TypeName); + if (edmType == null) + { + throw new ODataException(Error.Format(SRResources.ResourceTypeNotInModel, resourceValue.TypeName)); + } + + valueType = edmType.ToEdmTypeReference(true); + deserializer = deserializerProvider.GetEdmTypeDeserializer(valueType); + + IEdmStructuredTypeReference structuredType = valueType.AsStructured(); + typeKind = structuredType.IsEntity() ? EdmTypeKind.Entity : EdmTypeKind.Complex; + Type clrType; + if (readContext.IsNoClrType) + { + clrType = structuredType.IsEntity() + ? typeof(EdmEntityObject) + : typeof(EdmComplexObject); + } + else + { + clrType = readContext.Model.GetClrType(structuredType); + if (clrType == null) + { + throw new ODataException( + Error.Format(SRResources.MappingDoesNotContainResourceType, structuredType.FullName())); + } + } + + nestedReadContext.ResourceType = clrType; + return deserializer.ReadInline(resourceValue, valueType, nestedReadContext); + } + private static object ConvertPrimitiveValue(string value) { if (String.CompareOrdinal(value, "null") == 0) diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataCollectionDeserializer.cs b/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataCollectionDeserializer.cs index 38eaacf20..33c61e91c 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataCollectionDeserializer.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataCollectionDeserializer.cs @@ -108,7 +108,9 @@ public sealed override object ReadInline(object item, IEdmTypeReference edmType, } else { - Type elementClrType = readContext.Model.GetClrType(elementType); + Type elementClrType = elementType.IsUntyped() ? + typeof(object) : + readContext.Model.GetClrType(elementType); IEnumerable castedResult = _castMethodInfo.MakeGenericMethod(elementClrType).Invoke(null, new object[] { result }) as IEnumerable; return castedResult; } @@ -136,22 +138,33 @@ public virtual IEnumerable ReadCollectionValue(ODataCollectionValue collectionVa throw Error.ArgumentNull("elementType"); } - IODataEdmTypeDeserializer deserializer = DeserializerProvider.GetEdmTypeDeserializer(elementType); - if (deserializer == null) + if (elementType.IsUntyped()) { - throw new SerializationException( - Error.Format(SRResources.TypeCannotBeDeserialized, elementType.FullName())); + foreach (object item in collectionValue.Items) + { + IEdmTypeReference valueType = null; + yield return DeserializationHelpers.ConvertValue(item, ref valueType, DeserializerProvider, readContext, out _); + } } - - foreach (object item in collectionValue.Items) + else { - if (elementType.IsPrimitive()) + IODataEdmTypeDeserializer deserializer = DeserializerProvider.GetEdmTypeDeserializer(elementType); + if (deserializer == null) { - yield return item; + throw new SerializationException( + Error.Format(SRResources.TypeCannotBeDeserialized, elementType.FullName())); } - else + + foreach (object item in collectionValue.Items) { - yield return deserializer.ReadInline(item, elementType, readContext); + if (elementType.IsPrimitive()) + { + yield return item; + } + else + { + yield return deserializer.ReadInline(item, elementType, readContext); + } } } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataDeserializerContext.cs b/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataDeserializerContext.cs index 65fc77756..6e9227ef3 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataDeserializerContext.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataDeserializerContext.cs @@ -6,11 +6,15 @@ //------------------------------------------------------------------------------ using System; +using System.Reflection; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.OData.Common; using Microsoft.AspNetCore.OData.Deltas; +using Microsoft.AspNetCore.OData.Edm; using Microsoft.AspNetCore.OData.Formatter.Value; +using Microsoft.OData; using Microsoft.OData.Edm; +using Microsoft.OData.ModelBuilder; using Microsoft.OData.UriParser; namespace Microsoft.AspNetCore.OData.Formatter.Deserialization @@ -128,5 +132,78 @@ internal ODataDeserializerContext CloneWithoutType() TimeZone = this.TimeZone }; } + + private CachedItem _cached = new CachedItem(); + + internal IODataInstanceAnnotationContainer GetContainer(object resource, IEdmStructuredType structuredType) + { + if (resource == null || structuredType == null) + { + return null; + } + + // looking for cached first, we use the "reference equality" + if (object.ReferenceEquals(_cached.Resource, resource)) + { + return _cached.Container; + } + + _cached.Resource = resource; // update the cache + _cached.Container = null; + PropertyInfo propertyInfo = Model.GetInstanceAnnotationsContainer(structuredType); + if (propertyInfo == null) + { + return null; + } + + object value; + IDelta delta = resource as IDelta; + if (delta != null) + { + delta.TryGetPropertyValue(propertyInfo.Name, out value); + } + else + { + value = propertyInfo.GetValue(resource); + } + + IODataInstanceAnnotationContainer instanceAnnotationContainer = value as IODataInstanceAnnotationContainer; + if (instanceAnnotationContainer == null) + { + try + { + if (propertyInfo.PropertyType == typeof(ODataInstanceAnnotationContainer) || propertyInfo.PropertyType == typeof(IODataInstanceAnnotationContainer)) + { + instanceAnnotationContainer = new ODataInstanceAnnotationContainer(); + } + else + { + instanceAnnotationContainer = Activator.CreateInstance(propertyInfo.PropertyType) as IODataInstanceAnnotationContainer; + } + + if (delta != null) + { + delta.TrySetPropertyValue(propertyInfo.Name, instanceAnnotationContainer); + } + else + { + propertyInfo.SetValue(resource, instanceAnnotationContainer); + } + } + catch (Exception ex) + { + throw new ODataException(Error.Format(SRResources.CannotCreateInstanceForProperty, propertyInfo.Name), ex); + } + } + + _cached.Container = instanceAnnotationContainer; + return instanceAnnotationContainer; + } + + private struct CachedItem + { + public object Resource; + public IODataInstanceAnnotationContainer Container; + } } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataDeserializerProvider.cs b/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataDeserializerProvider.cs index 485abb688..869ef7a74 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataDeserializerProvider.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataDeserializerProvider.cs @@ -61,7 +61,17 @@ public virtual IODataEdmTypeDeserializer GetEdmTypeDeserializer(IEdmTypeReferenc } IEdmTypeReference elementType = edmType.AsCollection().ElementType(); - if (elementType.IsEntity() || elementType.IsComplex() || elementType.IsUntyped()) + if (elementType.IsUntyped()) + { + if (typeof(IEdmStructuredType).IsAssignableFrom(elementType.Definition.GetType())) + { + return _serviceProvider.GetRequiredService(); + } + + return _serviceProvider.GetRequiredService(); + } + + if (elementType.IsEntity() || elementType.IsComplex()) { return _serviceProvider.GetRequiredService(); } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataResourceDeserializer.cs b/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataResourceDeserializer.cs index 192a16725..bffcbc3d0 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataResourceDeserializer.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataResourceDeserializer.cs @@ -27,6 +27,8 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.OData; using Microsoft.OData.Edm; +using Microsoft.OData.Edm.Vocabularies; +using Microsoft.OData.ModelBuilder; using Microsoft.OData.UriParser; namespace Microsoft.AspNetCore.OData.Formatter.Deserialization @@ -118,6 +120,11 @@ public sealed override object ReadInline(object item, IEdmTypeReference edmType, throw Error.Argument("edmType", SRResources.ArgumentMustBeOfType, "Entity, Complex or Untyped"); } + if (item is ODataResourceValue resourceValue) + { + item = new ODataResourceWrapper(resourceValue); + } + ODataResourceWrapper resourceWrapper = item as ODataResourceWrapper; if (resourceWrapper == null) { @@ -152,9 +159,11 @@ public virtual object ReadResource(ODataResourceWrapper resourceWrapper, IEdmStr throw new ArgumentNullException(nameof(readContext)); } - if (!String.IsNullOrEmpty(resourceWrapper.Resource.TypeName) && - structuredType.FullName() != resourceWrapper.Resource.TypeName && - resourceWrapper.Resource.TypeName != "Edm.Untyped") + string typeName = resourceWrapper.IsResourceValue ? + resourceWrapper.ResourceValue.TypeName : + resourceWrapper.Resource.TypeName; + + if (!String.IsNullOrEmpty(typeName) && structuredType.FullName() != typeName && typeName != "Edm.Untyped") { // received a derived type in a base type deserializer. delegate it to the appropriate derived type deserializer. IEdmModel model = readContext.Model; @@ -164,15 +173,15 @@ 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(typeName) as IEdmStructuredType; if (actualType == null) { - throw new ODataException(Error.Format(SRResources.ResourceTypeNotInModel, resourceWrapper.Resource.TypeName)); + throw new ODataException(Error.Format(SRResources.ResourceTypeNotInModel, typeName)); } if (actualType.IsAbstract) { - string message = Error.Format(SRResources.CannotInstantiateAbstractResourceType, resourceWrapper.Resource.TypeName); + string message = Error.Format(SRResources.CannotInstantiateAbstractResourceType, typeName); throw new ODataException(message); } @@ -208,6 +217,7 @@ public virtual object ReadResource(ODataResourceWrapper resourceWrapper, IEdmStr { object resource = CreateResourceInstance(structuredType, readContext); ApplyResourceProperties(resource, resourceWrapper, structuredType, readContext); + ApplyResourceInstanceAnnotations(resource, resourceWrapper, structuredType, readContext); ApplyDeletedResource(resource, resourceWrapper, readContext); return resource; } @@ -269,19 +279,19 @@ public virtual object CreateResourceInstance(IEdmStructuredTypeReference structu if (readContext.IsDeltaOfT || readContext.IsDeltaDeleted) { - IEnumerable updatablePoperties = model.GetAllProperties(structuredType.StructuredDefinition()); + IEnumerable updatableProperties = model.GetAllProperties(structuredType.StructuredDefinition()); if (structuredType.IsOpen()) { PropertyInfo dynamicDictionaryPropertyInfo = model.GetDynamicPropertyDictionary( structuredType.StructuredDefinition()); - return Activator.CreateInstance(readContext.ResourceType, clrType, updatablePoperties, + return Activator.CreateInstance(readContext.ResourceType, clrType, updatableProperties, dynamicDictionaryPropertyInfo, structuredType.IsComplex()); } else { - return Activator.CreateInstance(readContext.ResourceType, clrType, updatablePoperties, null, structuredType.IsComplex()); + return Activator.CreateInstance(readContext.ResourceType, clrType, updatableProperties, null, structuredType.IsComplex()); } } else @@ -474,6 +484,67 @@ public virtual void ApplyNestedProperty(object resource, ODataNestedResourceInfo } } + /// + /// Deserializes the instance annotations from into . + /// + /// The object into which the annotations should be read. + /// The resource object containing the annotations. + /// The type of the resource. + /// The deserializer context. + public virtual void ApplyResourceInstanceAnnotations(object resource, ODataResourceWrapper resourceWrapper, + IEdmStructuredTypeReference structuredType, ODataDeserializerContext readContext) + { + if (resourceWrapper == null) + { + throw new ArgumentNullException(nameof(resourceWrapper)); + } + + ICollection annotations = resourceWrapper.IsResourceValue ? + resourceWrapper.ResourceValue.InstanceAnnotations : + resourceWrapper.Resource.InstanceAnnotations; + + if (annotations == null || annotations.Count == 0) + { + return; + } + + IODataInstanceAnnotationContainer container = readContext.GetContainer(resource, structuredType.StructuredDefinition()); + if (container == null) + { + return; + } + + foreach (ODataInstanceAnnotation annotation in annotations) + { + IEdmTypeReference valueType = null; + + object annotationValue = DeserializationHelpers.ConvertValue(annotation.Value, ref valueType, DeserializerProvider, readContext, out _); + container.AddResourceAnnotation(annotation.Name, annotationValue); + } + } + + /// + /// Deserializes the nested property info from into . + /// Nested property info contains annotations for the property but without the property value. + /// + /// The object into which the structural properties should be read. + /// The resource object containing the structural properties. + /// The type of the resource. + /// The deserializer context. + public virtual void ApplyNestedPropertyInfos(object resource, ODataResourceWrapper resourceWrapper, + IEdmStructuredTypeReference structuredType, ODataDeserializerContext readContext) + { + if (resourceWrapper == null) + { + throw new ArgumentNullException(nameof(resourceWrapper)); + } + + foreach (ODataPropertyInfo property in resourceWrapper.NestedPropertyInfos) + { + ApplyPropertyInstanceAnnotations(resource, property, structuredType, readContext); + } + } + /// /// Deserializes the structural properties from into . /// @@ -489,7 +560,11 @@ public virtual void ApplyStructuralProperties(object resource, ODataResourceWrap throw new ArgumentNullException(nameof(resourceWrapper)); } - foreach (ODataProperty property in resourceWrapper.Resource.Properties) + IEnumerable properties = resourceWrapper.IsResourceValue ? + resourceWrapper.ResourceValue.Properties : + resourceWrapper.Resource.Properties; + + foreach (ODataProperty property in properties) { ApplyStructuralProperty(resource, property, structuredType, readContext); } @@ -526,12 +601,56 @@ public virtual void ApplyStructuralProperty(object resource, ODataProperty struc } DeserializationHelpers.ApplyProperty(structuralProperty, structuredType, resource, DeserializerProvider, readContext); + + ApplyPropertyInstanceAnnotations(resource, structuralProperty, structuredType, readContext); + } + + /// + /// Deserializes the instance annotations into . + /// + /// The object into which the structural property should be read. + /// The structural property info. + /// The type of the resource. + /// The deserializer context. + public virtual void ApplyPropertyInstanceAnnotations(object resource, ODataPropertyInfo structuralProperty, + IEdmStructuredTypeReference structuredType, ODataDeserializerContext readContext) + { + if (structuralProperty == null) + { + throw new ArgumentNullException(nameof(structuralProperty)); + } + + if (structuralProperty.InstanceAnnotations.Count == 0) + { + return; + } + + IODataInstanceAnnotationContainer container = readContext.GetContainer(resource, structuredType.StructuredDefinition()); + if (container == null) + { + return; + } + + foreach (ODataInstanceAnnotation annotation in structuralProperty.InstanceAnnotations) + { + IEdmTypeReference valueType = null; + + IEdmTerm term = readContext.Model.ResolveTerm(annotation.Name); + if (term != null) + { + valueType = term.Type; + } + + object annotationValue = DeserializationHelpers.ConvertValue(annotation.Value, ref valueType, DeserializerProvider, readContext, out _); + container.AddPropertyAnnotation(structuralProperty.Name, annotation.Name, annotationValue); + } } private void ApplyResourceProperties(object resource, ODataResourceWrapper resourceWrapper, IEdmStructuredTypeReference structuredType, ODataDeserializerContext readContext) { ApplyStructuralProperties(resource, resourceWrapper, structuredType, readContext); + ApplyNestedPropertyInfos(resource, resourceWrapper, structuredType, readContext); ApplyNestedProperties(resource, resourceWrapper, structuredType, readContext); } @@ -566,15 +685,19 @@ private void ApplyDynamicResourceInNestedProperty(string propertyName, object re object value = null; if (resourceWrapper != null) { + string typeName = resourceWrapper.IsResourceValue ? + resourceWrapper.ResourceValue.TypeName : + resourceWrapper.Resource.TypeName; + IEdmTypeReference edmTypeReference; - if (resourceWrapper.Resource.TypeName == null || - string.Equals(resourceWrapper.Resource.TypeName, "Edm.Untyped", StringComparison.OrdinalIgnoreCase)) + if (typeName == null || + string.Equals(typeName, "Edm.Untyped", StringComparison.OrdinalIgnoreCase)) { edmTypeReference = EdmUntypedStructuredTypeReference.NullableTypeReference; } else { - IEdmSchemaType elementType = readContext.Model.FindDeclaredType(resourceWrapper.Resource.TypeName); + IEdmSchemaType elementType = readContext.Model.FindDeclaredType(typeName); edmTypeReference = elementType.ToEdmTypeReference(true); } @@ -613,7 +736,7 @@ private object ReadNestedResourceInline(ODataResourceWrapper resourceWrapper, IE { // We should use the given type name to replace the EdmType. // If it's real untyped, use untyped object to read. - edmType = readContext.Model.ResolveResourceType(resourceWrapper.Resource); + edmType = readContext.Model.ResolveResourceType(resourceWrapper); if (edmType.IsUntyped()) { nestedReadContext.ResourceType = typeof(EdmUntypedObject); diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataResourceSetDeserializer.cs b/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataResourceSetDeserializer.cs index f055cc981..ceb9e1c27 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataResourceSetDeserializer.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataResourceSetDeserializer.cs @@ -206,7 +206,7 @@ public virtual object ReadResourceItem(ODataResourceWrapper resourceWrapper, IEd if (elementType == null || elementType.IsUntyped()) { // We should use the given type name to read - elementType = readContext.Model.ResolveResourceType(resourceWrapper.Resource); + elementType = readContext.Model.ResolveResourceType(resourceWrapper); if (elementType.IsUntyped()) { nestedReadContext.ResourceType = typeof(EdmUntypedObject); diff --git a/src/Microsoft.AspNetCore.OData/Formatter/ODataInputFormatter.cs b/src/Microsoft.AspNetCore.OData/Formatter/ODataInputFormatter.cs index bea107117..a2afb7d2f 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/ODataInputFormatter.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/ODataInputFormatter.cs @@ -205,8 +205,6 @@ internal static async Task ReadFromStreamAsync( IODataRequestMessage oDataRequestMessage = ODataMessageWrapperHelper.Create(new StreamWrapper(request.Body), request.Headers, request.GetODataContentIdMapping(), request.GetRouteServices()); - ODataMessageReader oDataMessageReader = new ODataMessageReader(oDataRequestMessage, oDataReaderSettings, model); - disposes.Add(oDataMessageReader); ODataPath path = request.ODataFeature().Path; ODataDeserializerContext readContext = BuildDeserializerContext(request); @@ -216,6 +214,22 @@ internal static async Task ReadFromStreamAsync( readContext.ResourceType = type; readContext.ResourceEdmType = expectedPayloadType; + string preferHeader = RequestPreferenceHelpers.GetRequestPreferHeader(request.Headers); + string annotationFilter = null; + if (!string.IsNullOrEmpty(preferHeader)) + { + oDataRequestMessage.SetHeader(RequestPreferenceHelpers.PreferHeaderName, preferHeader); + annotationFilter = oDataRequestMessage.PreferHeader().AnnotationFilter; + } + + if (annotationFilter != null) + { + oDataReaderSettings.ShouldIncludeAnnotation = ODataUtils.CreateAnnotationFilter(annotationFilter); + } + + ODataMessageReader oDataMessageReader = new ODataMessageReader(oDataRequestMessage, oDataReaderSettings, model); + disposes.Add(oDataMessageReader); + result = await deserializer.ReadAsync(oDataMessageReader, type, readContext).ConfigureAwait(false); } catch (Exception ex) diff --git a/src/Microsoft.AspNetCore.OData/Formatter/ODataOutputFormatterHelper.cs b/src/Microsoft.AspNetCore.OData/Formatter/ODataOutputFormatterHelper.cs index 5ab2cb38a..2d8b7826a 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/ODataOutputFormatterHelper.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/ODataOutputFormatterHelper.cs @@ -154,6 +154,9 @@ internal static async Task WriteToStreamAsync( writeContext.SetComputedProperties(queryOptions?.Compute?.ComputeClause); writeContext.Type = type; + // Retrieve the instance annotations for top-level + writeContext.InstanceAnnotations = request.GetInstanceAnnotations(); + //Set the SelectExpandClause on the context if it was explicitly specified. if (selectExpandDifferentFromQueryOptions != null) { diff --git a/src/Microsoft.AspNetCore.OData/Formatter/ResourceContext.cs b/src/Microsoft.AspNetCore.OData/Formatter/ResourceContext.cs index 958bd4c69..32c12748a 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/ResourceContext.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/ResourceContext.cs @@ -9,6 +9,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; +using System.Reflection; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.OData.Common; using Microsoft.AspNetCore.OData.Deltas; @@ -18,6 +19,8 @@ using Microsoft.AspNetCore.OData.Formatter.Value; using Microsoft.AspNetCore.OData.Query.Wrapper; using Microsoft.OData.Edm; +using Microsoft.OData.ModelBuilder; +using Microsoft.VisualBasic; namespace Microsoft.AspNetCore.OData.Formatter { @@ -27,6 +30,8 @@ namespace Microsoft.AspNetCore.OData.Formatter public class ResourceContext { private object _resourceInstance; + private bool _annotationProcessed = false; + private IODataInstanceAnnotationContainer _annotationContainer = null; /// /// Initializes a new instance of the class. @@ -183,6 +188,44 @@ public object GetPropertyValue(string propertyName) } } + internal IDictionary GetPropertyInstanceAnnotations(IEdmProperty edmProperty) + { + IODataInstanceAnnotationContainer container = GetAnnotationContainer(); + if (container == null) + { + return null; + } + + string clrPropertyName = EdmModel.GetClrPropertyName(edmProperty); + return container.GetPropertyAnnotations(clrPropertyName); + } + + internal IODataInstanceAnnotationContainer GetAnnotationContainer() + { + if (_annotationProcessed) + { + return _annotationContainer; + } + + _annotationProcessed = true; + _annotationContainer = null; + if (EdmObject == null) + { + return _annotationContainer; + } + + object value; + PropertyInfo annotationPropertyInfo = EdmModel.GetInstanceAnnotationsContainer(StructuredType); + if (annotationPropertyInfo == null || + !EdmObject.TryGetPropertyValue(annotationPropertyInfo.Name, out value) || value == null) + { + return _annotationContainer; + } + + _annotationContainer = value as IODataInstanceAnnotationContainer; + return _annotationContainer; + } + private object BuildResourceInstance() { if (EdmObject == null) diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataCollectionSerializer.cs b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataCollectionSerializer.cs index b6fd611cc..c0fd3d327 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataCollectionSerializer.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataCollectionSerializer.cs @@ -159,7 +159,6 @@ public virtual ODataCollectionValue CreateODataCollectionValue(IEnumerable enume if (enumerable != null) { - IODataEdmTypeSerializer itemSerializer = null; foreach (object item in enumerable) { if (item == null) @@ -176,7 +175,7 @@ public virtual ODataCollectionValue CreateODataCollectionValue(IEnumerable enume IEdmTypeReference actualType = writeContext.GetEdmType(item, item.GetType()); Contract.Assert(actualType != null); - itemSerializer = itemSerializer ?? SerializerProvider.GetEdmTypeSerializer(actualType); + IODataEdmTypeSerializer itemSerializer = SerializerProvider.GetEdmTypeSerializer(actualType); if (itemSerializer == null) { throw new SerializationException( diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataPrimitiveSerializer.cs b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataPrimitiveSerializer.cs index fce16d90c..4e12035d6 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataPrimitiveSerializer.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataPrimitiveSerializer.cs @@ -29,6 +29,14 @@ public ODataPrimitiveSerializer() { } + /// + /// Initializes a new instance of . + /// + public ODataPrimitiveSerializer(IODataSerializerProvider serializerProvider) + : base(ODataPayloadKind.Property, serializerProvider) + { + } + /// public override async Task WriteObjectAsync(object graph, Type type, ODataMessageWriter messageWriter, ODataSerializerContext writeContext) { @@ -50,7 +58,15 @@ public override async Task WriteObjectAsync(object graph, Type type, ODataMessag IEdmTypeReference edmType = writeContext.GetEdmType(graph, type); Contract.Assert(edmType != null); - await messageWriter.WritePropertyAsync(this.CreateProperty(graph, edmType, writeContext.RootElementName, writeContext)).ConfigureAwait(false); + ODataProperty property = this.CreateProperty(graph, edmType, writeContext.RootElementName, writeContext); + + if (writeContext.InstanceAnnotations != null) + { + ODataSerializerHelper.AppendInstanceAnnotations(writeContext.InstanceAnnotations, + property.InstanceAnnotations, writeContext, SerializerProvider); + } + + await messageWriter.WritePropertyAsync(property).ConfigureAwait(false); } /// diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataResourceSerializer.cs b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataResourceSerializer.cs index 883c62687..adfb9241a 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataResourceSerializer.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataResourceSerializer.cs @@ -548,6 +548,9 @@ public virtual ODataResource CreateResource(SelectExpandNode selectExpandNode, R resource.SetSerializationInfo(serializationInfo); } + // Try to add the instance annotations for the resource + AppendResourceInstanceAnnotations(resource, selectExpandNode, resourceContext); + // Try to add the dynamic properties if the structural type is open. AppendDynamicProperties(resource, selectExpandNode, resourceContext); @@ -753,6 +756,96 @@ public virtual void AppendDynamicProperties(ODataResource resource, SelectExpand } } + /// + /// Appends the instance annotations to the ODataResource. + /// + /// The describing the resource, which is being annotated. + /// The describing the resource, which is being annotated. + /// The context for the resource instance, which is being annotated. + public virtual void AppendResourceInstanceAnnotations(ODataResourceBase resource, SelectExpandNode selectExpandNode, + ResourceContext resourceContext) + { + if (resource == null) + { + throw Error.ArgumentNull(nameof(resource)); + } + + if (resourceContext == null) + { + throw Error.ArgumentNull(nameof(resourceContext)); + } + + // Annotations as property annotation from the parent annotation container + ODataSerializerHelper.AppendInstanceAnnotations(resourceContext.SerializerContext.InstanceAnnotations, + resource.InstanceAnnotations, resourceContext.SerializerContext, SerializerProvider); + + // Annotations as resource annotation from its own annotation container + IODataInstanceAnnotationContainer instanceAnnotationContainer = resourceContext.GetAnnotationContainer(); + if (instanceAnnotationContainer != null) + { + IDictionary annotationsOnResource = instanceAnnotationContainer.GetResourceAnnotations(); + ODataSerializerHelper.AppendInstanceAnnotations(annotationsOnResource, resource.InstanceAnnotations, resourceContext.SerializerContext, SerializerProvider); + } + } + + /// + public sealed override ODataValue CreateODataValue(object value, IEdmTypeReference expectedType, ODataSerializerContext writeContext) + { + if (!expectedType.IsStructuredOrUntypedStructured()) + { + throw Error.InvalidOperation(SRResources.CannotWriteType, typeof(ODataResourceSerializer).Name, expectedType.FullName()); + } + + ODataResourceValue resourceValue = CreateODataResourceValue(value, expectedType.AsStructured(), writeContext); + if (resourceValue == null) + { + return ODataNullValueExtensions.NullValue; + } + + return resourceValue; + } + + /// + /// Creates an for the object represented by . + /// + /// The enum value. + /// The EDM structured type of the value. + /// The serializer write context. + /// The created . + public virtual ODataResourceValue CreateODataResourceValue(object value, IEdmStructuredTypeReference structuredType, ODataSerializerContext writeContext) + { + if (value == null) + { + return null; + } + + if (writeContext == null) + { + throw Error.ArgumentNull(nameof(writeContext)); + } + + IEdmStructuredTypeReference actualType = GetResourceType(value, writeContext); + ResourceContext resourceContext = new ResourceContext(writeContext, actualType, value); + + SelectExpandNode selectExpandNode = CreateSelectExpandNode(resourceContext); + + ODataResourceValue resourceValue = new ODataResourceValue + { + TypeName = actualType.FullName(), + Properties = CreateStructuralPropertyBag(selectExpandNode, resourceContext) + }; + + // Annotation from its own annotation container as resource annotation + IODataInstanceAnnotationContainer instanceAnnotationContainer = resourceContext.GetAnnotationContainer(); + if (instanceAnnotationContainer != null) + { + IDictionary annotationsOnResource = instanceAnnotationContainer.GetResourceAnnotations(); + ODataSerializerHelper.AppendInstanceAnnotations(annotationsOnResource, resourceValue.InstanceAnnotations, resourceContext.SerializerContext, SerializerProvider); + } + + return resourceValue; + } + /// /// Creates the ETag for the given entity. /// @@ -849,8 +942,15 @@ ODataNestedResourceInfo nestedResourceInfo if (nestedResourceInfo != null) { + IDictionary annotations = null; + IODataInstanceAnnotationContainer instanceAnnotationContainer = resourceContext.GetAnnotationContainer(); + if (instanceAnnotationContainer != null) + { + annotations = instanceAnnotationContainer.GetPropertyAnnotations(dynamicComplexProperty.Key); + } + await writer.WriteStartAsync(nestedResourceInfo).ConfigureAwait(false); - await WriteDynamicComplexPropertyAsync(dynamicComplexProperty.Value, edmTypeReference, resourceContext, writer) + await WriteDynamicComplexPropertyAsync(dynamicComplexProperty.Value, edmTypeReference, resourceContext, writer, annotations) .ConfigureAwait(false); await writer.WriteEndAsync().ConfigureAwait(false); } @@ -858,7 +958,8 @@ await WriteDynamicComplexPropertyAsync(dynamicComplexProperty.Value, edmTypeRefe } } - private async Task WriteDynamicComplexPropertyAsync(object propertyValue, IEdmTypeReference edmType, ResourceContext resourceContext, ODataWriter writer) + private async Task WriteDynamicComplexPropertyAsync(object propertyValue, IEdmTypeReference edmType, ResourceContext resourceContext, ODataWriter writer, + IDictionary annotations = null) { Contract.Assert(resourceContext != null); Contract.Assert(writer != null); @@ -868,6 +969,7 @@ private async Task WriteDynamicComplexPropertyAsync(object propertyValue, IEdmTy // Create the serializer context for the nested and expanded item. ODataSerializerContext nestedWriteContext = new ODataSerializerContext(resourceContext, null, null); + nestedWriteContext.InstanceAnnotations = annotations; // Write object. IODataEdmTypeSerializer serializer = SerializerProvider.GetEdmTypeSerializer(edmType); @@ -909,8 +1011,17 @@ private async Task WriteUntypedPropertiesAsync(SelectExpandNode selectExpandNode continue; } + IDictionary annotations = null; + IODataInstanceAnnotationContainer instanceAnnotationContainer = resourceContext.GetAnnotationContainer(); + if (instanceAnnotationContainer != null) + { + string clrPropertyName = resourceContext.EdmModel.GetClrPropertyName(structuralProperty); + annotations = instanceAnnotationContainer.GetPropertyAnnotations(clrPropertyName); + } + if (propertyValue is ODataProperty odataProperty) { + ODataSerializerHelper.AppendInstanceAnnotations(annotations, odataProperty.InstanceAnnotations, resourceContext.SerializerContext, SerializerProvider); await writer.WriteStartAsync(odataProperty).ConfigureAwait(false); await writer.WriteEndAsync().ConfigureAwait(false); continue; @@ -925,7 +1036,7 @@ ODataNestedResourceInfo nestedResourceInfo if (nestedResourceInfo != null) { await writer.WriteStartAsync(nestedResourceInfo).ConfigureAwait(false); - await WriteDynamicComplexPropertyAsync(propertyValue, actualType, resourceContext, writer).ConfigureAwait(false); + await WriteDynamicComplexPropertyAsync(propertyValue, actualType, resourceContext, writer, annotations).ConfigureAwait(false); await writer.WriteEndAsync().ConfigureAwait(false); } } @@ -1082,6 +1193,9 @@ private async Task WriteComplexAndExpandedNavigationPropertyAsync(IEdmProperty e object propertyValue = resourceContext.GetPropertyValue(edmProperty.Name); + // We could have the instance annotations for nested complex/entity on the parent's instance annotation bag. + IDictionary annotations = resourceContext.GetPropertyInstanceAnnotations(edmProperty); + if (propertyValue == null || propertyValue is NullEdmComplexObject) { if (edmProperty.Type.IsCollection()) @@ -1090,15 +1204,20 @@ private async Task WriteComplexAndExpandedNavigationPropertyAsync(IEdmProperty e // it may just be empty. // If a collection of complex or entities can be related, it is represented as a JSON array. An empty // collection of resources (one that contains no resource) is represented as an empty JSON array. - await writer.WriteStartAsync(new ODataResourceSet + ODataResourceSet resourceSet = new ODataResourceSet { TypeName = edmProperty.Type.FullName() - }).ConfigureAwait(false); + }; + + ODataSerializerHelper.AppendInstanceAnnotations(annotations, resourceSet.InstanceAnnotations, resourceContext.SerializerContext, SerializerProvider); + await writer.WriteStartAsync(resourceSet).ConfigureAwait(false); } else { // If at most one resource can be related, the value is null if no resource is currently related. await writer.WriteStartAsync(resource: null).ConfigureAwait(false); + + // TODO: What shall we do if the resource is null but contains the instance annotations? (@mike) } await writer.WriteEndAsync().ConfigureAwait(false); @@ -1107,6 +1226,7 @@ await writer.WriteStartAsync(new ODataResourceSet { // create the serializer context for the complex and expanded item. ODataSerializerContext nestedWriteContext = new ODataSerializerContext(resourceContext, edmProperty, resourceContext.SerializerContext.QueryContext, selectItem); + nestedWriteContext.InstanceAnnotations = annotations; // write object. IODataEdmTypeSerializer serializer = SerializerProvider.GetEdmTypeSerializer(edmProperty.Type); @@ -1458,7 +1578,7 @@ public virtual object CreateUntypedPropertyValue(IEdmStructuralProperty structur return propertyValue; } - // Ok, we have the Edm type associated and it's not strctured or untyped. + // Ok, we have the Edm type associated and it's not structured or untyped. // we only handle the 'Primitive', the defined 'Enum' or collection of them. IODataEdmTypeSerializer serializer = SerializerProvider.GetEdmTypeSerializer(actualType); if (serializer == null) @@ -1511,7 +1631,17 @@ public virtual ODataProperty CreateStructuralProperty(IEdmStructuralProperty str } } - return serializer.CreateProperty(propertyValue, propertyType, structuralProperty.Name, writeContext); + ODataProperty property = serializer.CreateProperty(propertyValue, propertyType, structuralProperty.Name, writeContext); + + IODataInstanceAnnotationContainer instanceAnnotationContainer = resourceContext.GetAnnotationContainer(); + if (instanceAnnotationContainer != null) + { + string clrPropertyName = writeContext.Model.GetClrPropertyName(structuralProperty); + ODataSerializerHelper.AppendInstanceAnnotations( + instanceAnnotationContainer.GetPropertyAnnotations(clrPropertyName), property.InstanceAnnotations, writeContext, SerializerProvider); + } + + return property; } private IEnumerable CreateODataActions( diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataResourceSetSerializer.cs b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataResourceSetSerializer.cs index 09a02049d..c67580730 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataResourceSetSerializer.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataResourceSetSerializer.cs @@ -125,6 +125,16 @@ private async Task WriteResourceSetAsync(IEnumerable enumerable, IEdmTypeReferen IEdmStructuredTypeReference elementType = GetResourceType(resourceSetType); ODataResourceSet resourceSet = CreateResourceSet(enumerable, resourceSetType.AsCollection(), writeContext); + if (writeContext.InstanceAnnotations != null) + { + ODataSerializerHelper.AppendInstanceAnnotations(writeContext.InstanceAnnotations, + resourceSet.InstanceAnnotations, writeContext, SerializerProvider); + } + + // Since the 'writeContext' is used for each item, let's clear the instance annotations for items. + IDictionary instanceAnnotationsBackup = writeContext.InstanceAnnotations; + writeContext.InstanceAnnotations = null; + Func nextLinkGenerator = GetNextLinkGenerator(resourceSet, enumerable, writeContext); WriteResourceSetInternal(resourceSet, elementType, resourceSetType, writeContext, out bool isUntypedCollection, out IODataEdmTypeSerializer resourceSerializer); @@ -139,6 +149,8 @@ private async Task WriteResourceSetAsync(IEnumerable enumerable, IEdmTypeReferen await WriteResourceSetItemAsync(item, elementType, isUntypedCollection, resourceSetType, writer, resourceSerializer, writeContext).ConfigureAwait(false); } + writeContext.InstanceAnnotations = instanceAnnotationsBackup; + // Subtle and surprising behavior: If the NextPageLink property is set before calling WriteStart(resourceSet), // the next page link will be written early in a manner not compatible with odata.streaming=true. Instead, if // the next page link is not set when calling WriteStart(resourceSet) but is instead set later on that resourceSet @@ -160,6 +172,15 @@ private async Task WriteResourceSetAsync(IAsyncEnumerable asyncEnumerabl IEdmStructuredTypeReference elementType = GetResourceType(resourceSetType); ODataResourceSet resourceSet = CreateResourceSet(asyncEnumerable, resourceSetType.AsCollection(), writeContext); + if (writeContext.InstanceAnnotations != null) + { + ODataSerializerHelper.AppendInstanceAnnotations(writeContext.InstanceAnnotations, + resourceSet.InstanceAnnotations, writeContext, SerializerProvider); + } + + // Since the 'writeContext' is used for each item, let's clear the instance annotations for items. + IDictionary instanceAnnotationsBackup = writeContext.InstanceAnnotations; + writeContext.InstanceAnnotations = null; Func nextLinkGenerator = GetNextLinkGenerator(resourceSet, asyncEnumerable, writeContext); @@ -175,6 +196,8 @@ private async Task WriteResourceSetAsync(IAsyncEnumerable asyncEnumerabl await WriteResourceSetItemAsync(item, elementType, isUntypedCollection, resourceSetType, writer, resourceSerializer, writeContext).ConfigureAwait(false); } + writeContext.InstanceAnnotations = instanceAnnotationsBackup; + // Subtle and surprising behavior: If the NextPageLink property is set before calling WriteStart(resourceSet), // the next page link will be written early in a manner not compatible with odata.streaming=true. Instead, if // the next page link is not set when calling WriteStart(resourceSet) but is instead set later on that resourceSet @@ -285,7 +308,7 @@ private async Task WriteUntypedResourceSetItemAsync(object item, IEdmTypeReferen if (itemEdmType.IsCollection()) { - // If the value is a IList, or other similars, the TryGetEdmType(...) return Collection(Edm.Int32). + // If the value is a IList, or other similar, the TryGetEdmType(...) return Collection(Edm.Int32). // But, ODL doesn't support to write ODataCollectionValue. // Let's directly use untyped collection serialization no matter what type this collection is. itemEdmType = EdmUntypedHelpers.NullableUntypedCollectionReference; diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataSerializerContext.cs b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataSerializerContext.cs index b6530dc8a..ab61fc3e5 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataSerializerContext.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataSerializerContext.cs @@ -251,6 +251,11 @@ private set } } + /// + /// Gets/sets the dictionary to store the instance annotations from the source. + /// + internal IDictionary InstanceAnnotations { get; set; } + /// /// Gets or sets the resource that is being expanded. /// diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataSerializerHelper.cs b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataSerializerHelper.cs new file mode 100644 index 000000000..f6cd161d8 --- /dev/null +++ b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataSerializerHelper.cs @@ -0,0 +1,73 @@ +//----------------------------------------------------------------------------- +// +// 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.AspNetCore.OData.Edm; +using Microsoft.OData; +using Microsoft.OData.Edm; +using Microsoft.AspNetCore.OData.Common; + +namespace Microsoft.AspNetCore.OData.Formatter.Serialization +{ + internal static class ODataSerializerHelper + { + /// + /// Appends instance annotations to the destination. + /// + /// The annotations to write. + /// The destination to hold the instance annotation created. + /// The serializer context. + /// The SerializerProvider to use to write annotations + internal static void AppendInstanceAnnotations(IDictionary annotations, + ICollection destination, + ODataSerializerContext writeContext, + IODataSerializerProvider serializerProvider) + { + if (destination == null || annotations == null || writeContext == null || serializerProvider == null) + { + return; + } + + foreach (var annotation in annotations) + { + string name = annotation.Key; + object value = annotation.Value; + if (value == null || value is ODataNullValue) + { + destination.Add(new ODataInstanceAnnotation(name, ODataNullValueExtensions.NullValue)); + continue; + } + + Type valueType = value.GetType(); + IEdmTypeReference edmTypeReference = writeContext.GetEdmType(value, valueType, isUntyped: true); + if (edmTypeReference.IsUntypedOrCollectionUntyped()) + { + if (TypeHelper.IsEnum(valueType)) + { + // we don't have the Edm enum type in the model, let's write it as string. + destination.Add(new ODataInstanceAnnotation(name, new ODataPrimitiveValue(value.ToString()))); + continue; + } + + // Important!! For the collection of untyped, we need to generate the 'ODataCollectionValue', not the ODataResourceSet. + // So, Let's switch to using Collection(Edm.Untyped) for primitive, not for resource. + edmTypeReference = edmTypeReference.IsCollectionUntyped() ? EdmUntypedHelpers.NullablePrimitiveUntypedCollectionReference : edmTypeReference; + } + + IODataEdmTypeSerializer propertySerializer = serializerProvider.GetEdmTypeSerializer(edmTypeReference); + if (propertySerializer == null) + { + throw Error.NotSupported(SRResources.TypeCannotBeSerialized, edmTypeReference.FullName()); + } + + destination.Add(new ODataInstanceAnnotation(name, + propertySerializer.CreateODataValue(value, edmTypeReference, writeContext))); + } + } + } +} diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataSerializerProvider.cs b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataSerializerProvider.cs index 74c62fed2..b1a7c231a 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataSerializerProvider.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataSerializerProvider.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; +using System.Reflection; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.OData.Common; @@ -60,16 +61,24 @@ public virtual IODataEdmTypeSerializer GetEdmTypeSerializer(IEdmTypeReference ed { return _serviceProvider.GetRequiredService(); } - else if (collectionType.ElementType().IsEntity() || collectionType.ElementType().IsComplex() - || collectionType.ElementType().IsUntyped()) + + var elementType = collectionType.ElementType(); + if (elementType.IsEntity() || elementType.IsComplex()) { return _serviceProvider.GetRequiredService(); } - else + + // Collection of untyped, one is collection of resource, the other is collection of value + if (elementType.IsUntyped()) { - return _serviceProvider.GetRequiredService(); + if (typeof(IEdmStructuredTypeReference).IsAssignableFrom(elementType.GetType())) + { + return _serviceProvider.GetRequiredService(); + } } + return _serviceProvider.GetRequiredService(); + case EdmTypeKind.Complex: case EdmTypeKind.Entity: case EdmTypeKind.Untyped: diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Wrapper/ODataReaderExtensions.cs b/src/Microsoft.AspNetCore.OData/Formatter/Wrapper/ODataReaderExtensions.cs index c0fc09238..f71e2116d 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Wrapper/ODataReaderExtensions.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Wrapper/ODataReaderExtensions.cs @@ -200,6 +200,12 @@ private static void ReadODataItem(ODataReader reader, Stack it resourceSetParentWrapper.Items.Add(new ODataPrimitiveWrapper((ODataPrimitiveValue)reader.Item)); break; + case ODataReaderState.NestedProperty: + Contract.Assert(itemsStack.Count > 0, "The nested property info should be a non-null primitive value within resource wrapper."); + ODataResourceWrapper resourceParentWrapper = (ODataResourceWrapper)itemsStack.Peek(); + resourceParentWrapper.NestedPropertyInfos.Add((ODataPropertyInfo)reader.Item); + break; + default: Contract.Assert(false, "We should never get here, it means the ODataReader reported a wrong state."); break; diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Wrapper/ODataResourceWrapper.cs b/src/Microsoft.AspNetCore.OData/Formatter/Wrapper/ODataResourceWrapper.cs index a0dc68b37..2f0a90047 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Wrapper/ODataResourceWrapper.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Wrapper/ODataResourceWrapper.cs @@ -11,7 +11,7 @@ namespace Microsoft.AspNetCore.OData.Formatter.Wrapper { /// - /// Encapsulates an . + /// Encapsulates an and . /// public sealed class ODataResourceWrapper : ODataItemWrapper { @@ -22,10 +22,21 @@ public sealed class ODataResourceWrapper : ODataItemWrapper public ODataResourceWrapper(ODataResourceBase resource) { Resource = resource; - IsDeletedResource = resource != null && resource is ODataDeletedResource; + ResourceValue = null; + IsResourceValue = false; + } - NestedResourceInfos = new List(); + /// + /// Initializes a new instance of . + /// + /// The wrapped resource value, it could NOT be null. + public ODataResourceWrapper(ODataResourceValue resourceValue) + { + ResourceValue = resourceValue ?? throw Error.ArgumentNull(nameof(resourceValue)); + IsResourceValue = true; + Resource = null; + IsDeletedResource = false; } /// @@ -33,6 +44,18 @@ public ODataResourceWrapper(ODataResourceBase resource) /// public ODataResourceBase Resource { get; } + /// + /// Gets the wrapped . + /// Since the ODataResource can't allow the 'ODataResourceValue' as property value, + /// We have to create a ODataResourceValue to hold the properties. + /// + public ODataResourceValue ResourceValue { get; } + + /// + /// Gets a boolean indicating whether the resource is resource value. + /// + public bool IsResourceValue { get; } + /// /// Gets a boolean indicating whether the resource is deleted resource. /// @@ -41,6 +64,12 @@ public ODataResourceWrapper(ODataResourceBase resource) /// /// Gets the inner nested resource infos. /// - public IList NestedResourceInfos { get; } + public IList NestedResourceInfos { get; } = new List(); + + /// + /// Gets the nested property infos. + /// The nested property info is a property without value but could have instance annotations. + /// + public IList NestedPropertyInfos { get; } = new List(); } } diff --git a/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.csproj b/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.csproj index 3a7bdf343..99bacce78 100644 --- a/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.csproj +++ b/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.csproj @@ -29,13 +29,13 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml b/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml index f529dde27..4e2fd0b97 100644 --- a/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml +++ b/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml @@ -2266,6 +2266,14 @@ The Edm type. The dynamic property container property info. + + + Gets the instance annotation container property info. + + The Edm model. + The Edm type. + The instance annotation container property info. + Gets the model name. @@ -2311,14 +2319,23 @@ The given resource set. The resolved type. - + Resolve the type reference from the type name of The Edm model. - The given resource. + The given resource wrapper. The resolved type. + + + Resolve the term using the annotation identifier. + + The Edm model. + It consists of the namespace or alias of the schema that defines the term, followed by a dot (.), + followed by the name of the term, optionally followed by a hash (#) and a qualifier. + The resolved term or null if not found. + Get all property names for the given structured type. @@ -3052,6 +3069,20 @@ The instance to extend. The from the request container. + + + Set the top-level instance annotations for the request. + + The instance to extend. + The instance annotations + + + + Get the top-level instance annotations for the request. + + The instance to extend. + null or top-level instance annotations. + Gets the setting. @@ -3636,6 +3667,25 @@ The type of the resource. The deserializer context. + + + Deserializes the instance annotations from into . + + The object into which the annotations should be read. + The resource object containing the annotations. + The type of the resource. + The deserializer context. + + + + Deserializes the nested property info from into . + Nested property info contains annotations for the property but without the property value. + + The object into which the structural properties should be read. + The resource object containing the structural properties. + The type of the resource. + The deserializer context. + Deserializes the structural properties from into . @@ -3654,6 +3704,15 @@ The type of the resource. The deserializer context. + + + Deserializes the instance annotations into . + + The object into which the structural property should be read. + The structural property info. + The type of the resource. + The deserializer context. + Create from a set of @@ -4733,6 +4792,11 @@ Initializes a new instance of . + + + Initializes a new instance of . + + @@ -4821,6 +4885,26 @@ The describing the response graph. The context for the resource instance being written. + + + Appends the instance annotations to the ODataResource. + + The describing the resource, which is being annotated. + The describing the resource, which is being annotated. + The context for the resource instance, which is being annotated. + + + + + + + Creates an for the object represented by . + + The enum value. + The EDM structured type of the value. + The serializer write context. + The created . + Creates the ETag for the given entity. @@ -5174,6 +5258,11 @@ Gets a property bag associated with this context to store any generic data. + + + Gets/sets the dictionary to store the instance annotations from the source. + + Gets or sets the resource that is being expanded. @@ -5194,6 +5283,15 @@ Gets or sets the navigation property being expanded. + + + Appends instance annotations to the destination. + + The annotations to write. + The destination to hold the instance annotation created. + The serializer context. + The SerializerProvider to use to write annotations + Creates an with name and value @@ -6337,7 +6435,7 @@ - Encapsulates an . + Encapsulates an and . @@ -6346,11 +6444,29 @@ The wrapped resource item, it could be null. + + + Initializes a new instance of . + + The wrapped resource value, it could NOT be null. + Gets the wrapped . + + + Gets the wrapped . + Since the ODataResource can't allow the 'ODataResourceValue' as property value, + We have to create a ODataResourceValue to hold the properties. + + + + + Gets a boolean indicating whether the resource is resource value. + + Gets a boolean indicating whether the resource is deleted resource. @@ -6361,6 +6477,12 @@ Gets the inner nested resource infos. + + + Gets the nested property infos. + The nested property info is a property without value but could have instance annotations. + + Provides extension methods for to add OData routes. @@ -6893,6 +7015,11 @@ Looks up a localized string similar to Cannot cast $filter of type '{0}' to type '{1}'.. + + + Looks up a localized string similar to Cannot Create an instance for the property '{0}'.. + + Looks up a localized string similar to The property '{0}' does not exist on type '{1}'. Make sure to only use property names that are defined by the type.. diff --git a/src/Microsoft.AspNetCore.OData/Properties/SRResources.Designer.cs b/src/Microsoft.AspNetCore.OData/Properties/SRResources.Designer.cs index a3a66ee9e..bb299b06c 100644 --- a/src/Microsoft.AspNetCore.OData/Properties/SRResources.Designer.cs +++ b/src/Microsoft.AspNetCore.OData/Properties/SRResources.Designer.cs @@ -294,6 +294,15 @@ internal static string CannotCastFilter { } } + /// + /// Looks up a localized string similar to Cannot Create an instance for the property '{0}'.. + /// + internal static string CannotCreateInstanceForProperty { + get { + return ResourceManager.GetString("CannotCreateInstanceForProperty", resourceCulture); + } + } + /// /// Looks up a localized string similar to The property '{0}' does not exist on type '{1}'. Make sure to only use property names that are defined by the type.. /// diff --git a/src/Microsoft.AspNetCore.OData/Properties/SRResources.resx b/src/Microsoft.AspNetCore.OData/Properties/SRResources.resx index 87ee6b31d..3c7561ba2 100644 --- a/src/Microsoft.AspNetCore.OData/Properties/SRResources.resx +++ b/src/Microsoft.AspNetCore.OData/Properties/SRResources.resx @@ -741,6 +741,9 @@ SkipToken doesn't support $orderby expression kind '{0}'. Only support property or simple property path with $orderby when SkipToken enabled. + + Cannot Create an instance for the property '{0}'. + Unable to identify a unique property named '{0}'. {0} = Property Name diff --git a/src/Microsoft.AspNetCore.OData/PublicAPI.Unshipped.txt b/src/Microsoft.AspNetCore.OData/PublicAPI.Unshipped.txt index 1874ae4d2..f485c326d 100644 --- a/src/Microsoft.AspNetCore.OData/PublicAPI.Unshipped.txt +++ b/src/Microsoft.AspNetCore.OData/PublicAPI.Unshipped.txt @@ -441,6 +441,7 @@ Microsoft.AspNetCore.OData.Formatter.Serialization.ODataMetadataSerializer Microsoft.AspNetCore.OData.Formatter.Serialization.ODataMetadataSerializer.ODataMetadataSerializer() -> void Microsoft.AspNetCore.OData.Formatter.Serialization.ODataPrimitiveSerializer Microsoft.AspNetCore.OData.Formatter.Serialization.ODataPrimitiveSerializer.ODataPrimitiveSerializer() -> void +Microsoft.AspNetCore.OData.Formatter.Serialization.ODataPrimitiveSerializer.ODataPrimitiveSerializer(Microsoft.AspNetCore.OData.Formatter.Serialization.IODataSerializerProvider serializerProvider) -> void Microsoft.AspNetCore.OData.Formatter.Serialization.ODataRawValueSerializer Microsoft.AspNetCore.OData.Formatter.Serialization.ODataRawValueSerializer.ODataRawValueSerializer() -> void Microsoft.AspNetCore.OData.Formatter.Serialization.ODataResourceSerializer @@ -655,9 +656,13 @@ Microsoft.AspNetCore.OData.Formatter.Wrapper.ODataResourceSetWrapper.Resources.g Microsoft.AspNetCore.OData.Formatter.Wrapper.ODataResourceSetWrapper.ResourceSet.get -> Microsoft.OData.ODataResourceSet Microsoft.AspNetCore.OData.Formatter.Wrapper.ODataResourceWrapper Microsoft.AspNetCore.OData.Formatter.Wrapper.ODataResourceWrapper.IsDeletedResource.get -> bool +Microsoft.AspNetCore.OData.Formatter.Wrapper.ODataResourceWrapper.IsResourceValue.get -> bool +Microsoft.AspNetCore.OData.Formatter.Wrapper.ODataResourceWrapper.NestedPropertyInfos.get -> System.Collections.Generic.IList Microsoft.AspNetCore.OData.Formatter.Wrapper.ODataResourceWrapper.NestedResourceInfos.get -> System.Collections.Generic.IList Microsoft.AspNetCore.OData.Formatter.Wrapper.ODataResourceWrapper.ODataResourceWrapper(Microsoft.OData.ODataResourceBase resource) -> void +Microsoft.AspNetCore.OData.Formatter.Wrapper.ODataResourceWrapper.ODataResourceWrapper(Microsoft.OData.ODataResourceValue resourceValue) -> void Microsoft.AspNetCore.OData.Formatter.Wrapper.ODataResourceWrapper.Resource.get -> Microsoft.OData.ODataResourceBase +Microsoft.AspNetCore.OData.Formatter.Wrapper.ODataResourceWrapper.ResourceValue.get -> Microsoft.OData.ODataResourceValue Microsoft.AspNetCore.OData.ODataApplicationBuilderExtensions Microsoft.AspNetCore.OData.ODataJsonOptionsSetup Microsoft.AspNetCore.OData.ODataJsonOptionsSetup.Configure(Microsoft.AspNetCore.Mvc.JsonOptions options) -> void @@ -1650,6 +1655,7 @@ override sealed Microsoft.AspNetCore.OData.Formatter.Deserialization.ODataResour override sealed Microsoft.AspNetCore.OData.Formatter.Serialization.ODataCollectionSerializer.CreateODataValue(object graph, Microsoft.OData.Edm.IEdmTypeReference expectedType, Microsoft.AspNetCore.OData.Formatter.Serialization.ODataSerializerContext writeContext) -> Microsoft.OData.ODataValue override sealed Microsoft.AspNetCore.OData.Formatter.Serialization.ODataEnumSerializer.CreateODataValue(object graph, Microsoft.OData.Edm.IEdmTypeReference expectedType, Microsoft.AspNetCore.OData.Formatter.Serialization.ODataSerializerContext writeContext) -> Microsoft.OData.ODataValue override sealed Microsoft.AspNetCore.OData.Formatter.Serialization.ODataPrimitiveSerializer.CreateODataValue(object graph, Microsoft.OData.Edm.IEdmTypeReference expectedType, Microsoft.AspNetCore.OData.Formatter.Serialization.ODataSerializerContext writeContext) -> Microsoft.OData.ODataValue +override sealed Microsoft.AspNetCore.OData.Formatter.Serialization.ODataResourceSerializer.CreateODataValue(object value, Microsoft.OData.Edm.IEdmTypeReference expectedType, Microsoft.AspNetCore.OData.Formatter.Serialization.ODataSerializerContext writeContext) -> Microsoft.OData.ODataValue static Microsoft.AspNetCore.OData.Batch.HttpRequestExtensions.CopyAbsoluteUrl(this Microsoft.AspNetCore.Http.HttpRequest request, System.Uri uri) -> void static Microsoft.AspNetCore.OData.Batch.HttpRequestExtensions.GetODataMessageReader(this Microsoft.AspNetCore.Http.HttpRequest request, System.IServiceProvider requestContainer) -> Microsoft.OData.ODataMessageReader static Microsoft.AspNetCore.OData.Batch.ODataBatchHttpRequestExtensions.GetODataBatchId(this Microsoft.AspNetCore.Http.HttpRequest request) -> System.Guid? @@ -1671,6 +1677,7 @@ static Microsoft.AspNetCore.OData.Edm.EdmModelAnnotationExtensions.GetClrEnumMem static Microsoft.AspNetCore.OData.Edm.EdmModelAnnotationExtensions.GetClrPropertyName(this Microsoft.OData.Edm.IEdmModel edmModel, Microsoft.OData.Edm.IEdmProperty edmProperty) -> string static Microsoft.AspNetCore.OData.Edm.EdmModelAnnotationExtensions.GetConcurrencyProperties(this Microsoft.OData.Edm.IEdmModel model, Microsoft.OData.Edm.IEdmNavigationSource navigationSource) -> System.Collections.Generic.IEnumerable static Microsoft.AspNetCore.OData.Edm.EdmModelAnnotationExtensions.GetDynamicPropertyDictionary(this Microsoft.OData.Edm.IEdmModel edmModel, Microsoft.OData.Edm.IEdmStructuredType edmType) -> System.Reflection.PropertyInfo +static Microsoft.AspNetCore.OData.Edm.EdmModelAnnotationExtensions.GetInstanceAnnotationsContainer(this Microsoft.OData.Edm.IEdmModel edmModel, Microsoft.OData.Edm.IEdmStructuredType edmType) -> System.Reflection.PropertyInfo static Microsoft.AspNetCore.OData.Edm.EdmModelAnnotationExtensions.GetModelName(this Microsoft.OData.Edm.IEdmModel model) -> string static Microsoft.AspNetCore.OData.Edm.EdmModelAnnotationExtensions.GetTypeMapper(this Microsoft.OData.Edm.IEdmModel model) -> Microsoft.AspNetCore.OData.Edm.IODataTypeMapper static Microsoft.AspNetCore.OData.Edm.EdmModelAnnotationExtensions.SetModelName(this Microsoft.OData.Edm.IEdmModel model, string name) -> void @@ -1703,6 +1710,7 @@ static Microsoft.AspNetCore.OData.Extensions.HttpRequestExtensions.CreateETag(th static Microsoft.AspNetCore.OData.Extensions.HttpRequestExtensions.CreateRouteServices(this Microsoft.AspNetCore.Http.HttpRequest request, string routePrefix) -> System.IServiceProvider static Microsoft.AspNetCore.OData.Extensions.HttpRequestExtensions.GetDeserializerProvider(this Microsoft.AspNetCore.Http.HttpRequest request) -> Microsoft.AspNetCore.OData.Formatter.Deserialization.IODataDeserializerProvider static Microsoft.AspNetCore.OData.Extensions.HttpRequestExtensions.GetETagHandler(this Microsoft.AspNetCore.Http.HttpRequest request) -> Microsoft.AspNetCore.OData.Abstracts.IETagHandler +static Microsoft.AspNetCore.OData.Extensions.HttpRequestExtensions.GetInstanceAnnotations(this Microsoft.AspNetCore.Http.HttpRequest request) -> System.Collections.Generic.IDictionary static Microsoft.AspNetCore.OData.Extensions.HttpRequestExtensions.GetModel(this Microsoft.AspNetCore.Http.HttpRequest request) -> Microsoft.OData.Edm.IEdmModel static Microsoft.AspNetCore.OData.Extensions.HttpRequestExtensions.GetNextPageLink(this Microsoft.AspNetCore.Http.HttpRequest request, int pageSize, object instance, System.Func objectToSkipTokenValue) -> System.Uri static Microsoft.AspNetCore.OData.Extensions.HttpRequestExtensions.GetODataVersion(this Microsoft.AspNetCore.Http.HttpRequest request) -> Microsoft.OData.ODataVersion @@ -1715,6 +1723,7 @@ static Microsoft.AspNetCore.OData.Extensions.HttpRequestExtensions.IsNoDollarQue static Microsoft.AspNetCore.OData.Extensions.HttpRequestExtensions.ODataBatchFeature(this Microsoft.AspNetCore.Http.HttpRequest request) -> Microsoft.AspNetCore.OData.Abstracts.IODataBatchFeature static Microsoft.AspNetCore.OData.Extensions.HttpRequestExtensions.ODataFeature(this Microsoft.AspNetCore.Http.HttpRequest request) -> Microsoft.AspNetCore.OData.Abstracts.IODataFeature static Microsoft.AspNetCore.OData.Extensions.HttpRequestExtensions.ODataOptions(this Microsoft.AspNetCore.Http.HttpRequest request) -> Microsoft.AspNetCore.OData.ODataOptions +static Microsoft.AspNetCore.OData.Extensions.HttpRequestExtensions.SetInstanceAnnotations(this Microsoft.AspNetCore.Http.HttpRequest request, System.Collections.Generic.IDictionary instanceAnnotations) -> Microsoft.AspNetCore.Http.HttpRequest static Microsoft.AspNetCore.OData.Extensions.HttpResponseExtensions.IsSuccessStatusCode(this Microsoft.AspNetCore.Http.HttpResponse response) -> bool static Microsoft.AspNetCore.OData.Extensions.LinkGeneratorHelpers.CreateODataLink(this Microsoft.AspNetCore.Http.HttpRequest request, params Microsoft.OData.UriParser.ODataPathSegment[] segments) -> string static Microsoft.AspNetCore.OData.Extensions.LinkGeneratorHelpers.CreateODataLink(this Microsoft.AspNetCore.Http.HttpRequest request, System.Collections.Generic.IList segments) -> string @@ -1817,6 +1826,9 @@ virtual Microsoft.AspNetCore.OData.Formatter.Deserialization.ODataPrimitiveDeser virtual Microsoft.AspNetCore.OData.Formatter.Deserialization.ODataResourceDeserializer.ApplyDeletedResource(object resource, Microsoft.AspNetCore.OData.Formatter.Wrapper.ODataResourceWrapper resourceWrapper, Microsoft.AspNetCore.OData.Formatter.Deserialization.ODataDeserializerContext readContext) -> void virtual Microsoft.AspNetCore.OData.Formatter.Deserialization.ODataResourceDeserializer.ApplyNestedProperties(object resource, Microsoft.AspNetCore.OData.Formatter.Wrapper.ODataResourceWrapper resourceWrapper, Microsoft.OData.Edm.IEdmStructuredTypeReference structuredType, Microsoft.AspNetCore.OData.Formatter.Deserialization.ODataDeserializerContext readContext) -> void virtual Microsoft.AspNetCore.OData.Formatter.Deserialization.ODataResourceDeserializer.ApplyNestedProperty(object resource, Microsoft.AspNetCore.OData.Formatter.Wrapper.ODataNestedResourceInfoWrapper resourceInfoWrapper, Microsoft.OData.Edm.IEdmStructuredTypeReference structuredType, Microsoft.AspNetCore.OData.Formatter.Deserialization.ODataDeserializerContext readContext) -> void +virtual Microsoft.AspNetCore.OData.Formatter.Deserialization.ODataResourceDeserializer.ApplyNestedPropertyInfos(object resource, Microsoft.AspNetCore.OData.Formatter.Wrapper.ODataResourceWrapper resourceWrapper, Microsoft.OData.Edm.IEdmStructuredTypeReference structuredType, Microsoft.AspNetCore.OData.Formatter.Deserialization.ODataDeserializerContext readContext) -> void +virtual Microsoft.AspNetCore.OData.Formatter.Deserialization.ODataResourceDeserializer.ApplyPropertyInstanceAnnotations(object resource, Microsoft.OData.ODataPropertyInfo structuralProperty, Microsoft.OData.Edm.IEdmStructuredTypeReference structuredType, Microsoft.AspNetCore.OData.Formatter.Deserialization.ODataDeserializerContext readContext) -> void +virtual Microsoft.AspNetCore.OData.Formatter.Deserialization.ODataResourceDeserializer.ApplyResourceInstanceAnnotations(object resource, Microsoft.AspNetCore.OData.Formatter.Wrapper.ODataResourceWrapper resourceWrapper, Microsoft.OData.Edm.IEdmStructuredTypeReference structuredType, Microsoft.AspNetCore.OData.Formatter.Deserialization.ODataDeserializerContext readContext) -> void virtual Microsoft.AspNetCore.OData.Formatter.Deserialization.ODataResourceDeserializer.ApplyStructuralProperties(object resource, Microsoft.AspNetCore.OData.Formatter.Wrapper.ODataResourceWrapper resourceWrapper, Microsoft.OData.Edm.IEdmStructuredTypeReference structuredType, Microsoft.AspNetCore.OData.Formatter.Deserialization.ODataDeserializerContext readContext) -> void virtual Microsoft.AspNetCore.OData.Formatter.Deserialization.ODataResourceDeserializer.ApplyStructuralProperty(object resource, Microsoft.OData.ODataProperty structuralProperty, Microsoft.OData.Edm.IEdmStructuredTypeReference structuredType, Microsoft.AspNetCore.OData.Formatter.Deserialization.ODataDeserializerContext readContext) -> void virtual Microsoft.AspNetCore.OData.Formatter.Deserialization.ODataResourceDeserializer.CreateResourceInstance(Microsoft.OData.Edm.IEdmStructuredTypeReference structuredType, Microsoft.AspNetCore.OData.Formatter.Deserialization.ODataDeserializerContext readContext) -> object @@ -1837,6 +1849,7 @@ virtual Microsoft.AspNetCore.OData.Formatter.Serialization.ODataEdmTypeSerialize virtual Microsoft.AspNetCore.OData.Formatter.Serialization.ODataEnumSerializer.CreateODataEnumValue(object graph, Microsoft.OData.Edm.IEdmEnumTypeReference enumType, Microsoft.AspNetCore.OData.Formatter.Serialization.ODataSerializerContext writeContext) -> Microsoft.OData.ODataEnumValue virtual Microsoft.AspNetCore.OData.Formatter.Serialization.ODataPrimitiveSerializer.CreateODataPrimitiveValue(object graph, Microsoft.OData.Edm.IEdmPrimitiveTypeReference primitiveType, Microsoft.AspNetCore.OData.Formatter.Serialization.ODataSerializerContext writeContext) -> Microsoft.OData.ODataPrimitiveValue virtual Microsoft.AspNetCore.OData.Formatter.Serialization.ODataResourceSerializer.AppendDynamicProperties(Microsoft.OData.ODataResource resource, Microsoft.AspNetCore.OData.Formatter.Serialization.SelectExpandNode selectExpandNode, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) -> void +virtual Microsoft.AspNetCore.OData.Formatter.Serialization.ODataResourceSerializer.AppendResourceInstanceAnnotations(Microsoft.OData.ODataResourceBase resource, Microsoft.AspNetCore.OData.Formatter.Serialization.SelectExpandNode selectExpandNode, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) -> void virtual Microsoft.AspNetCore.OData.Formatter.Serialization.ODataResourceSerializer.CreateComplexNestedResourceInfo(Microsoft.OData.Edm.IEdmStructuralProperty complexProperty, Microsoft.OData.UriParser.PathSelectItem pathSelectItem, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) -> Microsoft.OData.ODataNestedResourceInfo virtual Microsoft.AspNetCore.OData.Formatter.Serialization.ODataResourceSerializer.CreateComputedProperty(string propertyName, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) -> Microsoft.OData.ODataProperty virtual Microsoft.AspNetCore.OData.Formatter.Serialization.ODataResourceSerializer.CreateDynamicComplexNestedResourceInfo(string propertyName, object propertyValue, Microsoft.OData.Edm.IEdmTypeReference edmType, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) -> Microsoft.OData.ODataNestedResourceInfo @@ -1844,6 +1857,7 @@ virtual Microsoft.AspNetCore.OData.Formatter.Serialization.ODataResourceSerializ virtual Microsoft.AspNetCore.OData.Formatter.Serialization.ODataResourceSerializer.CreateNavigationLink(Microsoft.OData.Edm.IEdmNavigationProperty navigationProperty, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) -> Microsoft.OData.ODataNestedResourceInfo virtual Microsoft.AspNetCore.OData.Formatter.Serialization.ODataResourceSerializer.CreateODataAction(Microsoft.OData.Edm.IEdmAction action, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) -> Microsoft.OData.ODataAction virtual Microsoft.AspNetCore.OData.Formatter.Serialization.ODataResourceSerializer.CreateODataFunction(Microsoft.OData.Edm.IEdmFunction function, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) -> Microsoft.OData.ODataFunction +virtual Microsoft.AspNetCore.OData.Formatter.Serialization.ODataResourceSerializer.CreateODataResourceValue(object value, Microsoft.OData.Edm.IEdmStructuredTypeReference structuredType, Microsoft.AspNetCore.OData.Formatter.Serialization.ODataSerializerContext writeContext) -> Microsoft.OData.ODataResourceValue virtual Microsoft.AspNetCore.OData.Formatter.Serialization.ODataResourceSerializer.CreateResource(Microsoft.AspNetCore.OData.Formatter.Serialization.SelectExpandNode selectExpandNode, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) -> Microsoft.OData.ODataResource virtual Microsoft.AspNetCore.OData.Formatter.Serialization.ODataResourceSerializer.CreateSelectExpandNode(Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) -> Microsoft.AspNetCore.OData.Formatter.Serialization.SelectExpandNode virtual Microsoft.AspNetCore.OData.Formatter.Serialization.ODataResourceSerializer.CreateStreamProperty(Microsoft.OData.Edm.IEdmStructuralProperty structuralProperty, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) -> Microsoft.OData.ODataStreamPropertyInfo diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/InstanceAnnotations/InstanceAnnotationsController.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/InstanceAnnotations/InstanceAnnotationsController.cs new file mode 100644 index 000000000..cd0e658b7 --- /dev/null +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/InstanceAnnotations/InstanceAnnotationsController.cs @@ -0,0 +1,273 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.OData.Deltas; +using Microsoft.AspNetCore.OData.Extensions; +using Microsoft.AspNetCore.OData.Formatter.Value; +using Microsoft.AspNetCore.OData.Query; +using Microsoft.AspNetCore.OData.Routing.Controllers; +using Microsoft.OData.ModelBuilder; +using Xunit; + +namespace Microsoft.AspNetCore.OData.E2E.Tests.InstanceAnnotations +{ + public class CustomersController : ODataController + { + [EnableQuery] + public IActionResult Get() + { + IDictionary annotations = new Dictionary + { + { "NS.TestAnnotation", 1978 } + }; + + Request.SetInstanceAnnotations(annotations); + + return Ok(InstanceAnnotationsDataSource.Customers); + } + + public IActionResult Get(int key) + { + InsCustomer customer = InstanceAnnotationsDataSource.Customers.FirstOrDefault(c => c.Id == key); + if (customer == null) + { + return NotFound($"Cannot find customer using key {key}!"); + } + + return Ok(customer); + } + + public IActionResult GetAge(int key) + { + InsCustomer customer = InstanceAnnotationsDataSource.Customers.FirstOrDefault(c => c.Id == key); + if (customer == null) + { + return NotFound($"Cannot find customer using key {key}!"); + } + + IDictionary annotationsOfAge = null; + if (customer.AnnotationContainer != null) + { + annotationsOfAge = customer.AnnotationContainer.GetPropertyAnnotations("Age"); + } + + // Set the top-level instance annotations + Request.SetInstanceAnnotations(annotationsOfAge); + + return Ok(customer.Age); + } + + public IActionResult GetName(int key) + { + InsCustomer customer = InstanceAnnotationsDataSource.Customers.FirstOrDefault(c => c.Id == key); + if (customer == null) + { + return NotFound($"Cannot find customer using key {key}!"); + } + + IDictionary annotationsOfAge = null; + if (customer.AnnotationContainer != null) + { + annotationsOfAge = customer.AnnotationContainer.GetPropertyAnnotations("Name"); + } + + // Set the top-level instance annotations + Request.SetInstanceAnnotations(annotationsOfAge); + + return Ok(customer.Name); + } + + public IActionResult GetMagics(int key) + { + InsCustomer customer = InstanceAnnotationsDataSource.Customers.FirstOrDefault(c => c.Id == key); + if (customer == null) + { + return NotFound($"Cannot find customer using key {key}!"); + } + + IDictionary annotationsOfAge = null; + if (customer.AnnotationContainer != null) + { + annotationsOfAge = customer.AnnotationContainer.GetPropertyAnnotations("Magics"); + } + + // Set the top-level instance annotations + Request.SetInstanceAnnotations(annotationsOfAge); + + return Ok(customer.Magics); + } + + public IActionResult GetLocation(int key) + { + InsCustomer customer = InstanceAnnotationsDataSource.Customers.FirstOrDefault(c => c.Id == key); + if (customer == null) + { + return NotFound($"Cannot find customer using key {key}!"); + } + + if (customer.Location == null) + { + return Ok(customer.Location); + } + + IDictionary annotationsOfAge = null; + if (customer.AnnotationContainer != null) + { + annotationsOfAge = customer.AnnotationContainer.GetPropertyAnnotations("Location"); + } + + // Set the top-level instance annotations + Request.SetInstanceAnnotations(annotationsOfAge); + + return Ok(customer.Location); + } + + public IActionResult Post([FromBody] InsCustomer customer) + { + Assert.NotNull(customer); + if (customer.Name == "AnnotationOnTypeName1") + { + Assert.NotNull(customer.AnnotationContainer); + IODataInstanceAnnotationContainer container = customer.AnnotationContainer; + IDictionary resourceAnnotations = container.GetResourceAnnotations(); + Assert.Equal(2, resourceAnnotations.Count); + Assert.Equal(44, resourceAnnotations["NS.Primitive"]); + InsAddress address = Assert.IsType(resourceAnnotations["NS.Resource"]); + Assert.Equal("148TH AVE", address.Street); + Assert.Equal("Seattle", address.City); + Assert.Null(address.AnnotationContainer); + } + else if (customer.Name == "AnnotationOnPropertyName1") + { + Assert.NotNull(customer.AnnotationContainer); + IODataInstanceAnnotationContainer container = customer.AnnotationContainer; + + IDictionary resourceAnnotations = container.GetResourceAnnotations(); + KeyValuePair resourceAnnotation = Assert.Single(resourceAnnotations); + Assert.Equal("NS.Primitive", resourceAnnotation.Key); + Assert.Equal(45, resourceAnnotation.Value); + + IDictionary propertyAnnotations = container.GetPropertyAnnotations("Age"); + + Assert.Equal(2, propertyAnnotations.Count); + Assert.Equal(74, propertyAnnotations["NS.Primitive"]); + IEnumerable collection = propertyAnnotations["NS.CollectionTerm"] as IEnumerable; + Assert.Equal(new int[] { 1, 2, 3 }, collection); + + propertyAnnotations = container.GetPropertyAnnotations("Magics"); + InsAddress address = Assert.IsType(propertyAnnotations["NS.Resource"]); + Assert.Equal("228TH ST", address.Street); + Assert.Equal("Issaquah", address.City); + Assert.Null(address.AnnotationContainer); + } + else if (customer.Name == "AdvancedAnnotations") + { + + } + else if (customer.Name == "UntypedAnnotations") + { + Assert.NotNull(customer.AnnotationContainer); + IODataInstanceAnnotationContainer container = customer.AnnotationContainer; + + IDictionary resourceAnnotations = container.GetResourceAnnotations(); + Assert.Null(resourceAnnotations); + + IDictionary propertyAnnotations = container.GetPropertyAnnotations("Magics"); + + KeyValuePair annotation = Assert.Single(propertyAnnotations); + Assert.Equal("NS.Collection", annotation.Key); + + IEnumerable collection = annotation.Value as IEnumerable; + Assert.Collection(collection, + e => + { + EdmUntypedObject untypedObject = Assert.IsType(e); + Assert.Equal("1199 RD", untypedObject["Street"]); + Assert.Equal("Xin", untypedObject["City"]); + Assert.Equal("Mei", untypedObject["Region"]); + + }, + e => Assert.Null(e), + e => + { + InsAddress address = Assert.IsType(e); + Assert.Equal("Ren RD", address.Street); + Assert.Equal("Shang", address.City); + Assert.Null(address.AnnotationContainer); + }); + + // TODO: ODL can't write the untyped instance annotation value correctly, see https://github.com/OData/odata.net/issues/2994 + // So, have to clear the annotation for serialization correctly. Please remove the codes below when fix the issue at ODL. + container.InstanceAnnotations["Magics"].Clear(); + } + + // In real APP, use the following line to add the new customer into DB. + // For the test purpose, skip it to avoid conflict + // customer.Id = InstanceAnnotationsDataSource.Customers.Count + 1; + // InstanceAnnotationsDataSource.Customers.Add(customer); + + return Created(customer); + } + + public IActionResult Patch(int key, Delta patch) + { + if (key == 77) + { + Assert.NotNull(patch); + Assert.True(patch.TryGetPropertyValue("AnnotationContainer", out object container)); + ODataInstanceAnnotationContainer annotationContainer = Assert.IsType(container); + Assert.Equal(3, annotationContainer.InstanceAnnotations.Count); + Assert.Single(annotationContainer.GetResourceAnnotations()); + Assert.Equal(2, annotationContainer.GetPropertyAnnotations("Age").Count); + Assert.Equal(2, annotationContainer.GetPropertyAnnotations("Magics").Count); + + InsCustomer dummyCustomer = new InsCustomer + { + Id = 77, + Name = "Patch a Dummy" + }; + + Assert.Null(dummyCustomer.AnnotationContainer); // Guard + + patch.Patch(dummyCustomer); + + Assert.Equal(3, dummyCustomer.AnnotationContainer.InstanceAnnotations.Count); + var annotationsForCustomer = Assert.Single(dummyCustomer.AnnotationContainer.GetResourceAnnotations()); + Assert.Equal("NS.Primitive", annotationsForCustomer.Key); + Assert.Equal(777, annotationsForCustomer.Value); + + var annotationsForAgeProperty = annotationContainer.GetPropertyAnnotations("Age"); + Assert.Equal(2, annotationsForAgeProperty.Count); + Assert.Equal(2077, annotationsForAgeProperty["NS.BirthYear"]); + Assert.Equal(new int[] { 71, 72, 73 }, annotationsForAgeProperty["NS.CollectionTerm"]); + + var annotationsForMagicsProperty = annotationContainer.GetPropertyAnnotations("Magics"); + Assert.Equal(2, annotationsForMagicsProperty.Count); + Assert.Equal(new object[] { "Skyline", 7, "Beaver" }, annotationsForMagicsProperty["NS.StringCollection"]); + + InsAddress address = Assert.IsType(annotationsForMagicsProperty["NS.Resource"]); + Assert.Equal("228TH ST", address.Street); + Assert.Equal("Earth", address.City); + Assert.Null(address.AnnotationContainer); + + return Updated(dummyCustomer); + } + + InsCustomer customer = InstanceAnnotationsDataSource.Customers.FirstOrDefault(c => c.Id == key); + if (customer == null) + { + return NotFound($"Cannot find customer using key {key}!"); + } + + return Updated(customer); + } + } +} diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/InstanceAnnotations/InstanceAnnotationsDataModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/InstanceAnnotations/InstanceAnnotationsDataModel.cs new file mode 100644 index 000000000..edabca512 --- /dev/null +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/InstanceAnnotations/InstanceAnnotationsDataModel.cs @@ -0,0 +1,60 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +using System.Collections.Generic; +using Microsoft.OData.ModelBuilder; + +namespace Microsoft.AspNetCore.OData.E2E.Tests.InstanceAnnotations +{ + public class InsCustomer + { + public int Id { get; set; } + + public string Name { get; set; } + + public int Age { get; set; } + + public IList Magics { get; set; } + + public InsAddress Location { get; set; } + + public InsFavoriteSports FavoriteSports { get; set; } + + public IODataInstanceAnnotationContainer AnnotationContainer { get; set; } + } + + public class InsAddress + { + public string City { get; set; } + + public string Street { get; set; } + + public IODataInstanceAnnotationContainer AnnotationContainer { get; set; } + } + + public enum InsSport + { + Soccer, + + Badminton, + + Basketball, + + Baseball, + + Swimming, + + Tennis + } + + public class InsFavoriteSports + { + public InsSport LikeMost { get; set; } + + public List Like { get; set; } + } +} diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/InstanceAnnotations/InstanceAnnotationsDataSource.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/InstanceAnnotations/InstanceAnnotationsDataSource.cs new file mode 100644 index 000000000..d31b3b008 --- /dev/null +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/InstanceAnnotations/InstanceAnnotationsDataSource.cs @@ -0,0 +1,161 @@ +//----------------------------------------------------------------------------- +// +// 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.ModelBuilder; + +namespace Microsoft.AspNetCore.OData.E2E.Tests.InstanceAnnotations +{ + public class InstanceAnnotationsDataSource + { + private static IList _customers = null; + + public static IList Customers => _customers; + + static InstanceAnnotationsDataSource() + { + _customers = new List(); + + // Be noted: The data for customers (1,2...6) are designed for certain test cases. + // If you want to change, remember to change the related test cases. + + // 1. Without any instance annotation + InsCustomer customer1 = new InsCustomer + { + Id = 1, + Name = "Peter", + Age = 19, + Magics = new List() { 1, 2 }, + Location = new InsAddress { City = "City 1", Street = "Street 1" }, + FavoriteSports = new InsFavoriteSports() + { + LikeMost = InsSport.Soccer, + Like = new List { InsSport.Basketball, InsSport.Badminton } + } + }; + _customers.Add(customer1); + + // 2. With instance annotation on entity + InsCustomer customer2 = new InsCustomer + { + Id = 2, + Name = "Sam", + Age = 40, + Magics = new List() { 15 }, + Location = new InsAddress { City = "City 2", Street = "Street 2" }, + FavoriteSports = new InsFavoriteSports() + { + LikeMost = InsSport.Badminton, + Like = new List { InsSport.Soccer, InsSport.Tennis } + } + }; + customer2.AnnotationContainer = new ODataInstanceAnnotationContainer(); + customer2.AnnotationContainer.AddResourceAnnotation("NS.CUSTOMER2.Primitive", 22); + _customers.Add(customer2); + + // 3. With instance annotation on property + InsCustomer customer3 = new InsCustomer + { + Id = 3, + Name = "John", + Age = 34, + Magics = new List() { 98, 81 }, + Location = new InsAddress { City = "City 3", Street = "Street 3" }, + FavoriteSports = new InsFavoriteSports() + { + LikeMost = InsSport.Swimming, + Like = new List { InsSport.Tennis } + } + }; + customer3.AnnotationContainer = new ODataInstanceAnnotationContainer(); + customer3.AnnotationContainer.AddPropertyAnnotation("Age", "NS.CUSTOMER3.Primitive", 33); + customer3.AnnotationContainer.AddPropertyAnnotation("Age", "NS.CUSTOMER3.Collection", new string[] { "abc", "xyz"}); + _customers.Add(customer3); + + // 4. With instance annotation on entity and property + InsCustomer customer4 = new InsCustomer + { + Id = 4, + Name = "Kerry", + Age = 29, + Magics = new List() { 6, 4, 5 }, + Location = new InsAddress { City = "City 4", Street = "Street 4" }, + FavoriteSports = new InsFavoriteSports() + { + LikeMost = InsSport.Tennis, + Like = new List { InsSport.Soccer } + } + }; + customer4.AnnotationContainer = new ODataInstanceAnnotationContainer(); + customer4.AnnotationContainer.AddResourceAnnotation("NS.CUSTOMER4.Complex", new InsAddress + { + Street = "1199 RD", + City = "Shanghai" + }); + customer4.AnnotationContainer.AddPropertyAnnotation("Magics", "NS.CUSTOMER4.Enum", InsSport.Badminton); + _customers.Add(customer4); + + // 5. With nested instance annotation on entity and property + InsCustomer customer5 = new InsCustomer + { + Id = 5, + Name = "Alex", + Age = 08, + Magics = new List() { 9, 10 }, + Location = new InsAddress { City = "City 5", Street = "Street 5" }, + FavoriteSports = new InsFavoriteSports() + { + LikeMost = InsSport.Baseball, + Like = new List { InsSport.Baseball } + } + }; + + customer5.AnnotationContainer = new ODataInstanceAnnotationContainer(); + + InsAddress address = new InsAddress + { + Street = "1115 Star", + City = "Mars", + AnnotationContainer = new ODataInstanceAnnotationContainer() + }; + address.AnnotationContainer.AddResourceAnnotation("NS.CUSTOMER5.NESTED.Primitive", 1101); + address.AnnotationContainer.AddPropertyAnnotation("Street", "NS.CUSTOMER5.NESTED.Primitive", 987); + + customer5.AnnotationContainer.AddPropertyAnnotation("Name", "NS.CUSTOMER5.Complex", address); + _customers.Add(customer5); + + // 6. With instance annotations on complex property and on its type also + InsCustomer customer6 = new InsCustomer + { + Id = 6, + Name = "Liang", + Age = 08, + Magics = new List() { 15 }, + Location = new InsAddress { City = "City 6", Street = "Street 6" }, + FavoriteSports = new InsFavoriteSports() + { + LikeMost = InsSport.Badminton, + Like = new List { InsSport.Badminton } + } + }; + + customer6.AnnotationContainer = new ODataInstanceAnnotationContainer(); + + // annotation for enum property + customer6.AnnotationContainer.AddPropertyAnnotation("FavoriteSports", "NS.CUSTOMER6.Primitive", 921); + + // annotation for complex property on parent entity + customer6.AnnotationContainer.AddPropertyAnnotation("Location", "NS.CUSTOMER6.Primitive", 1115); + + // annotation for complex property on its entity + customer6.Location.AnnotationContainer = new ODataInstanceAnnotationContainer(); + customer6.Location.AnnotationContainer.AddResourceAnnotation("NS.CUSTOMER6.Location.Primitive", 71); + + _customers.Add(customer6); + } + } +} diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/InstanceAnnotations/InstanceAnnotationsEdmModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/InstanceAnnotations/InstanceAnnotationsEdmModel.cs new file mode 100644 index 000000000..ac4493ff2 --- /dev/null +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/InstanceAnnotations/InstanceAnnotationsEdmModel.cs @@ -0,0 +1,32 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +using Microsoft.OData.Edm; +using Microsoft.OData.Edm.Vocabularies; +using Microsoft.OData.ModelBuilder; + +namespace Microsoft.AspNetCore.OData.E2E.Tests.InstanceAnnotations +{ + internal class InstanceAnnotationsEdmModel + { + public static IEdmModel GetEdmModel() + { + var builder = new ODataConventionModelBuilder(); + builder.EntitySet("Customers"); + EdmModel model = builder.GetEdmModel() as EdmModel; + + AddTerm(model); + return model; + } + + private static void AddTerm(EdmModel model) + { + EdmTerm term1 = new EdmTerm("NS", "CollectionTerm", new EdmCollectionTypeReference(new EdmCollectionType(EdmCoreModel.Instance.GetInt32(true)))); + model.AddElement(term1); + } + } +} diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/InstanceAnnotations/InstanceAnnotationsTests.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/InstanceAnnotations/InstanceAnnotationsTests.cs new file mode 100644 index 000000000..cb978253b --- /dev/null +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/InstanceAnnotations/InstanceAnnotationsTests.cs @@ -0,0 +1,513 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading.Tasks; +using Microsoft.AspNetCore.OData.E2E.Tests.Extensions; +using Microsoft.AspNetCore.OData.TestCommon; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.OData.Edm; +using Newtonsoft.Json.Linq; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.AspNetCore.OData.E2E.Tests.InstanceAnnotations +{ + public class InstanceAnnotationsTests : WebApiTestBase + { + private readonly ITestOutputHelper output; + + public InstanceAnnotationsTests(WebApiTestFixture fixture, ITestOutputHelper output) + : base(fixture) + { + this.output = output; + } + + protected static void UpdateConfigureServices(IServiceCollection services) + { + IEdmModel edmModel = InstanceAnnotationsEdmModel.GetEdmModel(); + + services.ConfigureControllers(typeof(CustomersController)); + + services.AddControllers().AddOData(opt => + opt.EnableQueryFeatures().AddRouteComponents("odata", edmModel)); + } + + [Fact] + public async Task QueryEntitySetWithTopLevelInstanceAnnotation_Works() + { + // Arrange + HttpClient client = CreateClient(); + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "odata/customers?$top=2&$select=name"); + request.Headers.Add("accept", "application/json"); + request.Headers.Add("Prefer", @"odata.include-annotations=""*"""); + + // Act + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + + string payloadBody = await response.Content.ReadAsStringAsync(); + + Assert.Equal("{\"@odata.context\":\"http://localhost/odata/$metadata#Customers(Name)\"," + + "\"@NS.TestAnnotation\":1978," + + "\"value\":[" + + "{\"@NS.TestAnnotation\":1978,\"Name\":\"Peter\"}," + + "{\"@NS.TestAnnotation\":1978,\"Name\":\"Sam\"}" + + "]" + + "}", payloadBody); + } + + [Fact] + public async Task QueryEntityWithOutAnyInstanceAnnotation_Works() + { + // Arrange + HttpClient client = CreateClient(); + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "odata/customers/1"); + request.Headers.Add("accept", "application/json"); + request.Headers.Add("Prefer", @"odata.include-annotations=""*"""); + + // Act + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + + string payloadBody = await response.Content.ReadAsStringAsync(); + + Assert.Equal("{\"@odata.context\":\"http://localhost/odata/$metadata#Customers/$entity\"," + + "\"Id\":1," + + "\"Name\":\"Peter\"," + + "\"Age\":19," + + "\"Magics\":[1,2]," + + "\"Location\":{\"City\":\"City 1\",\"Street\":\"Street 1\"}," + + "\"FavoriteSports\":{\"LikeMost\":\"Soccer\",\"Like\":[\"Basketball\",\"Badminton\"]}" + + "}", payloadBody); + } + + [Fact] + public async Task QueryEntityWithInstanceAnnotationOnTypeOnly_Works() + { + // Arrange + HttpClient client = CreateClient(); + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "odata/customers/2"); + request.Headers.Add("accept", "application/json"); + request.Headers.Add("Prefer", @"odata.include-annotations=""*"""); + + // Act + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + + string payloadBody = await response.Content.ReadAsStringAsync(); + + Assert.Equal("{\"@odata.context\":\"http://localhost/odata/$metadata#Customers/$entity\"," + + "\"@NS.CUSTOMER2.Primitive\":22," + + "\"Id\":2," + + "\"Name\":\"Sam\"," + + "\"Age\":40," + + "\"Magics\":[15]," + + "\"Location\":{\"City\":\"City 2\",\"Street\":\"Street 2\"}," + + "\"FavoriteSports\":{\"LikeMost\":\"Badminton\",\"Like\":[\"Soccer\",\"Tennis\"]}}", payloadBody); + } + + [Fact] + public async Task QueryEntityWithSimpleInstanceAnnotationOnPropertyOnly_Works() + { + // Arrange + HttpClient client = CreateClient(); + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "odata/customers/3"); + request.Headers.Add("accept", "application/json"); + request.Headers.Add("Prefer", @"odata.include-annotations=""*"""); + + // Act + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + + string payloadBody = await response.Content.ReadAsStringAsync(); + + Assert.Equal("{\"@odata.context\":\"http://localhost/odata/$metadata#Customers/$entity\"," + + "\"Id\":3," + + "\"Name\":\"John\"," + + "\"Age@NS.CUSTOMER3.Primitive\":33," + + "\"NS.CUSTOMER3.Collection@odata.type\":\"#Collection(String)\"," + + "\"Age@NS.CUSTOMER3.Collection\":[\"abc\",\"xyz\"]," + + "\"Age\":34," + + "\"Magics\":[98,81]," + + "\"Location\":{\"City\":\"City 3\",\"Street\":\"Street 3\"}," + + "\"FavoriteSports\":{\"LikeMost\":\"Swimming\",\"Like\":[\"Tennis\"]}}", payloadBody); + } + + [Fact] + public async Task QueryEntityWithInstanceAnnotationOnTypeAndProperty_Works() + { + // Arrange + HttpClient client = CreateClient(); + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "odata/customers/4"); + request.Headers.Add("accept", "application/json"); + request.Headers.Add("Prefer", @"odata.include-annotations=""*"""); + + // Act + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + + string payloadBody = await response.Content.ReadAsStringAsync(); + + Assert.Equal("{\"@odata.context\":\"http://localhost/odata/$metadata#Customers/$entity\"," + + "\"@NS.CUSTOMER4.Complex\":{" + + "\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.InstanceAnnotations.InsAddress\"," + + "\"City\":\"Shanghai\"," + + "\"Street\":\"1199 RD\"" + + "}," + + "\"Id\":4," + + "\"Name\":\"Kerry\"," + + "\"Age\":29," + + "\"Magics@NS.CUSTOMER4.Enum\":\"Badminton\"," + // Should contain an odata.type annotation for NS.CUSTOMER4.ENUM + "\"Magics\":[6,4,5]," + + "\"Location\":{\"City\":\"City 4\",\"Street\":\"Street 4\"}," + + "\"FavoriteSports\":{\"LikeMost\":\"Tennis\",\"Like\":[\"Soccer\"]}}", payloadBody); + } + + [Fact] + public async Task QueryEntityWithNestedInstanceAnnotation_Works() + { + // Arrange + HttpClient client = CreateClient(); + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "odata/customers/5"); + request.Headers.Add("accept", "application/json"); + request.Headers.Add("Prefer", @"odata.include-annotations=""*"""); + + // Act + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + + string payloadBody = await response.Content.ReadAsStringAsync(); + + Assert.Equal("{\"@odata.context\":\"http://localhost/odata/$metadata#Customers/$entity\"," + + "\"Id\":5," + + "\"Name@NS.CUSTOMER5.Complex\":{" + + "\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.InstanceAnnotations.InsAddress\"," + + "\"@NS.CUSTOMER5.NESTED.Primitive\":1101," + + "\"City\":\"Mars\"," + + "\"Street@NS.CUSTOMER5.NESTED.Primitive\":987," + + "\"Street\":\"1115 Star\"" + + "}," + + "\"Name\":\"Alex\"," + + "\"Age\":8," + + "\"Magics\":[9,10]," + + "\"Location\":{\"City\":\"City 5\",\"Street\":\"Street 5\"}," + + "\"FavoriteSports\":{\"LikeMost\":\"Baseball\",\"Like\":[\"Baseball\"]}}", payloadBody); + } + + [Fact] + public async Task QueryPrimitiveValuePropertyWithInstanceAnnotation_Works() + { + // Arrange + HttpClient client = CreateClient(); + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "odata/customers/3/age"); + request.Headers.Add("accept", "application/json"); + request.Headers.Add("Prefer", @"odata.include-annotations=""*"""); + + // Act + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + + string payloadBody = await response.Content.ReadAsStringAsync(); + + Assert.Equal("{\"@odata.context\":\"http://localhost/odata/$metadata#Customers(3)/Age\"," + + "\"@NS.CUSTOMER3.Primitive\":33," + + "\"NS.CUSTOMER3.Collection@odata.type\":\"#Collection(String)\"," + + "\"@NS.CUSTOMER3.Collection\":[\"abc\",\"xyz\"]," + + "\"value\":34}", payloadBody); + } + + [Fact(Skip = "https://github.com/OData/odata.net/issues/3001")] + public async Task QueryCollectionValuePropertyWithInstanceAnnotation_Works() + { + // Arrange + HttpClient client = CreateClient(); + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "odata/customers/4/magics"); + request.Headers.Add("accept", "application/json"); + request.Headers.Add("Prefer", @"odata.include-annotations=""*"""); + + // Act + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + + string payloadBody = await response.Content.ReadAsStringAsync(); + + Assert.Equal("{\"@odata.context\":\"http://localhost/odata/$metadata#Customers(4)/Magics\"," + + "\"@NS.CUSTOMER4.Enum\":\"Badminton\"," + + "\"value\":[6,4,5]}", payloadBody); + } + + [Fact] + public async Task QueryPropertyValueWithResourceInstanceAnnotation_Works() + { + // Arrange + HttpClient client = CreateClient(); + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "odata/customers/5/name"); + request.Headers.Add("accept", "application/json"); + request.Headers.Add("Prefer", @"odata.include-annotations=""*"""); + + // Act + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + + string payloadBody = await response.Content.ReadAsStringAsync(); + + Assert.Equal("{\"@odata.context\":\"http://localhost/odata/$metadata#Customers(5)/Name\"," + + "\"@NS.CUSTOMER5.Complex\":{" + + "\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.InstanceAnnotations.InsAddress\"," + + "\"@NS.CUSTOMER5.NESTED.Primitive\":1101," + + "\"City\":\"Mars\"," + + "\"Street@NS.CUSTOMER5.NESTED.Primitive\":987," + + "\"Street\":\"1115 Star\"" + + "}," + + "\"value\":\"Alex\"}", payloadBody); + } + + [Fact] + public async Task QueryPropertyValueWithInstanceAnnotationOnPropertyAndInstanceAnnotationOnType_Works() + { + // Arrange + HttpClient client = CreateClient(); + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "odata/customers/6/location"); + request.Headers.Add("accept", "application/json"); + request.Headers.Add("Prefer", @"odata.include-annotations=""*"""); + + // Act + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + + string payloadBody = await response.Content.ReadAsStringAsync(); + + // Instance annotations merged + Assert.Equal("{\"@odata.context\":\"http://localhost/odata/$metadata#Customers(6)/Location\"," + + "\"@NS.CUSTOMER6.Primitive\":1115," + + "\"@NS.CUSTOMER6.Location.Primitive\":71," + + "\"City\":\"City 6\"," + + "\"Street\":\"Street 6\"}", payloadBody); + } + + [Fact] + public async Task CreateEntityWithSimpleInstanceAnnotationOnType_WorksRoundTrip() + { + // Arrange + string payload = @"{ + ""Name"": ""AnnotationOnTypeName1"", + ""Age"": 71, + ""Magics"": [ 3, 42 ], + ""Location"": { ""Street"": ""1 Microsoft Way"", ""City"": ""Redmond"" }, + ""FavoriteSports"":{ + ""LikeMost"":""Badminton"", + ""Like"":[""Basketball"",""Tennis""] + }, + ""@NS.Primitive"":44, + ""@NS.Resource"":{""@odata.type"": ""#Microsoft.AspNetCore.OData.E2E.Tests.InstanceAnnotations.InsAddress"", ""Street"": ""148TH AVE"", ""City"": ""Seattle"" } + }"; + + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, "odata/customers"); + request.Headers.Add("accept", "application/json"); + request.Headers.Add("Prefer", @"odata.include-annotations=""*"""); + request.Content = new StringContent(payload); + request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); + request.Content.Headers.ContentLength = payload.Length; + + HttpClient client = CreateClient(); + + // Act + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.Created, response.StatusCode); + + var result = await response.Content.ReadAsStringAsync(); + Assert.Contains("\"@NS.Primitive\":44,", result); + Assert.Contains("\"@NS.Resource\":{", result); + } + + [Fact] + public async Task CreateEntityWithSimpleInstanceAnnotationOnProperty_WorksRoundTrip() + { + // Arrange + string payload = @"{ + ""Name"": ""AnnotationOnPropertyName1"", + ""Age@NS.Primitive"": 74, + ""Age@NS.CollectionTerm"": [1,2,3], + ""Age"": 71, + ""Magics@NS.Resource"":{""@odata.type"": ""#Microsoft.AspNetCore.OData.E2E.Tests.InstanceAnnotations.InsAddress"", ""Street"": ""228TH ST"", ""City"": ""Issaquah"" }, + ""Magics"": [ 3, 42 ], + ""Location"": { ""Street"": ""1 Microsoft Way"", ""City"": ""Redmond"" }, + ""FavoriteSports"":{ + ""LikeMost"":""Badminton"", + ""Like"":[""Basketball"",""Tennis""] + }, + ""@NS.Primitive"":45 + }"; + + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, "odata/customers"); + request.Headers.Add("accept", "application/json"); + request.Headers.Add("Prefer", @"odata.include-annotations=""*"""); + request.Content = new StringContent(payload); + request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); + request.Content.Headers.ContentLength = payload.Length; + + HttpClient client = CreateClient(); + + // Act + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.Created, response.StatusCode); + + var responseResult = await response.Content.ReadAsStringAsync(); + Assert.Contains("\"@NS.Primitive\":45,", responseResult); + Assert.Contains("\"Age@NS.CollectionTerm\":[1,2,3],", responseResult); + Assert.Contains(",\"Magics@NS.Resource\":{\"@odata.type\":\"#Microsoft.As", responseResult); // the expect string is input intentionally. + } + + [Fact] + public async Task CreateEntityWithAdvancedInstanceAnnotations_WorksRoundTrip() + { + // Arrange + string payload = @"{ + ""Name"": ""AdvancedAnnotations"", + ""Age"": 54, + ""Magics@NS.CollectionResources"":[ + {""@odata.type"": ""#Microsoft.AspNetCore.OData.E2E.Tests.InstanceAnnotations.InsAddress"", ""Street"": ""228TH ST"", ""City"": ""Issaquah"" }, + {""@odata.type"": ""#Microsoft.AspNetCore.OData.E2E.Tests.InstanceAnnotations.InsAddress"", ""Street"": ""228TH ST"", ""City"": ""Issaquah"" }], + ""Magics"": [ 13, 14 ], + ""Location"": { ""Street"": ""1 Microsoft Way"", ""City"": ""Redmond"" }, + ""@NS.Primitive"":520 + }"; + + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, "odata/customers"); + request.Headers.Add("accept", "application/json"); + request.Headers.Add("Prefer", @"odata.include-annotations=""*"""); + request.Content = new StringContent(payload); + request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); + request.Content.Headers.ContentLength = payload.Length; + + HttpClient client = CreateClient(); + + // Act + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.Created, response.StatusCode); + + var responseResult = await response.Content.ReadAsStringAsync(); + Assert.Contains("\"@NS.Primitive\":520,", responseResult); + Assert.Contains("\"NS.CollectionResources@odata.type\":\"#Collection(Untyped)\"", responseResult); + Assert.Contains(",\"Magics@NS.CollectionResources\":[{\"@odata.type\":\"#Microsoft.As", responseResult); // the expect string is input intentionally. + } + + [Fact] + public async Task CreateEntityWithUntypedResourceValueInstanceAnnotations_WorksRoundTrip() + { + // Arrange + string payload = @"{ + ""Name"": ""UntypedAnnotations"", + ""Age"": 101, + ""Magics@NS.Collection"":[ + { ""Street"": ""1199 RD"", ""City"": ""Xin"", ""Region"": ""Mei"" }, + null, + {""@odata.type"": ""#Microsoft.AspNetCore.OData.E2E.Tests.InstanceAnnotations.InsAddress"", ""Street"": ""Ren RD"", ""City"": ""Shang"" }], + ""Magics"": [ 97 ] + }"; + + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, "odata/customers"); + request.Headers.Add("accept", "application/json"); + request.Headers.Add("Prefer", @"odata.include-annotations=""*"""); + request.Content = new StringContent(payload); + request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); + request.Content.Headers.ContentLength = payload.Length; + + HttpClient client = CreateClient(); + + // Act + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.Created, response.StatusCode); + + var responseResult = await response.Content.ReadAsStringAsync(); + + // When fix the issue at ODL https://github.com/OData/odata.net/issues/2994, add more codes to verify the untyped instance annotation serialization. + Assert.NotNull(responseResult); + } + + [Fact] + public async Task UpdateEntityWithSimpleInstanceAnnotationOnPropertyButWithoutValue_WorksRoundTrip() + { + // Arrange + string payload = @"{ + ""Name"": ""NewName"", + ""Age@NS.BirthYear"": 2077, + ""Age@NS.CollectionTerm"": [71,72,73], + ""Age"": 71, + ""Magics@NS.StringCollection"":[""Skyline"",7,""Beaver""], + ""Magics@NS.Resource"":{""@odata.type"": ""#Microsoft.AspNetCore.OData.E2E.Tests.InstanceAnnotations.InsAddress"", ""Street"": ""228TH ST"", ""City"": ""Earth"" }, + ""@NS.Primitive"":777 + }"; + + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Patch, "odata/customers/77"); + request.Headers.Add("accept", "application/json"); + request.Headers.Add("Prefer", @"odata.include-annotations=""*"""); + request.Content = new StringContent(payload); + request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); + request.Content.Headers.ContentLength = payload.Length; + + HttpClient client = CreateClient(); + + // Act + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); + } + } +} diff --git a/test/Microsoft.AspNetCore.OData.Tests/Deltas/DeltaTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Deltas/DeltaTests.cs index b29f1f50c..42693aa75 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Deltas/DeltaTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Deltas/DeltaTests.cs @@ -17,6 +17,7 @@ using Microsoft.AspNetCore.OData.Tests.Commons; using Microsoft.AspNetCore.OData.Tests.Models; using Microsoft.OData.Edm; +using Microsoft.OData.ModelBuilder; using Xunit; namespace Microsoft.AspNetCore.OData.Tests.Deltas @@ -99,6 +100,30 @@ public void RoundTrip_Properties(string propertyName, object value) Assert.Equal(value, retrievedValue); } + [Fact] + public void RoundTrip_PropertyForInstanceAnnotationContainer() + { + // Arrange + string propertyName = "InstanceAnnotations"; + Type dynamicType = typeof(AddressWithDynamicContainer); + PropertyInfo instanceAnnotationContainer = dynamicType.GetProperty(propertyName); + Delta delta = new Delta(); + + // Act & Assert + Assert.True(delta.TryGetPropertyType(propertyName, out _)); + + // Act & Assert + IODataInstanceAnnotationContainer container = new ODataInstanceAnnotationContainer(); + Assert.True(delta.TrySetPropertyValue(propertyName, container)); + + // Act & Assert + Assert.True(delta.TryGetPropertyType(propertyName, out Type propertyType)); + Assert.Same(propertyType, instanceAnnotationContainer.PropertyType); + + delta.TryGetPropertyValue(propertyName, out object retrievedValue); + Assert.Same(container, retrievedValue); + } + [Fact] public void RoundTrip_Properties_InDynamicContainer() { @@ -1139,6 +1164,8 @@ public class AddressWithDynamicContainer public IDictionary Dynamics { get; set; } public IDictionary NonSetDynamics { get; } + + public IODataInstanceAnnotationContainer InstanceAnnotations { get; set; } } } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Edm/EdmModelAnnotationExtensionsTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Edm/EdmModelAnnotationExtensionsTests.cs index 7fec55a8a..0c225a17c 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Edm/EdmModelAnnotationExtensionsTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Edm/EdmModelAnnotationExtensionsTests.cs @@ -8,6 +8,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; using Microsoft.AspNetCore.OData.Edm; using Microsoft.AspNetCore.OData.Tests.Commons; using Microsoft.OData.Edm; @@ -15,6 +16,7 @@ using Microsoft.OData.Edm.Vocabularies; using Microsoft.OData.Edm.Vocabularies.Community.V1; using Microsoft.OData.Edm.Vocabularies.V1; +using Microsoft.OData.ModelBuilder; using Moq; using Xunit; @@ -124,7 +126,7 @@ public void GetAlternateKeysTest_WorksForCommunityAlternateKeys() } [Fact] - public void GetClrEnumMemberAnnotation_ThrowsArugmentNull_ForInputParameters() + public void GetClrEnumMemberAnnotation_ThrowsArgumentNull_ForInputParameters() { // Arrange & Act & Assert IEdmModel model = null; @@ -135,7 +137,7 @@ public void GetClrEnumMemberAnnotation_ThrowsArugmentNull_ForInputParameters() } [Fact] - public void GetClrPropertyName_ThrowsArugmentNull_ForInputParameters() + public void GetClrPropertyName_ThrowsArgumentNull_ForInputParameters() { // Arrange & Act & Assert IEdmModel model = null; @@ -146,7 +148,7 @@ public void GetClrPropertyName_ThrowsArugmentNull_ForInputParameters() } [Fact] - public void GetDynamicPropertyDictionary_ThrowsArugmentNull_ForInputParameters() + public void GetDynamicPropertyDictionary_ThrowsArgumentNull_ForInputParameters() { // Arrange & Act & Assert IEdmModel model = null; @@ -157,7 +159,44 @@ public void GetDynamicPropertyDictionary_ThrowsArugmentNull_ForInputParameters() } [Fact] - public void GetModelName_ThrowsArugmentNull_Model() + public void GetInstanceAnnotationsContainer_ThrowsArgumentNull_ForInputParameters() + { + // Arrange & Act & Assert + IEdmModel model = null; + ExceptionAssert.ThrowsArgumentNull(() => model.GetInstanceAnnotationsContainer(null), "edmModel"); + + model = new Mock().Object; + ExceptionAssert.ThrowsArgumentNull(() => model.GetInstanceAnnotationsContainer(null), "edmType"); + } + + [Fact] + public void GetInstanceAnnotationsContainer_ReturnsCorrectPropertyInfo() + { + // Arrange + PropertyInfo propertyInfo = typeof(InstanceAnnotationTest).GetProperty("Container"); + InstanceAnnotationContainerAnnotation annotation = new InstanceAnnotationContainerAnnotation(propertyInfo); + + EdmModel model = new EdmModel(); + EdmComplexType complex = new EdmComplexType("NS", "Test"); + model.AddElement(complex); + + // Act & Assert + PropertyInfo actual = model.GetInstanceAnnotationsContainer(complex); + Assert.Null(actual); + + // Act & Assert + model.SetAnnotationValue(complex, annotation); + actual = model.GetInstanceAnnotationsContainer(complex); + Assert.Same(propertyInfo, actual); + } + + public class InstanceAnnotationTest + { + public IODataInstanceAnnotationContainer Container { get;} + } + + [Fact] + public void GetModelName_ThrowsArgumentNull_Model() { // Arrange & Act & Assert IEdmModel model = null; @@ -165,7 +204,7 @@ public void GetModelName_ThrowsArugmentNull_Model() } [Fact] - public void SetModelName_ThrowsArugmentNull_Model() + public void SetModelName_ThrowsArgumentNull_Model() { // Arrange & Act & Assert IEdmModel model = null; @@ -221,7 +260,7 @@ public void GetTypeMapper_ReturnsDefaultTypeMapper_IfNullModelOrWithoutTypeMappe } [Fact] - public void SetTypeMapper_ThrowsArugmentNull_Model() + public void SetTypeMapper_ThrowsArgumentNull_Model() { // Arrange & Act & Assert IEdmModel model = null; @@ -247,7 +286,7 @@ public void GetAndSetTypeMapper_RoundTrip() } [Fact] - public void GetAlternateKeys_ThrowsArugmentNull_ForInputParameters() + public void GetAlternateKeys_ThrowsArgumentNull_ForInputParameters() { // Arrange & Act & Assert IEdmModel model = null; diff --git a/test/Microsoft.AspNetCore.OData.Tests/Edm/EdmModelExtensionsTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Edm/EdmModelExtensionsTests.cs index a58e51ea3..76a5fbba3 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Edm/EdmModelExtensionsTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Edm/EdmModelExtensionsTests.cs @@ -6,11 +6,13 @@ //------------------------------------------------------------------------------ using System; +using System.Collections.Generic; using System.Linq; using Microsoft.AspNetCore.OData.Edm; using Microsoft.AspNetCore.OData.Tests.Commons; using Microsoft.OData; using Microsoft.OData.Edm; +using Microsoft.OData.Edm.Csdl; using Microsoft.OData.Edm.Vocabularies; using Moq; using Xunit; @@ -53,7 +55,6 @@ public void ResolveResourceSetType_ThrowsArgumentNull() ExceptionAssert.ThrowsArgumentNull(() => model.ResolveResourceSetType(null), "resourceSet"); } - [Fact] public void GetAllProperties_ThrowsArgumentNull() { @@ -66,6 +67,43 @@ public void GetAllProperties_ThrowsArgumentNull() ExceptionAssert.ThrowsArgumentNull(() => model.GetAllProperties(null), "structuredType"); } + [Fact] + public void ResolveTerm_ThrowsArgumentNull_ForModel() + { + // Arrange & Act & Assert + IEdmModel model = null; + ExceptionAssert.ThrowsArgumentNull(() => model.ResolveTerm(null), "model"); + } + + [Fact] + public void ResolveTerm_WorksForDefinedTerms_CaseSensitiveAndInsensitive() + { + // Arrange + EdmModel model = new EdmModel(); + + // Act & Assert + IEdmTerm term = model.ResolveTerm("Org.OData.Core.V1.Description"); + Assert.NotNull(term); + + // We can't use 'Assert.Same' to compare the object since it's SemanticEdmTerm + Assert.Equal(term.Name, model.ResolveTerm("Org.odata.core.V1.description").Name); + Assert.Equal(term.Name, model.ResolveTerm("Org.OData.Core.V1.Description#any").Name); + } + + [Fact] + public void ResolveTerm_WorksForUserDefinedTerms_CaseSensitiveAndInsensitive() + { + // Arrange + EdmModel model = new EdmModel(); + EdmTerm term = new EdmTerm("NS", "TestTerm", EdmPrimitiveTypeKind.Guid); + model.AddElement(term); + + // Act & Assert + Assert.Same(term, model.ResolveTerm("NS.TestTerm")); + Assert.Same(term, model.ResolveTerm("nS.testterm")); + Assert.Same(term, model.ResolveTerm("nS.testterm#any")); + } + [Fact] public void ResolvePropertyTest_WorksForCaseSensitiveAndInsensitive() { @@ -121,7 +159,7 @@ public void ResolvePropertyTest_ThrowsForAmbiguousPropertyName() } [Fact] - public void FindProperty_ThrowsArugmentNull_ForInputParameters() + public void FindProperty_ThrowsArgumentNull_ForInputParameters() { // Arrange & Act & Assert IEdmModel model = null; @@ -182,7 +220,7 @@ public void FindPropertyTest_ThrowsForResourceTypeNotInModel() } [Fact] - public void ResolveNavigationSource_ThrowsArugmentNull() + public void ResolveNavigationSource_ThrowsArgumentNull() { // Arrange & Act & Assert IEdmModel model = null; @@ -195,11 +233,11 @@ public void ResolveNavigationSource_ThrowsODataException_AmbiguousIdentifier() // Arrange EdmModel model = new EdmModel(); EdmEntityType entityType = new EdmEntityType("NS", "Entity"); - EdmEntityContainer containter = new EdmEntityContainer("NS", "Default"); + EdmEntityContainer container = new EdmEntityContainer("NS", "Default"); model.AddElement(entityType); - model.AddElement(containter); - containter.AddEntitySet("entities", entityType); - containter.AddEntitySet("enTIties", entityType); + model.AddElement(container); + container.AddEntitySet("entities", entityType); + container.AddEntitySet("enTIties", entityType); // Act & Assert Assert.NotNull(model.ResolveNavigationSource("enTIties")); diff --git a/test/Microsoft.AspNetCore.OData.Tests/Extensions/HttpRequestExtensionsTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Extensions/HttpRequestExtensionsTests.cs index af9292141..4e9687330 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Extensions/HttpRequestExtensionsTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Extensions/HttpRequestExtensionsTests.cs @@ -12,6 +12,7 @@ using Microsoft.Extensions.DependencyInjection; using Moq; using System; +using System.Collections.Generic; using Xunit; namespace Microsoft.AspNetCore.OData.Tests.Extensions @@ -34,6 +35,28 @@ public void ODataBatchFeature_ThrowsArgumentNull_Request() ExceptionAssert.ThrowsArgumentNull(() => request.ODataBatchFeature(), "request"); } + [Fact] + public void SetInstanceAnnotations_ThrowsArgumentNull_Request() + { + // Arrange & Act & Assert + HttpRequest request = null; + ExceptionAssert.ThrowsArgumentNull(() => request.SetInstanceAnnotations(null), "request"); + } + + [Fact] + public void SetAndGetInstanceAnnotations_Correctly() + { + // Arrange & Act & Assert + HttpRequest request = RequestFactory.Create(); + Assert.Null(request.GetInstanceAnnotations()); + + // Arrange & Act & Assert + Mock> annotations = new Mock>(); + request.SetInstanceAnnotations(annotations.Object); + + Assert.Same(annotations.Object, request.GetInstanceAnnotations()); + } + [Fact] public void GetModel_ThrowsArgumentNull_Request() { diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/DeserializationHelpersTest.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/DeserializationHelpersTest.cs index 132fd92a3..d7d429a1e 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/DeserializationHelpersTest.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/DeserializationHelpersTest.cs @@ -13,12 +13,15 @@ using Microsoft.AspNetCore.OData.Deltas; using Microsoft.AspNetCore.OData.Edm; using Microsoft.AspNetCore.OData.Formatter.Deserialization; +using Microsoft.AspNetCore.OData.Formatter.Value; using Microsoft.AspNetCore.OData.Tests.Commons; using Microsoft.AspNetCore.OData.Tests.Models; using Microsoft.OData; using Microsoft.OData.Edm; +using Microsoft.OData.ModelBuilder; using Moq; using Xunit; +using Xunit.Sdk; namespace Microsoft.AspNetCore.OData.Tests.Formatter.Deserialization { @@ -348,6 +351,71 @@ public void ConvertValue_Works_WithODataUntypedValue_Double() Assert.Equal((double)-1643000, value); } + [Fact] + public void ConvertValue_Works_WithODataResourceValue() + { + // Arrange + object expect = new object(); + object oDataValue = new ODataResourceValue + { + TypeName = "NS.ResourceValue", + Properties = Enumerable.Empty() + }; + + EdmModel model = new EdmModel(); + EdmComplexType complexType = new EdmComplexType("NS", "ResourceValue"); + var streetProp = complexType.AddStructuralProperty("Street", EdmPrimitiveTypeKind.String); + model.AddElement(complexType); + + Mock deserializer = new Mock(); + deserializer.Setup(e => e.ReadInline(oDataValue, It.IsAny(), It.IsAny())).Returns(expect); + + Mock deserializerProvider = new Mock(); + deserializerProvider.Setup(e => e.GetEdmTypeDeserializer(It.IsAny(), It.IsAny())).Returns(deserializer.Object); + + ODataDeserializerContext readContext = new ODataDeserializerContext + { + Model = model, + ResourceType = typeof(IEdmObject) + }; + + // Act + IEdmTypeReference typeRef = null; + object value = DeserializationHelpers.ConvertValue(oDataValue, ref typeRef, deserializerProvider.Object, readContext, out EdmTypeKind typeKind); + + // Assert + Assert.Equal(EdmTypeKind.Complex, typeKind); + Assert.Same(expect, value); + } + + [Fact] + public void ConvertValue_Works_WithODataCollectionValueWithoutTypeName() + { + // Arrange + object expect = new object(); + object oDataValue = new ODataCollectionValue + { + TypeName = null, + Items = Enumerable.Empty() + }; + + Mock deserializer = new Mock(); + deserializer.Setup(e => e.ReadInline(oDataValue, It.IsAny(), It.IsAny())).Returns(expect); + + Mock deserializerProvider = new Mock(); + deserializerProvider.Setup(e => e.GetEdmTypeDeserializer(It.IsAny(), It.IsAny())).Returns(deserializer.Object); + + ODataDeserializerContext readContext = new ODataDeserializerContext(); + + // Act + IEdmTypeReference typeRef = null; + object value = DeserializationHelpers.ConvertValue(oDataValue, ref typeRef, deserializerProvider.Object, readContext, out EdmTypeKind typeKind); + + // Assert + Assert.Equal(EdmTypeKind.Collection, typeKind); + Assert.Same(expect, value); + } + [Theory] [InlineData("[abc1.643e6]")] [InlineData("{abc1.643e6}")] diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/ODataCollectionDeserializerTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/ODataCollectionDeserializerTests.cs index 3a6c11c84..84eddea92 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/ODataCollectionDeserializerTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/ODataCollectionDeserializerTests.cs @@ -271,6 +271,7 @@ private static IEdmModel GetEdmModel() { ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); builder.EnumType().Namespace = "NS"; + builder.ComplexType().Namespace = "NS"; return builder.GetEdmModel(); } @@ -280,5 +281,112 @@ public enum Color Blue, Green } + + public class CollComplex + { + public string City { get; set; } + public string Street { get; set; } + } + + [Fact] + public void ReadInline_CanRead_CollectionOfResourceValue() + { + // Arrange + ODataCollectionValue collectionValue = new ODataCollectionValue + { + TypeName = "Collection(NS.CollComplex)" + }; + collectionValue.Items = new ODataResourceValue[] + { + new ODataResourceValue + { + TypeName = "NS.CollComplex", + Properties = new ODataProperty[] + { + new ODataProperty { Name = "City", Value = "Shang" }, + new ODataProperty { Name = "Street", Value = "Xia" }, + } + }, + new ODataResourceValue + { + TypeName = "NS.CollComplex", + Properties = new ODataProperty[] + { + new ODataProperty { Name = "City", Value = "Zuo" }, + new ODataProperty { Name = "Street", Value = "You" }, + } + } + }; + + ODataCollectionDeserializer deserializer = new ODataCollectionDeserializer(DeserializerProvider); + IEdmComplexType complexType = Model.SchemaElements.OfType().First(c => c.Name == "CollComplex"); + IEdmTypeReference edmType = new EdmCollectionTypeReference(new EdmCollectionType(new EdmComplexTypeReference(complexType, true))); + + ODataDeserializerContext readContext = new ODataDeserializerContext() { Model = Model }; + + // Act + IEnumerable results = deserializer.ReadInline(collectionValue, edmType, readContext) as IEnumerable; + + // Assert + Assert.NotNull(results); + var complexInstances = results.Cast(); + Assert.Equal(2, complexInstances.Count()); + Assert.Collection(complexInstances, + e => + { + Assert.Equal("Shang", e.City); + Assert.Equal("Xia", e.Street); + }, + e => + { + Assert.Equal("Zuo", e.City); + Assert.Equal("You", e.Street); + }); + } + + [Fact] + public void ReadInline_CanRead_CollectionOfAnyTypeOfValues() + { + // Arrange + ODataCollectionValue collectionValue = new ODataCollectionValue(); + collectionValue.Items = new ODataValue[] + { + new ODataResourceValue + { + TypeName = "NS.CollComplex", + Properties = new ODataProperty[] + { + new ODataProperty { Name = "City", Value = "Xu" }, + new ODataProperty { Name = "Street", Value = "Wu" }, + } + }, + null, + new ODataPrimitiveValue(42), + new ODataEnumValue("Blue", "NS.Color") + }; + + ODataCollectionDeserializer deserializer = new ODataCollectionDeserializer(DeserializerProvider); + IEdmTypeReference edmType = EdmUntypedHelpers.NullablePrimitiveUntypedCollectionReference; + + ODataDeserializerContext readContext = new ODataDeserializerContext() { Model = Model }; + + // Act + IEnumerable results = deserializer.ReadInline(collectionValue, edmType, readContext) as IEnumerable; + + // Assert + Assert.NotNull(results); + var complexInstances = results.Cast(); + Assert.Equal(4, complexInstances.Count()); + Assert.Collection(complexInstances, + e => + { + CollComplex c = Assert.IsType(e); + Assert.Equal("Xu", c.City); + Assert.Equal("Wu", c.Street); + }, + e => Assert.Null(e), + e => Assert.Equal(42, e), + e => Assert.Equal(Color.Blue, e)); + } } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/ODataDeserializerContextTest.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/ODataDeserializerContextTest.cs index 52f255805..dda54db23 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/ODataDeserializerContextTest.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/ODataDeserializerContextTest.cs @@ -6,11 +6,16 @@ //------------------------------------------------------------------------------ using System; +using System.Reflection; using Microsoft.AspNetCore.OData.Deltas; +using Microsoft.AspNetCore.OData.Edm; using Microsoft.AspNetCore.OData.Formatter; using Microsoft.AspNetCore.OData.Formatter.Deserialization; using Microsoft.AspNetCore.OData.Formatter.Value; using Microsoft.AspNetCore.OData.Tests.Models; +using Microsoft.OData.Edm; +using Microsoft.OData.ModelBuilder; +using Moq; using Xunit; namespace Microsoft.AspNetCore.OData.Tests.Formatter.Deserialization @@ -50,5 +55,72 @@ public void Property_IsNoClrType_HasRightValue(Type resourceType, bool expectedR // Assert Assert.Equal(expectedResult, context.IsNoClrType); } + + [Fact] + public void GetContainer_Returns_InstanceAnnotationContainer() + { + // Arrange + EdmModel model = new EdmModel(); + EdmComplexType complex = new EdmComplexType("NS", "Complex"); + model.AddElement(complex); + PropertyInfo propertyInfo = typeof(InstanceAnnotationClass).GetProperty("Container"); + InstanceAnnotationContainerAnnotation annotation = new InstanceAnnotationContainerAnnotation(propertyInfo); + model.SetAnnotationValue(complex, annotation); + + ODataDeserializerContext context = new ODataDeserializerContext + { + Model = model + }; + + // Act + InstanceAnnotationClass resource = new InstanceAnnotationClass(); + Assert.Null(resource.Container); + IODataInstanceAnnotationContainer container = context.GetContainer(resource, complex); + + // Assert + Assert.NotNull(container); + Assert.Same(resource.Container, container); + + IODataInstanceAnnotationContainer container2 = context.GetContainer(resource, complex); + Assert.Same(container, container2); + } + + [Fact] + public void GetContainer_Returns_InstanceAnnotationContainer_ForIDelta() + { + // Arrange + EdmModel model = new EdmModel(); + EdmComplexType complex = new EdmComplexType("NS", "Complex"); + model.AddElement(complex); + PropertyInfo propertyInfo = typeof(InstanceAnnotationClass).GetProperty("Container"); + InstanceAnnotationContainerAnnotation annotation = new InstanceAnnotationContainerAnnotation(propertyInfo); + model.SetAnnotationValue(complex, annotation); + + ODataDeserializerContext context = new ODataDeserializerContext + { + Model = model + }; + + // Act + Delta resource = new Delta(); + Assert.True(resource.TryGetPropertyValue("Container", out object containerOnDelta)); + Assert.Null(containerOnDelta); + + IODataInstanceAnnotationContainer container = context.GetContainer(resource, complex); + + // Assert + Assert.NotNull(container); + Assert.True(resource.TryGetPropertyValue("Container", out containerOnDelta)); + Assert.NotNull(containerOnDelta); + Assert.Same(containerOnDelta, container); + + IODataInstanceAnnotationContainer container2 = context.GetContainer(resource, complex); + Assert.Same(container, container2); + } + + class InstanceAnnotationClass + { + public IODataInstanceAnnotationContainer Container { get; set; } + } } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/ODataDeserializerProviderTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/ODataDeserializerProviderTests.cs index 67fc834cd..fc45714d7 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/ODataDeserializerProviderTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/ODataDeserializerProviderTests.cs @@ -260,7 +260,7 @@ public void GetEdmTypeDeserializer_ReturnsCorrectDeserializer_ForEdmUntyped() } [Fact] - public void GetEdmTypeDeserializer_ReturnsCorrectDeserializer_ForCollectionOfEdmUntyped() + public void GetEdmTypeDeserializer_ReturnsCorrectDeserializer_ForCollectionOfEdmUntypedStructural() { // Arrange IEdmTypeReference edmType = EdmUntypedHelpers.NullableUntypedCollectionReference; @@ -273,6 +273,20 @@ public void GetEdmTypeDeserializer_ReturnsCorrectDeserializer_ForCollectionOfEdm Assert.Equal(ODataPayloadKind.ResourceSet, setSerializer.ODataPayloadKind); } + [Fact] + public void GetEdmTypeDeserializer_ReturnsCorrectDeserializer_ForCollectionOfEdmUntypedPrimitive() + { + // Arrange + IEdmTypeReference edmType = EdmUntypedHelpers.NullablePrimitiveUntypedCollectionReference; + + // Act + var deserializer = _deserializerProvider.GetEdmTypeDeserializer(edmType); + + // Assert + ODataCollectionDeserializer collectionDeserializer = Assert.IsType(deserializer); + Assert.Equal(ODataPayloadKind.Collection, collectionDeserializer.ODataPayloadKind); + } + private static IServiceProvider GetServiceProvider() { IServiceCollection services = new ServiceCollection(); diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/ODataResourceDeserializerTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/ODataResourceDeserializerTests.cs index 00fbe5d0d..739b534c3 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/ODataResourceDeserializerTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/ODataResourceDeserializerTests.cs @@ -418,6 +418,40 @@ public void ReadResource_Calls_ApplyStructuralProperties() deserializer.Verify(); } + [Fact] + public void ReadResource_Calls_ApplyResourceInstanceAnnotations() + { + // Arrange + Mock deserializer = new Mock(_deserializerProvider); + ODataResourceWrapper resourceWrapper = new ODataResourceWrapper(new ODataResource { Properties = Enumerable.Empty() }); + deserializer.CallBase = true; + deserializer.Setup(d => d.CreateResourceInstance(_productEdmType, _readContext)).Returns(42); + deserializer.Setup(d => d.ApplyResourceInstanceAnnotations(42, resourceWrapper, _productEdmType, _readContext)).Verifiable(); + + // Act + deserializer.Object.ReadResource(resourceWrapper, _productEdmType, _readContext); + + // Assert + deserializer.Verify(); + } + + [Fact] + public void ReadResource_Calls_ApplyNestedPropertyInfos() + { + // Arrange + Mock deserializer = new Mock(_deserializerProvider); + ODataResourceWrapper resourceWrapper = new ODataResourceWrapper(new ODataResource { Properties = Enumerable.Empty() }); + deserializer.CallBase = true; + deserializer.Setup(d => d.CreateResourceInstance(_productEdmType, _readContext)).Returns(42); + deserializer.Setup(d => d.ApplyNestedPropertyInfos(42, resourceWrapper, _productEdmType, _readContext)).Verifiable(); + + // Act + deserializer.Object.ReadResource(resourceWrapper, _productEdmType, _readContext); + + // Assert + deserializer.Verify(); + } + [Fact] public void ReadResource_Calls_ApplyNestedProperties() { @@ -653,6 +687,232 @@ public void ReadResource_CanReadDatTimeRelatedProperties() Assert.Equal(new TimeSpan(0, 1, 2, 3, 4), customer.ReleaseTime); } + [Fact] + public void ReadResource_CanReadInstanceAnnotations() + { + // Arrange + ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); + builder.EntityType(); + builder.EnumType(); + IEdmModel model = builder.GetEdmModel(); + + IEdmEntityTypeReference customerTypeReference = model.GetEdmTypeReference(typeof(SimpleOpenCustomer)).AsEntity(); + + var deserializer = new ODataResourceDeserializer(_deserializerProvider); + + ODataEnumValue enumValue = new ODataEnumValue("Third", typeof(SimpleEnum).FullName); + List instanceAnnotations = new List + { + new ODataInstanceAnnotation("NS.Test1", new ODataPrimitiveValue(42)), + new ODataInstanceAnnotation("NS.Test2", new ODataPrimitiveValue(true)) + }; + + ODataResource odataResource = new ODataResource + { + Properties = new ODataProperty[] + { + new ODataProperty + { + Name = "Name", Value = "AManWithExtraData", + InstanceAnnotations = new List { new ODataInstanceAnnotation("NS.ExtraData", enumValue) } + } + }, + TypeName = typeof(SimpleOpenCustomer).FullName, + InstanceAnnotations = instanceAnnotations + }; + + ODataDeserializerContext readContext = new ODataDeserializerContext() + { + Model = model + }; + + ODataResourceWrapper topLevelResourceWrapper = new ODataResourceWrapper(odataResource); + + // Act + SimpleOpenCustomer customer = deserializer.ReadResource(topLevelResourceWrapper, customerTypeReference, readContext) + as SimpleOpenCustomer; + + // Assert + Assert.NotNull(customer); + + // Verify the declared properties + Assert.Equal("AManWithExtraData", customer.Name); + + // Verify the instance annotations + Assert.NotNull(customer.InstanceAnnotations); + Assert.Equal(2, customer.InstanceAnnotations.InstanceAnnotations.Count); + + // Verify instance annotations on resource/entity + IDictionary annotationsOnResource = customer.InstanceAnnotations.GetResourceAnnotations(); + Assert.Equal(2, annotationsOnResource.Count); + + Assert.Equal(42, annotationsOnResource["NS.Test1"]); + Assert.True((bool)annotationsOnResource["NS.Test2"]); + + // Verify the instance annotation on property + IDictionary annotationsOnProperty = customer.InstanceAnnotations.GetPropertyAnnotations("Name"); + KeyValuePair annotationOnProperty = Assert.Single(annotationsOnProperty); + + Assert.Equal("NS.ExtraData", annotationOnProperty.Key); + Assert.Equal(SimpleEnum.Third, annotationOnProperty.Value); + } + + [Fact] + public void ReadResource_CanReadAdvancedInstanceAnnotations() + { + // Arrange + ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); + builder.EntityType(); + builder.EnumType(); + IEdmModel model = builder.GetEdmModel(); + + IEdmEntityTypeReference customerTypeReference = model.GetEdmTypeReference(typeof(SimpleOpenCustomer)).AsEntity(); + var deserializer = new ODataResourceDeserializer(_deserializerProvider); + + ODataResource odataResource = new ODataResource + { + Properties = new ODataProperty[] + { + new ODataProperty + { + Name = "Name", Value = "AManWithExtraResourceValueData", + InstanceAnnotations = new List + { + new ODataInstanceAnnotation("NS.AnnotationOnProperty", new ODataResourceValue + { + TypeName = typeof(SimpleOpenAddress).FullName, + Properties = new[] + { + // declared properties + new ODataProperty {Name = "Street", Value = "Street in property"}, + new ODataProperty {Name = "City", Value = "City in property"}, + } + }) + } + } + }, + TypeName = typeof(SimpleOpenCustomer).FullName, + InstanceAnnotations = new List + { + new ODataInstanceAnnotation("NS.AnnotationOnResource", new ODataResourceValue + { + TypeName = typeof(SimpleOpenAddress).FullName, + Properties = new[] + { + // declared properties + new ODataProperty {Name = "Street", Value = "Street in Resource"}, + new ODataProperty {Name = "City", Value = "City in Resource"}, + } + }) + } + }; + + ODataDeserializerContext readContext = new ODataDeserializerContext() + { + Model = model + }; + + ODataResourceWrapper topLevelResourceWrapper = new ODataResourceWrapper(odataResource); + + // Act + SimpleOpenCustomer customer = deserializer.ReadResource(topLevelResourceWrapper, customerTypeReference, readContext) + as SimpleOpenCustomer; + + // Assert + Assert.NotNull(customer); + + // Verify the declared properties + Assert.Equal("AManWithExtraResourceValueData", customer.Name); + + // Verify the instance annotations + Assert.NotNull(customer.InstanceAnnotations); + Assert.Equal(2, customer.InstanceAnnotations.InstanceAnnotations.Count); + + // Verify instance annotations on resource/entity + IDictionary annotationsOnResource = customer.InstanceAnnotations.GetResourceAnnotations(); + KeyValuePair annotationOnResource = Assert.Single(annotationsOnResource); + + Assert.Equal("NS.AnnotationOnResource", annotationOnResource.Key); + SimpleOpenAddress addressOnResource = Assert.IsType(annotationOnResource.Value); + Assert.Equal("City in Resource", addressOnResource.City); + Assert.Equal("Street in Resource", addressOnResource.Street); + + // Verify the instance annotation on property + IDictionary annotationsOnProperty = customer.InstanceAnnotations.GetPropertyAnnotations("Name"); + KeyValuePair annotationOnProperty = Assert.Single(annotationsOnProperty); + + Assert.Equal("NS.AnnotationOnProperty", annotationOnProperty.Key); + SimpleOpenAddress addressOnProperty = Assert.IsType(annotationOnProperty.Value); + Assert.Equal("City in property", addressOnProperty.City); + Assert.Equal("Street in property", addressOnProperty.Street); + } + + [Fact] + public void ReadResource_CanReadNestedPropertyInfo() + { + // Arrange + ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); + builder.EntityType(); + builder.EnumType(); + IEdmModel model = builder.GetEdmModel(); + + IEdmEntityTypeReference customerTypeReference = model.GetEdmTypeReference(typeof(SimpleOpenCustomer)).AsEntity(); + var deserializer = new ODataResourceDeserializer(_deserializerProvider); + + ODataPropertyInfo propertyInfo = new ODataPropertyInfo + { + Name = "Address", + InstanceAnnotations = new List + { + new ODataInstanceAnnotation("NS.AnnotationOnPropertyWithoutValue", new ODataCollectionValue + { + TypeName = "Collection(Edm.Int32)", + Items = new object[] { 15, 16 } + }) + } + }; + + ODataResource odataResource = new ODataResource + { + Properties = new ODataProperty[] + { + new ODataProperty { Name = "Name", Value = "AManWithNestedPropertyInfo" } + }, + TypeName = typeof(SimpleOpenCustomer).FullName + }; + + ODataDeserializerContext readContext = new ODataDeserializerContext() + { + Model = model + }; + + ODataResourceWrapper topLevelResourceWrapper = new ODataResourceWrapper(odataResource); + topLevelResourceWrapper.NestedPropertyInfos.Add(propertyInfo); + + // Act + SimpleOpenCustomer customer = deserializer.ReadResource(topLevelResourceWrapper, customerTypeReference, readContext) + as SimpleOpenCustomer; + + // Assert + Assert.NotNull(customer); + + // Verify the declared properties + Assert.Equal("AManWithNestedPropertyInfo", customer.Name); + + // Verify the instance annotations + Assert.NotNull(customer.InstanceAnnotations); + KeyValuePair> annotation = Assert.Single(customer.InstanceAnnotations.InstanceAnnotations); + + // Verify instance annotations on resource/entity + Assert.Equal("Address", annotation.Key); + + KeyValuePair annotationOnProperty = Assert.Single(annotation.Value); + + Assert.Equal("NS.AnnotationOnPropertyWithoutValue", annotationOnProperty.Key); + IEnumerable collectionValue = annotationOnProperty.Value as IEnumerable; + Assert.Equal(new int[] { 15, 16 }, collectionValue); + } + [Fact] public void CreateResourceInstance_ThrowsArgumentNull_ReadContext() { @@ -1215,7 +1475,7 @@ public void ApplyNestedProperty_Works_ForDeclaredOrUndelcaredUntypedProperty_Nes [InlineData("Data")] // ==> declared Edm.Untyped property [InlineData("Sources")] // ==> declared Collection(Edm.Untyped) property [InlineData("AnyDynamicPropertyName")] // ==> un-declared (or dynamic) property - public void ApplyNestedProperty_Works_ForDeclaredOrUndelcaredUntypedProperty_NestedCollectionofCollection(string propertyName) + public void ApplyNestedProperty_Works_ForDeclaredOrUndeclaredUntypedProperty_NestedCollectionOfCollection(string propertyName) { // Arrange /* @@ -1280,8 +1540,8 @@ public void ApplyNestedProperty_Works_ForDeclaredOrUndelcaredUntypedProperty_Nes { KeyValuePair singleProperty = Assert.Single(o); Assert.Equal("Aws/Name", singleProperty.Key); - EdmUntypedCollection aswNameValuecol = Assert.IsType(singleProperty.Value); - EdmUntypedCollection colInAwsNameCol = Assert.IsType(Assert.Single(aswNameValuecol)); + EdmUntypedCollection aswNameValueCol = Assert.IsType(singleProperty.Value); + EdmUntypedCollection colInAwsNameCol = Assert.IsType(Assert.Single(aswNameValueCol)); Assert.Equal(2, colInAwsNameCol.Count); Assert.Collection(colInAwsNameCol, e => Assert.True((bool)e), @@ -1377,6 +1637,32 @@ public void ApplyNestedProperty_Works_ForDeltaResourceSetWrapper() Assert.True(supplier.TryGetPropertyValue("Products", out _)); } + [Fact] + public void ApplyResourceInstanceAnnotations_ThrowsArgumentNull_ResourceWrapper() + { + // Arrange + var deserializer = new ODataResourceDeserializer(_deserializerProvider); + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull( + () => deserializer.ApplyResourceInstanceAnnotations(resource: null, resourceWrapper: null, + structuredType: _productEdmType, readContext: _readContext), + "resourceWrapper"); + } + + [Fact] + public void ApplyNestedPropertyInfos_ThrowsArgumentNull_ResourceWrapper() + { + // Arrange + var deserializer = new ODataResourceDeserializer(_deserializerProvider); + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull( + () => deserializer.ApplyNestedPropertyInfos(resource: null, resourceWrapper: null, + structuredType: _productEdmType, readContext: _readContext), + "resourceWrapper"); + } + [Fact] public void ApplyStructuralProperties_ThrowsArgumentNull_resourceWrapper() { @@ -1442,6 +1728,27 @@ public void ApplyStructuralProperty_SetsProperty() Assert.Equal(42, product.ID); } + [Fact] + public void ApplyNestedPropertyInfos_Calls_ApplyPropertyInstanceAnnotationsOnEachPropertyInfo() + { + // Arrange + var deserializer = new Mock(_deserializerProvider); + ODataPropertyInfo[] properties = new[] { new ODataPropertyInfo(), new ODataPropertyInfo() }; + ODataResourceWrapper resourceWrapper = new ODataResourceWrapper(new ODataResource()); + resourceWrapper.NestedPropertyInfos.Add(properties[0]); + resourceWrapper.NestedPropertyInfos.Add(properties[1]); + + deserializer.CallBase = true; + deserializer.Setup(d => d.ApplyPropertyInstanceAnnotations(42, properties[0], _productEdmType, _readContext)).Verifiable(); + deserializer.Setup(d => d.ApplyPropertyInstanceAnnotations(42, properties[1], _productEdmType, _readContext)).Verifiable(); + + // Act + deserializer.Object.ApplyNestedPropertyInfos(42, resourceWrapper, _productEdmType, _readContext); + + // Assert + deserializer.Verify(); + } + [Fact] public async Task ReadFromStreamAsync() { diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataResourceSerializerTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataResourceSerializerTests.cs index 524e57028..7e7dc6479 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataResourceSerializerTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataResourceSerializerTests.cs @@ -786,6 +786,26 @@ public void CreateResource_Calls_CreateStructuralProperty_ForEachSelectedStructu Assert.Equal(properties, entry.Properties); } + [Fact] + public void CreateResource_Calls_AppendResourceInstanceAnnotations() + { + // Arrange + SelectExpandNode selectExpandNode = new SelectExpandNode(); + + Mock serializer = new Mock(_serializerProvider); + serializer.CallBase = true; + + serializer + .Setup(s => s.AppendResourceInstanceAnnotations(It.IsAny(), selectExpandNode, _entityContext)) + .Verifiable(); + + // Act + serializer.Object.CreateResource(selectExpandNode, _entityContext); + + // Assert + serializer.Verify(); + } + [Fact] public void CreateResource_SetsETagToNull_IfRequestIsNull() { @@ -1314,6 +1334,34 @@ public void CreateResource_Throws_IfNullDynamicPropertyUsesExistingName_ForOpenT "Name", partialMatch: true); } + + [Fact] + public void CreateODataResourceValue_ThrowsArgumentNull_ForInputParameters() + { + // Arrange + IODataSerializerProvider provider = new Mock().Object; + ODataResourceSerializer serializer = new ODataResourceSerializer(provider); + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull( + () => serializer.CreateODataResourceValue(new object(), null, writeContext: null), + "writeContext"); + } + + [Fact] + public void CreateODataResourceValue_ReturnsODataNullValue_ForNUll() + { + // Arrange + Mock serializerProvider = new Mock(); + ODataResourceSerializer serializer = new ODataResourceSerializer(serializerProvider.Object); + + // Act + object result = serializer.CreateODataResourceValue(null, null, null); + + // Assert + Assert.Null(result); + } + [Fact] public void CreateUntypedPropertyValue_ThrowsArgumentNull_StructuralProperty() { @@ -1488,6 +1536,108 @@ public void CreateStructuralProperty_Calls_CreateODataValueOnInnerSerializer() Assert.Equal(propertyValue, createdProperty.Value); } + public class CustomerWithInstanceAnnotation + { + public int Id { get; set; } + + public IODataInstanceAnnotationContainer Container { get; set; } + } + + [Fact] + public void CreateResource_Adds_InstanceAnnotationsForResource() + { + // Arrange + var builder = new ODataConventionModelBuilder(); + builder.EntitySet("Customers"); + IEdmModel model = builder.GetEdmModel(); + IEdmEntitySet customerSet = model.FindDeclaredEntitySet("Customers"); + var customerType = customerSet.EntityType(); + var idProperty = customerType.FindProperty("Id") as IEdmStructuralProperty; + + ODataSerializerContext writeContext = new ODataSerializerContext() + { + NavigationSource = customerSet, + Model = model + }; + + Mock serializerProvider = new Mock(MockBehavior.Strict); + var entity = new CustomerWithInstanceAnnotation + { + Id = 42, + Container = new ODataInstanceAnnotationContainer() + }; + entity.Container.AddResourceAnnotation("NS.TestAnnotation", 88); + + Mock innerSerializer = new Mock(ODataPayloadKind.Property); + ODataValue propertyValue = new Mock().Object; + serializerProvider.Setup(s => s.GetEdmTypeSerializer(It.IsAny())).Returns(innerSerializer.Object); + innerSerializer.Setup(s => s.CreateODataValue(88, It.IsAny(), writeContext)).Returns(propertyValue).Verifiable(); + + var serializer = new ODataResourceSerializer(serializerProvider.Object); + ResourceContext entityContext = new ResourceContext(writeContext, customerType.ToEdmTypeReference(true).AsEntity(), entity); + SelectExpandNode selectExpandNode = new SelectExpandNode(null, customerType, model); + + // Act + ODataResource resource = serializer.CreateResource(selectExpandNode, entityContext); + + // Assert + innerSerializer.Verify(); + + ODataInstanceAnnotation annotation = Assert.Single(resource.InstanceAnnotations); + Assert.Equal("NS.TestAnnotation", annotation.Name); + Assert.Same(propertyValue, annotation.Value); + } + + [Fact] + public void CreateStructuralProperty_Adds_InstanceAnnotationsForProperty() + { + // Arrange + var builder = new ODataConventionModelBuilder(); + builder.EntitySet("Customers"); + IEdmModel model = builder.GetEdmModel(); + IEdmEntitySet customerSet = model.FindDeclaredEntitySet("Customers"); + var customerType = customerSet.EntityType().ToEdmTypeReference(true).AsEntity(); + var idProperty = customerType.FindProperty("Id") as IEdmStructuralProperty; + + ODataSerializerContext writeContext = new ODataSerializerContext() + { + NavigationSource = customerSet, + Model = model + }; + + Mock serializerProvider = new Mock(MockBehavior.Strict); + var entity = new CustomerWithInstanceAnnotation + { + Id = 42, + Container = new ODataInstanceAnnotationContainer() + }; + entity.Container.AddPropertyAnnotation("Id", "NS.TestAnnotation", 23); + + Mock innerSerializer = new Mock(ODataPayloadKind.Property); + + ODataValue propertyValue = new Mock().Object; + + serializerProvider.Setup(s => s.GetEdmTypeSerializer(It.IsAny())).Returns(innerSerializer.Object); + innerSerializer.Setup(s => s.CreateODataValue(42, idProperty.Type, writeContext)).Returns(propertyValue).Verifiable(); + innerSerializer.Setup(s => s.CreateODataValue(23, It.IsAny(), writeContext)).Returns(propertyValue).Verifiable(); + + var serializer = new ODataResourceSerializer(serializerProvider.Object); + + ResourceContext entityContext = new ResourceContext(writeContext, customerType, entity); + + // Act + ODataProperty createdProperty = serializer.CreateStructuralProperty(idProperty, entityContext); + + // Assert + innerSerializer.Verify(); + Assert.Equal("Id", createdProperty.Name); + Assert.Equal(propertyValue, createdProperty.Value); + + ODataInstanceAnnotation annotation = Assert.Single(createdProperty.InstanceAnnotations); + Assert.Equal("NS.TestAnnotation", annotation.Name); + Assert.Same(propertyValue, annotation.Value); + } + private bool Verify(ResourceContext instanceContext, object instance, ODataSerializerContext writeContext) { Assert.Same(instance, (instanceContext.EdmObject as TypedEdmEntityObject).Instance); @@ -1498,6 +1648,25 @@ private bool Verify(ResourceContext instanceContext, object instance, ODataSeria return true; } + [Fact] + public void CreateODataValue_Calls_CreateODataResourceValue() + { + // Arrange + object value = new object(); + IEdmTypeReference typeRefence = EdmUntypedStructuredTypeReference.NullableTypeReference; + ODataSerializerContext writeContext = new ODataSerializerContext(); + + Mock serializerProvider = new Mock(); + Mock serializer = new Mock(serializerProvider.Object); + serializer.Setup(s => s.CreateODataResourceValue(value, It.IsAny(), writeContext)).Verifiable(); + + // Act + object createdProperty = serializer.Object.CreateODataValue(value, typeRefence, writeContext); + + // Assert + serializer.Verify(); + } + [Fact] public void CreateUntypedNestedResourceInfo_ThrowsArgumentNull_StructuralProperty() { diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataResourceSetSerializerTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataResourceSetSerializerTests.cs index 206971841..384a3c581 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataResourceSetSerializerTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataResourceSetSerializerTests.cs @@ -164,18 +164,21 @@ public async Task WriteObjectAsync_CanWriteTopLevelResourceSetContainsNullComple "]}", result); } - [Fact] - public async Task WriteObjectAsync_CanWrite_TopLevelResourceSet_ContainsEmptyCollectionOfDynamicComplexElement() + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task WriteObjectAsync_CanWrite_TopLevelResourceSet_ContainsEmptyCollectionOfDynamicComplexElement(bool containsAnnotation) { // Arrange IODataSerializerProvider serializerProvider = GetServiceProvider().GetService(); ODataResourceSetSerializer serializer = new ODataResourceSetSerializer(serializerProvider); MemoryStream stream = new MemoryStream(); IODataResponseMessageAsync message = new ODataMessageWrapper(stream); + message.PreferenceAppliedHeader().AnnotationFilter = "*"; ODataMessageWriterSettings settings = new ODataMessageWriterSettings { - ODataUri = new ODataUri { ServiceRoot = new Uri("http://any/"), } + ODataUri = new ODataUri { ServiceRoot = new Uri("http://any/"), }, }; settings.SetContentType(ODataFormat.Json); @@ -198,6 +201,13 @@ public async Task WriteObjectAsync_CanWrite_TopLevelResourceSet_ContainsEmptyCol builder.ComplexType(); IEdmModel model = builder.GetEdmModel(); ODataSerializerContext writeContext = new ODataSerializerContext { Model = model }; + if (containsAnnotation) + { + writeContext.InstanceAnnotations = new Dictionary + { + { "NS.TestAnnotation", "Xiao" } + }; + } // Act await serializer.WriteObjectAsync(addresses, typeof(IList), writer, writeContext); @@ -205,18 +215,38 @@ public async Task WriteObjectAsync_CanWrite_TopLevelResourceSet_ContainsEmptyCol JObject result = JObject.Parse(await new StreamReader(stream).ReadToEndAsync());//.ToString(); // Assert - Assert.Equal(JObject.Parse(@"{ - ""@odata.context"": ""http://any/$metadata#Collection(Microsoft.AspNetCore.OData.Tests.Formatter.Models.SimpleOpenAddress)"", - ""value"": [ - { - ""Street"": ""Microsoft Rd"", - ""City"": ""Redmond"", - ""StringProp"": ""abc"", - ""Locations@odata.type"": ""#Collection(Microsoft.AspNetCore.OData.Tests.Formatter.Models.SimpleOpenAddress)"", - ""Locations"": [] - } - ] - }"), result); + if (containsAnnotation) + { + Assert.Equal(JObject.Parse(@"{ + ""@odata.context"": ""http://any/$metadata#Collection(Microsoft.AspNetCore.OData.Tests.Formatter.Models.SimpleOpenAddress)"", + ""@NS.TestAnnotation"": ""Xiao"", + ""value"": [ + { + ""Street"": ""Microsoft Rd"", + ""City"": ""Redmond"", + ""StringProp"": ""abc"", + ""Locations@odata.type"": ""#Collection(Microsoft.AspNetCore.OData.Tests.Formatter.Models.SimpleOpenAddress)"", + ""Locations"": [] + } + ] + }"), result); + } + else + { + + Assert.Equal(JObject.Parse(@"{ + ""@odata.context"": ""http://any/$metadata#Collection(Microsoft.AspNetCore.OData.Tests.Formatter.Models.SimpleOpenAddress)"", + ""value"": [ + { + ""Street"": ""Microsoft Rd"", + ""City"": ""Redmond"", + ""StringProp"": ""abc"", + ""Locations@odata.type"": ""#Collection(Microsoft.AspNetCore.OData.Tests.Formatter.Models.SimpleOpenAddress)"", + ""Locations"": [] + } + ] + }"), result); + } } [Fact] diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataSerializerHelperTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataSerializerHelperTests.cs new file mode 100644 index 000000000..140c822cc --- /dev/null +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataSerializerHelperTests.cs @@ -0,0 +1,104 @@ +//----------------------------------------------------------------------------- +// +// 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.Collections.ObjectModel; +using Microsoft.AspNetCore.OData.Edm; +using Microsoft.AspNetCore.OData.Formatter.Serialization; +using Microsoft.AspNetCore.OData.Formatter.Value; +using Microsoft.OData; +using Microsoft.OData.Edm; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.OData.Tests.Formatter.Serialization +{ + public class ODataSerializerHelperTests + { + [Fact] + public void AppendInstanceAnnotations_AddNullAnnotationsIntoDestination() + { + // Arrange + IDictionary annotations = new Dictionary + { + { "NS.Test1", null }, + { "NS.Test2", new ODataNullValue() }, + }; + + ICollection destination = new Collection(); + ODataSerializerContext context = new ODataSerializerContext(); + IODataSerializerProvider provider = new Mock().Object; + + // Act + ODataSerializerHelper.AppendInstanceAnnotations(annotations, destination, context, provider); + + // Assert + Assert.Equal(2, destination.Count); + Assert.Collection(destination, + e => + { + Assert.Equal("NS.Test1", e.Name); + Assert.Same(ODataNullValueExtensions.NullValue, e.Value); + }, + e => + { + Assert.Equal("NS.Test2", e.Name); + Assert.Same(ODataNullValueExtensions.NullValue, e.Value); + }); + } + + [Fact] + public void AppendInstanceAnnotations_AddInstanceAnnotationsIntoDestination() + { + // Arrange + Mock serializer = new Mock(); + + Mock serializerProvider = new Mock(); + serializerProvider.Setup(c => c.GetEdmTypeSerializer(It.IsAny())).Returns(serializer.Object); + + Mock type1 = new Mock(); + Mock value1 = new Mock(); + value1.Setup(c => c.GetEdmType()).Returns(type1.Object); + + Mock type2 = new Mock(); + Mock value2 = new Mock(); + value2.Setup(c => c.GetEdmType()).Returns(type2.Object); + + IDictionary annotations = new Dictionary + { + { "NS.Test1", value1.Object }, + { "NS.Test2", value2.Object }, + }; + + ODataSerializerContext context = new ODataSerializerContext(); + ODataPrimitiveValue oValue1 = new ODataPrimitiveValue(42); + ODataPrimitiveValue oValue2 = new ODataPrimitiveValue(43); + + serializer.Setup(s => s.CreateODataValue(value1.Object, type1.Object, context)).Returns(oValue1); + serializer.Setup(s => s.CreateODataValue(value2.Object, type2.Object, context)).Returns(oValue2); + + ICollection destination = new Collection(); + + // Act + ODataSerializerHelper.AppendInstanceAnnotations(annotations, destination, context, serializerProvider.Object); + + // Assert + Assert.Equal(2, destination.Count); + Assert.Collection(destination, + e => + { + Assert.Equal("NS.Test1", e.Name); + Assert.Same(oValue1, e.Value); + }, + e => + { + Assert.Equal("NS.Test2", e.Name); + Assert.Same(oValue2, e.Value); + }); + } + } +} diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataSerializerProviderTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataSerializerProviderTests.cs index 5b33a02a7..ebedcc007 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataSerializerProviderTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataSerializerProviderTests.cs @@ -10,6 +10,7 @@ using System.IO; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.OData.Deltas; +using Microsoft.AspNetCore.OData.Edm; using Microsoft.AspNetCore.OData.Extensions; using Microsoft.AspNetCore.OData.Formatter.Serialization; using Microsoft.AspNetCore.OData.Results; @@ -415,6 +416,34 @@ public void GetEdmTypeSerializer_Caches_CreateEdmTypeSerializerOutput() Assert.Same(serializer2, serializer1); } + [Fact] + public void GetEdmTypeSerializer_ReturnsCorrectSerializer_ForCollectionOfEdmUntypedStructural() + { + // Arrange + IEdmTypeReference edmType = EdmUntypedHelpers.NullableUntypedCollectionReference; + + // Act + IODataSerializer serializer = _serializerProvider.GetEdmTypeSerializer(edmType); + + // Assert + ODataResourceSetSerializer setSerializer = Assert.IsType(serializer); + Assert.Equal(ODataPayloadKind.ResourceSet, setSerializer.ODataPayloadKind); + } + + [Fact] + public void GetEdmTypeSerializer_ReturnsCorrectSerializer_ForCollectionOfEdmUntypedPrimitive() + { + // Arrange + IEdmTypeReference edmType = EdmUntypedHelpers.NullablePrimitiveUntypedCollectionReference; + + // Act + IODataSerializer serializer = _serializerProvider.GetEdmTypeSerializer(edmType); + + // Assert + ODataCollectionSerializer collectionSerializer = Assert.IsType(serializer); + Assert.Equal(ODataPayloadKind.Collection, collectionSerializer.ODataPayloadKind); + } + private static IServiceProvider GetServiceProvider() { IServiceCollection services = new ServiceCollection(); diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Wrapper/ODataReaderExtensionsTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Wrapper/ODataReaderExtensionsTests.cs index 92255f24a..860287a80 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Wrapper/ODataReaderExtensionsTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Wrapper/ODataReaderExtensionsTests.cs @@ -116,6 +116,51 @@ public async Task ReadResourceWorksAsExpected() Assert.Equal(new[] { "Location", "Order", "Orders" }, resource.NestedResourceInfos.Select(n => n.NestedResourceInfo.Name)); } + [Fact] + public async Task ReadResourceWithPropertyWithoutValueButWithInstanceAnnotationsWorksAsExpected() + { + // Arrange + // Property 'Name' without value but with instance annotations + const string payload = + "{" + + "\"@odata.context\":\"http://localhost/$metadata#Customers/$entity\"," + + "\"CustomerID\": 17," + + "\"Name@Custom.PrimitiveAnnotation\":123," + + "\"Name@Custom.BooleanAnnotation\":true," + + "\"Location\": { \"Street\":\"154TH AVE\"}" + + "}"; + + IEdmEntitySet customers = Model.EntityContainer.FindEntitySet("Customers"); + Assert.NotNull(customers); // Guard + + // Act + Func> func = mr => mr.CreateODataResourceReaderAsync(customers, customers.EntityType()); + ODataItemWrapper item = await ReadPayloadAsync(payload, Model, func, ODataVersion.V4, false, "*"); + + // Assert + Assert.NotNull(item); + ODataResourceWrapper resource = Assert.IsType(item); + Assert.NotNull(resource.Resource); + ODataProperty customerIdProp = Assert.Single(resource.Resource.Properties); + Assert.Equal("CustomerID", customerIdProp.Name); + Assert.Equal(17, customerIdProp.Value); + + ODataPropertyInfo nameProp = Assert.Single(resource.NestedPropertyInfos); + Assert.Equal("Name", nameProp.Name); + Assert.Equal(2, nameProp.InstanceAnnotations.Count); + + ODataInstanceAnnotation primitiveAnnotation = nameProp.InstanceAnnotations.First(i => i.Name == "Custom.PrimitiveAnnotation"); + ODataPrimitiveValue primitiveValue = Assert.IsType(primitiveAnnotation.Value); + Assert.Equal(123, primitiveValue.Value); + + ODataInstanceAnnotation booleanAnnotation = nameProp.InstanceAnnotations.First(i => i.Name == "Custom.BooleanAnnotation"); + ODataPrimitiveValue booleanValue = Assert.IsType(booleanAnnotation.Value); + Assert.True((bool)booleanValue.Value); + + ODataNestedResourceInfoWrapper nestedInfoWrapper = Assert.Single(resource.NestedResourceInfos); + Assert.Equal("Location", nestedInfoWrapper.NestedResourceInfo.Name); + } + [Fact] public async Task ReadResourceSetWorksAsExpected() { @@ -575,7 +620,8 @@ public async Task ReadEntityReferenceLinksSetWorksAsExpected_V401() private async Task ReadPayloadAsync(string payload, IEdmModel edmModel, Func> createReader, ODataVersion version = ODataVersion.V4, - bool readUntypedAsString = false) + bool readUntypedAsString = false, + string annotationFilter = null) { var message = new InMemoryMessage() { @@ -591,6 +637,11 @@ private async Task ReadPayloadAsync(string payload, Version = version, }; + if (annotationFilter != null) + { + readerSettings.ShouldIncludeAnnotation = ODataUtils.CreateAnnotationFilter(annotationFilter); + } + using (var msgReader = new ODataMessageReader((IODataRequestMessageAsync)message, readerSettings, edmModel)) { ODataReader reader = await createReader(msgReader); diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Wrapper/ODataResourceWrapperTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Wrapper/ODataResourceWrapperTests.cs new file mode 100644 index 000000000..47608b8e2 --- /dev/null +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Wrapper/ODataResourceWrapperTests.cs @@ -0,0 +1,87 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +using Microsoft.AspNetCore.OData.Formatter.Wrapper; +using Microsoft.AspNetCore.OData.Tests.Commons; +using Microsoft.OData; +using Xunit; + +namespace Microsoft.AspNetCore.OData.Tests.Formatter.Wrapper +{ + public class ODataResourceWrapperTests + { + [Fact] + public void Ctor_ThrowsArgumentNull_ResourceValue() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new ODataResourceWrapper((ODataResourceValue)null), "resourceValue"); + } + + [Fact] + public void Ctor_SetsUsingODataResource_CorrectProperties() + { + // Arrange & Act & Assert + ODataResource resource = new ODataResource + { + TypeName = "NS.Namespace" + }; + + // Act + ODataResourceWrapper resourceWrapper = new ODataResourceWrapper(resource); + + // Assert + Assert.Same(resource, resourceWrapper.Resource); + Assert.Null(resourceWrapper.ResourceValue); + Assert.False(resourceWrapper.IsResourceValue); + Assert.False(resourceWrapper.IsDeletedResource); + Assert.Empty(resourceWrapper.NestedPropertyInfos); + Assert.Empty(resourceWrapper.NestedResourceInfos); + } + + [Fact] + public void Ctor_SetsUsingODataDeletedResource_CorrectProperties() + { + // Arrange & Act & Assert + ODataDeletedResource deletedResource = new ODataDeletedResource + { + TypeName = "NS.Namespace" + }; + + // Act + ODataResourceWrapper resourceWrapper = new ODataResourceWrapper(deletedResource); + + // Assert + Assert.Same(deletedResource, resourceWrapper.Resource); + Assert.Null(resourceWrapper.ResourceValue); + Assert.False(resourceWrapper.IsResourceValue); + Assert.True(resourceWrapper.IsDeletedResource); + Assert.Empty(resourceWrapper.NestedPropertyInfos); + Assert.Empty(resourceWrapper.NestedResourceInfos); + } + + [Fact] + public void Ctor_SetsUsingODataResourceValue_CorrectProperties() + { + // Arrange & Act & Assert + ODataResourceValue resourceValue = new ODataResourceValue + { + TypeName = "NS.Namespace" + }; + + // Act + ODataResourceWrapper resourceWrapper = new ODataResourceWrapper(resourceValue); + + // Assert + Assert.Null(resourceWrapper.Resource); + Assert.Same(resourceValue, resourceWrapper.ResourceValue); + Assert.True(resourceWrapper.IsResourceValue); + Assert.False(resourceWrapper.IsDeletedResource); + Assert.Empty(resourceWrapper.NestedPropertyInfos); + Assert.Empty(resourceWrapper.NestedResourceInfos); + } + } +} From 9023dca3afe4417ee4e56cb98421b03b97126c45 Mon Sep 17 00:00:00 2001 From: Sam Xu Date: Mon, 10 Jun 2024 16:15:03 -0700 Subject: [PATCH 2/3] Resolve the failing public api test cases --- ...rosoft.AspNetCore.OData.PublicApi.Net6.bsl | 26 +++++++++++++++++++ ...t.AspNetCore.OData.PublicApi.NetCore31.bsl | 26 +++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.Net6.bsl b/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.Net6.bsl index 4cc6d0c27..91f72adc7 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.Net6.bsl +++ b/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.Net6.bsl @@ -589,6 +589,11 @@ public sealed class Microsoft.AspNetCore.OData.Edm.EdmModelAnnotationExtensions ] public static System.Reflection.PropertyInfo GetDynamicPropertyDictionary (Microsoft.OData.Edm.IEdmModel edmModel, Microsoft.OData.Edm.IEdmStructuredType edmType) + [ + ExtensionAttribute(), + ] + public static System.Reflection.PropertyInfo GetInstanceAnnotationsContainer (Microsoft.OData.Edm.IEdmModel edmModel, Microsoft.OData.Edm.IEdmStructuredType edmType) + [ ExtensionAttribute(), ] @@ -849,6 +854,11 @@ public sealed class Microsoft.AspNetCore.OData.Extensions.HttpRequestExtensions ] public static Microsoft.AspNetCore.OData.Abstracts.IETagHandler GetETagHandler (Microsoft.AspNetCore.Http.HttpRequest request) + [ + ExtensionAttribute(), + ] + public static System.Collections.Generic.IDictionary`2[[System.String],[System.Object]] GetInstanceAnnotations (Microsoft.AspNetCore.Http.HttpRequest request) + [ ExtensionAttribute(), ] @@ -908,6 +918,11 @@ public sealed class Microsoft.AspNetCore.OData.Extensions.HttpRequestExtensions ExtensionAttribute(), ] public static Microsoft.AspNetCore.OData.ODataOptions ODataOptions (Microsoft.AspNetCore.Http.HttpRequest request) + + [ + ExtensionAttribute(), + ] + public static Microsoft.AspNetCore.Http.HttpRequest SetInstanceAnnotations (Microsoft.AspNetCore.Http.HttpRequest request, System.Collections.Generic.IDictionary`2[[System.String],[System.Object]] instanceAnnotations) } [ @@ -2018,6 +2033,9 @@ public class Microsoft.AspNetCore.OData.Formatter.Deserialization.ODataResourceD public virtual void ApplyDeletedResource (object resource, Microsoft.AspNetCore.OData.Formatter.Wrapper.ODataResourceWrapper resourceWrapper, Microsoft.AspNetCore.OData.Formatter.Deserialization.ODataDeserializerContext readContext) public virtual void ApplyNestedProperties (object resource, Microsoft.AspNetCore.OData.Formatter.Wrapper.ODataResourceWrapper resourceWrapper, Microsoft.OData.Edm.IEdmStructuredTypeReference structuredType, Microsoft.AspNetCore.OData.Formatter.Deserialization.ODataDeserializerContext readContext) public virtual void ApplyNestedProperty (object resource, Microsoft.AspNetCore.OData.Formatter.Wrapper.ODataNestedResourceInfoWrapper resourceInfoWrapper, Microsoft.OData.Edm.IEdmStructuredTypeReference structuredType, Microsoft.AspNetCore.OData.Formatter.Deserialization.ODataDeserializerContext readContext) + public virtual void ApplyNestedPropertyInfos (object resource, Microsoft.AspNetCore.OData.Formatter.Wrapper.ODataResourceWrapper resourceWrapper, Microsoft.OData.Edm.IEdmStructuredTypeReference structuredType, Microsoft.AspNetCore.OData.Formatter.Deserialization.ODataDeserializerContext readContext) + public virtual void ApplyPropertyInstanceAnnotations (object resource, Microsoft.OData.ODataPropertyInfo structuralProperty, Microsoft.OData.Edm.IEdmStructuredTypeReference structuredType, Microsoft.AspNetCore.OData.Formatter.Deserialization.ODataDeserializerContext readContext) + public virtual void ApplyResourceInstanceAnnotations (object resource, Microsoft.AspNetCore.OData.Formatter.Wrapper.ODataResourceWrapper resourceWrapper, Microsoft.OData.Edm.IEdmStructuredTypeReference structuredType, Microsoft.AspNetCore.OData.Formatter.Deserialization.ODataDeserializerContext readContext) public virtual void ApplyStructuralProperties (object resource, Microsoft.AspNetCore.OData.Formatter.Wrapper.ODataResourceWrapper resourceWrapper, Microsoft.OData.Edm.IEdmStructuredTypeReference structuredType, Microsoft.AspNetCore.OData.Formatter.Deserialization.ODataDeserializerContext readContext) public virtual void ApplyStructuralProperty (object resource, Microsoft.OData.ODataProperty structuralProperty, Microsoft.OData.Edm.IEdmStructuredTypeReference structuredType, Microsoft.AspNetCore.OData.Formatter.Deserialization.ODataDeserializerContext readContext) public virtual object CreateResourceInstance (Microsoft.OData.Edm.IEdmStructuredTypeReference structuredType, Microsoft.AspNetCore.OData.Formatter.Deserialization.ODataDeserializerContext readContext) @@ -2245,6 +2263,7 @@ public class Microsoft.AspNetCore.OData.Formatter.Serialization.ODataMetadataSer public class Microsoft.AspNetCore.OData.Formatter.Serialization.ODataPrimitiveSerializer : Microsoft.AspNetCore.OData.Formatter.Serialization.ODataEdmTypeSerializer, IODataEdmTypeSerializer, IODataSerializer { public ODataPrimitiveSerializer () + public ODataPrimitiveSerializer (Microsoft.AspNetCore.OData.Formatter.Serialization.IODataSerializerProvider serializerProvider) public virtual Microsoft.OData.ODataPrimitiveValue CreateODataPrimitiveValue (object graph, Microsoft.OData.Edm.IEdmPrimitiveTypeReference primitiveType, Microsoft.AspNetCore.OData.Formatter.Serialization.ODataSerializerContext writeContext) public virtual Microsoft.OData.ODataValue CreateODataValue (object graph, Microsoft.OData.Edm.IEdmTypeReference expectedType, Microsoft.AspNetCore.OData.Formatter.Serialization.ODataSerializerContext writeContext) @@ -2267,6 +2286,7 @@ public class Microsoft.AspNetCore.OData.Formatter.Serialization.ODataResourceSer public ODataResourceSerializer (Microsoft.AspNetCore.OData.Formatter.Serialization.IODataSerializerProvider serializerProvider) public virtual void AppendDynamicProperties (Microsoft.OData.ODataResource resource, Microsoft.AspNetCore.OData.Formatter.Serialization.SelectExpandNode selectExpandNode, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) + public virtual void AppendResourceInstanceAnnotations (Microsoft.OData.ODataResourceBase resource, Microsoft.AspNetCore.OData.Formatter.Serialization.SelectExpandNode selectExpandNode, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) public virtual Microsoft.OData.ODataNestedResourceInfo CreateComplexNestedResourceInfo (Microsoft.OData.Edm.IEdmStructuralProperty complexProperty, Microsoft.OData.UriParser.PathSelectItem pathSelectItem, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) public virtual Microsoft.OData.ODataProperty CreateComputedProperty (string propertyName, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) public virtual Microsoft.OData.ODataNestedResourceInfo CreateDynamicComplexNestedResourceInfo (string propertyName, object propertyValue, Microsoft.OData.Edm.IEdmTypeReference edmType, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) @@ -2274,6 +2294,8 @@ public class Microsoft.AspNetCore.OData.Formatter.Serialization.ODataResourceSer public virtual Microsoft.OData.ODataNestedResourceInfo CreateNavigationLink (Microsoft.OData.Edm.IEdmNavigationProperty navigationProperty, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) public virtual Microsoft.OData.ODataAction CreateODataAction (Microsoft.OData.Edm.IEdmAction action, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) public virtual Microsoft.OData.ODataFunction CreateODataFunction (Microsoft.OData.Edm.IEdmFunction function, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) + public virtual Microsoft.OData.ODataResourceValue CreateODataResourceValue (object value, Microsoft.OData.Edm.IEdmStructuredTypeReference structuredType, Microsoft.AspNetCore.OData.Formatter.Serialization.ODataSerializerContext writeContext) + public virtual Microsoft.OData.ODataValue CreateODataValue (object value, Microsoft.OData.Edm.IEdmTypeReference expectedType, Microsoft.AspNetCore.OData.Formatter.Serialization.ODataSerializerContext writeContext) public virtual Microsoft.OData.ODataResource CreateResource (Microsoft.AspNetCore.OData.Formatter.Serialization.SelectExpandNode selectExpandNode, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) public virtual Microsoft.AspNetCore.OData.Formatter.Serialization.SelectExpandNode CreateSelectExpandNode (Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) public virtual Microsoft.OData.ODataStreamPropertyInfo CreateStreamProperty (Microsoft.OData.Edm.IEdmStructuralProperty structuralProperty, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) @@ -2714,10 +2736,14 @@ public sealed class Microsoft.AspNetCore.OData.Formatter.Wrapper.ODataResourceSe public sealed class Microsoft.AspNetCore.OData.Formatter.Wrapper.ODataResourceWrapper : Microsoft.AspNetCore.OData.Formatter.Wrapper.ODataItemWrapper { public ODataResourceWrapper (Microsoft.OData.ODataResourceBase resource) + public ODataResourceWrapper (Microsoft.OData.ODataResourceValue resourceValue) bool IsDeletedResource { public get; } + bool IsResourceValue { public get; } + System.Collections.Generic.IList`1[[Microsoft.OData.ODataPropertyInfo]] NestedPropertyInfos { public get; } System.Collections.Generic.IList`1[[Microsoft.AspNetCore.OData.Formatter.Wrapper.ODataNestedResourceInfoWrapper]] NestedResourceInfos { public get; } Microsoft.OData.ODataResourceBase Resource { public get; } + Microsoft.OData.ODataResourceValue ResourceValue { public get; } } public interface Microsoft.AspNetCore.OData.Query.Container.IPropertyMapper { diff --git a/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.NetCore31.bsl b/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.NetCore31.bsl index 4cc6d0c27..91f72adc7 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.NetCore31.bsl +++ b/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.NetCore31.bsl @@ -589,6 +589,11 @@ public sealed class Microsoft.AspNetCore.OData.Edm.EdmModelAnnotationExtensions ] public static System.Reflection.PropertyInfo GetDynamicPropertyDictionary (Microsoft.OData.Edm.IEdmModel edmModel, Microsoft.OData.Edm.IEdmStructuredType edmType) + [ + ExtensionAttribute(), + ] + public static System.Reflection.PropertyInfo GetInstanceAnnotationsContainer (Microsoft.OData.Edm.IEdmModel edmModel, Microsoft.OData.Edm.IEdmStructuredType edmType) + [ ExtensionAttribute(), ] @@ -849,6 +854,11 @@ public sealed class Microsoft.AspNetCore.OData.Extensions.HttpRequestExtensions ] public static Microsoft.AspNetCore.OData.Abstracts.IETagHandler GetETagHandler (Microsoft.AspNetCore.Http.HttpRequest request) + [ + ExtensionAttribute(), + ] + public static System.Collections.Generic.IDictionary`2[[System.String],[System.Object]] GetInstanceAnnotations (Microsoft.AspNetCore.Http.HttpRequest request) + [ ExtensionAttribute(), ] @@ -908,6 +918,11 @@ public sealed class Microsoft.AspNetCore.OData.Extensions.HttpRequestExtensions ExtensionAttribute(), ] public static Microsoft.AspNetCore.OData.ODataOptions ODataOptions (Microsoft.AspNetCore.Http.HttpRequest request) + + [ + ExtensionAttribute(), + ] + public static Microsoft.AspNetCore.Http.HttpRequest SetInstanceAnnotations (Microsoft.AspNetCore.Http.HttpRequest request, System.Collections.Generic.IDictionary`2[[System.String],[System.Object]] instanceAnnotations) } [ @@ -2018,6 +2033,9 @@ public class Microsoft.AspNetCore.OData.Formatter.Deserialization.ODataResourceD public virtual void ApplyDeletedResource (object resource, Microsoft.AspNetCore.OData.Formatter.Wrapper.ODataResourceWrapper resourceWrapper, Microsoft.AspNetCore.OData.Formatter.Deserialization.ODataDeserializerContext readContext) public virtual void ApplyNestedProperties (object resource, Microsoft.AspNetCore.OData.Formatter.Wrapper.ODataResourceWrapper resourceWrapper, Microsoft.OData.Edm.IEdmStructuredTypeReference structuredType, Microsoft.AspNetCore.OData.Formatter.Deserialization.ODataDeserializerContext readContext) public virtual void ApplyNestedProperty (object resource, Microsoft.AspNetCore.OData.Formatter.Wrapper.ODataNestedResourceInfoWrapper resourceInfoWrapper, Microsoft.OData.Edm.IEdmStructuredTypeReference structuredType, Microsoft.AspNetCore.OData.Formatter.Deserialization.ODataDeserializerContext readContext) + public virtual void ApplyNestedPropertyInfos (object resource, Microsoft.AspNetCore.OData.Formatter.Wrapper.ODataResourceWrapper resourceWrapper, Microsoft.OData.Edm.IEdmStructuredTypeReference structuredType, Microsoft.AspNetCore.OData.Formatter.Deserialization.ODataDeserializerContext readContext) + public virtual void ApplyPropertyInstanceAnnotations (object resource, Microsoft.OData.ODataPropertyInfo structuralProperty, Microsoft.OData.Edm.IEdmStructuredTypeReference structuredType, Microsoft.AspNetCore.OData.Formatter.Deserialization.ODataDeserializerContext readContext) + public virtual void ApplyResourceInstanceAnnotations (object resource, Microsoft.AspNetCore.OData.Formatter.Wrapper.ODataResourceWrapper resourceWrapper, Microsoft.OData.Edm.IEdmStructuredTypeReference structuredType, Microsoft.AspNetCore.OData.Formatter.Deserialization.ODataDeserializerContext readContext) public virtual void ApplyStructuralProperties (object resource, Microsoft.AspNetCore.OData.Formatter.Wrapper.ODataResourceWrapper resourceWrapper, Microsoft.OData.Edm.IEdmStructuredTypeReference structuredType, Microsoft.AspNetCore.OData.Formatter.Deserialization.ODataDeserializerContext readContext) public virtual void ApplyStructuralProperty (object resource, Microsoft.OData.ODataProperty structuralProperty, Microsoft.OData.Edm.IEdmStructuredTypeReference structuredType, Microsoft.AspNetCore.OData.Formatter.Deserialization.ODataDeserializerContext readContext) public virtual object CreateResourceInstance (Microsoft.OData.Edm.IEdmStructuredTypeReference structuredType, Microsoft.AspNetCore.OData.Formatter.Deserialization.ODataDeserializerContext readContext) @@ -2245,6 +2263,7 @@ public class Microsoft.AspNetCore.OData.Formatter.Serialization.ODataMetadataSer public class Microsoft.AspNetCore.OData.Formatter.Serialization.ODataPrimitiveSerializer : Microsoft.AspNetCore.OData.Formatter.Serialization.ODataEdmTypeSerializer, IODataEdmTypeSerializer, IODataSerializer { public ODataPrimitiveSerializer () + public ODataPrimitiveSerializer (Microsoft.AspNetCore.OData.Formatter.Serialization.IODataSerializerProvider serializerProvider) public virtual Microsoft.OData.ODataPrimitiveValue CreateODataPrimitiveValue (object graph, Microsoft.OData.Edm.IEdmPrimitiveTypeReference primitiveType, Microsoft.AspNetCore.OData.Formatter.Serialization.ODataSerializerContext writeContext) public virtual Microsoft.OData.ODataValue CreateODataValue (object graph, Microsoft.OData.Edm.IEdmTypeReference expectedType, Microsoft.AspNetCore.OData.Formatter.Serialization.ODataSerializerContext writeContext) @@ -2267,6 +2286,7 @@ public class Microsoft.AspNetCore.OData.Formatter.Serialization.ODataResourceSer public ODataResourceSerializer (Microsoft.AspNetCore.OData.Formatter.Serialization.IODataSerializerProvider serializerProvider) public virtual void AppendDynamicProperties (Microsoft.OData.ODataResource resource, Microsoft.AspNetCore.OData.Formatter.Serialization.SelectExpandNode selectExpandNode, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) + public virtual void AppendResourceInstanceAnnotations (Microsoft.OData.ODataResourceBase resource, Microsoft.AspNetCore.OData.Formatter.Serialization.SelectExpandNode selectExpandNode, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) public virtual Microsoft.OData.ODataNestedResourceInfo CreateComplexNestedResourceInfo (Microsoft.OData.Edm.IEdmStructuralProperty complexProperty, Microsoft.OData.UriParser.PathSelectItem pathSelectItem, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) public virtual Microsoft.OData.ODataProperty CreateComputedProperty (string propertyName, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) public virtual Microsoft.OData.ODataNestedResourceInfo CreateDynamicComplexNestedResourceInfo (string propertyName, object propertyValue, Microsoft.OData.Edm.IEdmTypeReference edmType, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) @@ -2274,6 +2294,8 @@ public class Microsoft.AspNetCore.OData.Formatter.Serialization.ODataResourceSer public virtual Microsoft.OData.ODataNestedResourceInfo CreateNavigationLink (Microsoft.OData.Edm.IEdmNavigationProperty navigationProperty, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) public virtual Microsoft.OData.ODataAction CreateODataAction (Microsoft.OData.Edm.IEdmAction action, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) public virtual Microsoft.OData.ODataFunction CreateODataFunction (Microsoft.OData.Edm.IEdmFunction function, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) + public virtual Microsoft.OData.ODataResourceValue CreateODataResourceValue (object value, Microsoft.OData.Edm.IEdmStructuredTypeReference structuredType, Microsoft.AspNetCore.OData.Formatter.Serialization.ODataSerializerContext writeContext) + public virtual Microsoft.OData.ODataValue CreateODataValue (object value, Microsoft.OData.Edm.IEdmTypeReference expectedType, Microsoft.AspNetCore.OData.Formatter.Serialization.ODataSerializerContext writeContext) public virtual Microsoft.OData.ODataResource CreateResource (Microsoft.AspNetCore.OData.Formatter.Serialization.SelectExpandNode selectExpandNode, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) public virtual Microsoft.AspNetCore.OData.Formatter.Serialization.SelectExpandNode CreateSelectExpandNode (Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) public virtual Microsoft.OData.ODataStreamPropertyInfo CreateStreamProperty (Microsoft.OData.Edm.IEdmStructuralProperty structuralProperty, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) @@ -2714,10 +2736,14 @@ public sealed class Microsoft.AspNetCore.OData.Formatter.Wrapper.ODataResourceSe public sealed class Microsoft.AspNetCore.OData.Formatter.Wrapper.ODataResourceWrapper : Microsoft.AspNetCore.OData.Formatter.Wrapper.ODataItemWrapper { public ODataResourceWrapper (Microsoft.OData.ODataResourceBase resource) + public ODataResourceWrapper (Microsoft.OData.ODataResourceValue resourceValue) bool IsDeletedResource { public get; } + bool IsResourceValue { public get; } + System.Collections.Generic.IList`1[[Microsoft.OData.ODataPropertyInfo]] NestedPropertyInfos { public get; } System.Collections.Generic.IList`1[[Microsoft.AspNetCore.OData.Formatter.Wrapper.ODataNestedResourceInfoWrapper]] NestedResourceInfos { public get; } Microsoft.OData.ODataResourceBase Resource { public get; } + Microsoft.OData.ODataResourceValue ResourceValue { public get; } } public interface Microsoft.AspNetCore.OData.Query.Container.IPropertyMapper { From de2520b53989f8fd61749ce173edff76be0a7769 Mon Sep 17 00:00:00 2001 From: Sam Xu Date: Tue, 11 Jun 2024 09:03:30 -0700 Subject: [PATCH 3/3] Fixes the failing test cases --- .../InstanceAnnotations/InstanceAnnotationsTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/InstanceAnnotations/InstanceAnnotationsTests.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/InstanceAnnotations/InstanceAnnotationsTests.cs index cb978253b..74fc17536 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/InstanceAnnotations/InstanceAnnotationsTests.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/InstanceAnnotations/InstanceAnnotationsTests.cs @@ -61,8 +61,8 @@ public async Task QueryEntitySetWithTopLevelInstanceAnnotation_Works() Assert.Equal("{\"@odata.context\":\"http://localhost/odata/$metadata#Customers(Name)\"," + "\"@NS.TestAnnotation\":1978," + "\"value\":[" + - "{\"@NS.TestAnnotation\":1978,\"Name\":\"Peter\"}," + - "{\"@NS.TestAnnotation\":1978,\"Name\":\"Sam\"}" + + "{\"Name\":\"Peter\"}," + + "{\"Name\":\"Sam\"}" + "]" + "}", payloadBody); }