From 3813ee7cc2c3b407f98684951c0617a0ea4811bc Mon Sep 17 00:00:00 2001 From: mikepizzo Date: Thu, 21 Nov 2024 15:18:46 -0800 Subject: [PATCH 1/9] Addition of DeltaFunctionsSample --- AspNetCoreOData.sln | 7 + .../RecoveryPreviewJobsController.cs | 131 ++++++++++++++++++ .../DeltaFunctionsSample.csproj | 12 ++ .../DeltaFunctions/Models/EdmModelBuilder.cs | 85 ++++++++++++ .../Models/recoveryChangeObject.cs | 16 +++ .../Models/recoveryPreviewJob.cs | 9 ++ sample/DeltaFunctions/Program.cs | 21 +++ .../Properties/launchSettings.json | 31 +++++ .../appsettings.Development.json | 8 ++ sample/DeltaFunctions/appsettings.json | 9 ++ 10 files changed, 329 insertions(+) create mode 100644 sample/DeltaFunctions/Controllers/RecoveryPreviewJobsController.cs create mode 100644 sample/DeltaFunctions/DeltaFunctionsSample.csproj create mode 100644 sample/DeltaFunctions/Models/EdmModelBuilder.cs create mode 100644 sample/DeltaFunctions/Models/recoveryChangeObject.cs create mode 100644 sample/DeltaFunctions/Models/recoveryPreviewJob.cs create mode 100644 sample/DeltaFunctions/Program.cs create mode 100644 sample/DeltaFunctions/Properties/launchSettings.json create mode 100644 sample/DeltaFunctions/appsettings.Development.json create mode 100644 sample/DeltaFunctions/appsettings.json diff --git a/AspNetCoreOData.sln b/AspNetCoreOData.sln index 06a9b8f0a..398eff15f 100644 --- a/AspNetCoreOData.sln +++ b/AspNetCoreOData.sln @@ -29,6 +29,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ODataAlternateKeySample", " EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BenchmarkServer", "sample\BenchmarkServer\BenchmarkServer.csproj", "{8346AD1B-00E3-462D-B6B1-9AA3C2FB2850}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DeltaFunctionsSample", "sample\DeltaFunctions\DeltaFunctionsSample.csproj", "{B4D2B1EE-14DA-47E2-AFAD-11087C8508D2}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -75,6 +77,10 @@ Global {8346AD1B-00E3-462D-B6B1-9AA3C2FB2850}.Debug|Any CPU.Build.0 = Debug|Any CPU {8346AD1B-00E3-462D-B6B1-9AA3C2FB2850}.Release|Any CPU.ActiveCfg = Release|Any CPU {8346AD1B-00E3-462D-B6B1-9AA3C2FB2850}.Release|Any CPU.Build.0 = Release|Any CPU + {B4D2B1EE-14DA-47E2-AFAD-11087C8508D2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B4D2B1EE-14DA-47E2-AFAD-11087C8508D2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B4D2B1EE-14DA-47E2-AFAD-11087C8508D2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B4D2B1EE-14DA-47E2-AFAD-11087C8508D2}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -90,6 +96,7 @@ Global {647EFCFA-55A7-4F0A-AD40-4B6EB1BFCFFA} = {B1F86961-6958-4617-ACA4-C231F95AE099} {7B153669-A42F-4511-8BDB-587B3B27B2F3} = {B1F86961-6958-4617-ACA4-C231F95AE099} {8346AD1B-00E3-462D-B6B1-9AA3C2FB2850} = {B1F86961-6958-4617-ACA4-C231F95AE099} + {B4D2B1EE-14DA-47E2-AFAD-11087C8508D2} = {B1F86961-6958-4617-ACA4-C231F95AE099} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {540C9752-AAC0-49EA-BA60-78490C90FF86} diff --git a/sample/DeltaFunctions/Controllers/RecoveryPreviewJobsController.cs b/sample/DeltaFunctions/Controllers/RecoveryPreviewJobsController.cs new file mode 100644 index 000000000..3e8b04682 --- /dev/null +++ b/sample/DeltaFunctions/Controllers/RecoveryPreviewJobsController.cs @@ -0,0 +1,131 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.OData.Formatter.Value; +using Microsoft.AspNetCore.OData.Query; +using Microsoft.AspNetCore.OData.Routing.Controllers; +using Microsoft.OData.Edm; + +namespace microsoft.graph; + +public class recoveryPreviewJobsController : ODataController +{ + private recoveryPreviewJob[] recoveryPreviewJobs = + { + new recoveryPreviewJob() + }; + + [HttpGet] + public recoveryPreviewJob Get(string id) + { + return recoveryPreviewJobs[0]; + } + + [HttpGet] + public IActionResult Get() + { + return Ok(recoveryPreviewJobs); + } + + [HttpGet] + public IActionResult getChanges([FromRoute] string key, ODataQueryOptions queryOptions) + { + var result = new EdmChangedObjectCollection(GraphModel.recoveryChangeObjectType); + //could alternatively use DeltaSet + //var result = new DeltaSet(); + + // Get IQueryable of the changes and apply filter, orderby, etc. to that queryable + // IQueryable changeObjects = getChangeObjects(); + // var filteredChanges = queryOptions.ApplyTo(changeObjects); + // Loop through the recoveryChangeObject instances and generate the delta payload + // foreach(recoveryChangeObject changeObject in filteredChanges)... + + // example: create recoveryChangeObject for changed user + var recoveryChangeObject = new EdmEntityObject(GraphModel.recoveryChangeObjectType); + // could alternatively do Delta + // var recoveryChangeObject = new Delta(); + // Set properties + // recoveryChangeObject.TrySetPropertyValue("id", "1"); + recoveryChangeObject.TrySetPropertyValue("currentState", getUser()); + recoveryChangeObject.TrySetPropertyValue("deltaFromCurrent", getChangedUser()); + // Add to result + result.Add(recoveryChangeObject); + + // Create recoveryChangeObject for group w/members + recoveryChangeObject = new EdmEntityObject(GraphModel.recoveryChangeObjectType); + // alternatively: + // recoveryChangeObject = new Delta(); + // Set Properties + recoveryChangeObject.TrySetPropertyValue("id", "2"); + recoveryChangeObject.TrySetPropertyValue("currentState", getGroup()); + recoveryChangeObject.TrySetPropertyValue("deltaFromCurrent", getChangedGroup()); + // Add to result + result.Add(recoveryChangeObject); + + return Ok(result); + } + + //TODO: getChanges isn't found as a valid segment + //[HttpGet("recoveryChangeObjects/{id}/microsoft.graph.getChanges()/{key}/deltaFromCurrent")] + [EnableQuery] + public IActionResult getChangesFromRecoveryChangeObject([FromRoute] string id, [FromRoute] string key) + { + return Ok(getChangedUser()); + } + + // Example function to get a user + private EdmEntityObject getUser() + { + EdmEntityObject user = new EdmEntityObject(GraphModel.userType); + user.TrySetPropertyValue("id", "user1"); + user.TrySetPropertyValue("displayName", "William"); + return user; + } + + // Example function to get a group + private EdmEntityObject getGroup() + { + EdmEntityObject user = new EdmEntityObject(GraphModel.groupType); + user.TrySetPropertyValue("id", "group1"); + var members = new EdmEntityObjectCollection(new EdmCollectionTypeReference(new EdmCollectionType(new EdmEntityTypeReference(EdmCoreModel.Instance.GetEntityType(),false)))); + var member1 = new EdmEntityObject(GraphModel.userType); + member1.TrySetPropertyValue("id", "user7"); + member1.TrySetPropertyValue("email", "user7@users.com"); + members.Add(member1); + user.TrySetPropertyValue("members",members); + return user; + } + + // Example function showing creating a changed user + private EdmDeltaResourceObject getChangedUser() + { + var changedUser = new EdmDeltaResourceObject(GraphModel.userType); + changedUser.TrySetPropertyValue("id", "user1"); + changedUser.TrySetPropertyValue("displayName", "Bill"); + return changedUser; + } + + // Example function showing creating a changed group + private EdmDeltaResourceObject getChangedGroup() { + var members = new EdmChangedObjectCollection(EdmCoreModel.Instance.GetEntityType()); + + var addedMember = new EdmDeltaResourceObject(GraphModel.userType); + addedMember.TrySetPropertyValue("id", "user3"); + members.Add(addedMember); + + var deletedMember = new EdmDeltaDeletedResourceObject(GraphModel.userType); + deletedMember.Id = new Uri("https://graph.microsoft.com/v1.0/users/4"); + deletedMember.TrySetPropertyValue("id", "user4"); + members.Add(deletedMember); + + var group = new EdmDeltaResourceObject(GraphModel.groupType); + group.TrySetPropertyValue("id", "group1"); + group.TrySetPropertyValue("members", members); + return group; + } +} diff --git a/sample/DeltaFunctions/DeltaFunctionsSample.csproj b/sample/DeltaFunctions/DeltaFunctionsSample.csproj new file mode 100644 index 000000000..504ee1849 --- /dev/null +++ b/sample/DeltaFunctions/DeltaFunctionsSample.csproj @@ -0,0 +1,12 @@ + + + + net8.0 + enable + + + + + + + diff --git a/sample/DeltaFunctions/Models/EdmModelBuilder.cs b/sample/DeltaFunctions/Models/EdmModelBuilder.cs new file mode 100644 index 000000000..6563ea75b --- /dev/null +++ b/sample/DeltaFunctions/Models/EdmModelBuilder.cs @@ -0,0 +1,85 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +using Microsoft.AspNetCore.OData.Formatter.Value; +using Microsoft.OData.Edm; +using Microsoft.OData.ModelBuilder; +using System.ComponentModel; + +namespace microsoft.graph; + +public class GraphModel +{ + public static EdmModel Model = GetEdmModel(); + public static EdmEntityType userType; + public static EdmEntityType groupType; + public static EdmEntityType recoveryChangeObjectType; + public static EdmModel GetEdmModel() + { + var model = new EdmModel(); + string graphNamespace = "microsoft.graph"; + var entityContainer = model.AddEntityContainer(graphNamespace, "graphService"); + + // define recovery preview job + var recoveryPreviewJobType = model.AddEntityType(graphNamespace,"recoveryPreviewJob"); + recoveryPreviewJobType.AddKeys( + recoveryPreviewJobType.AddStructuralProperty("id", EdmPrimitiveTypeKind.String) + ); + entityContainer.AddEntitySet("recoveryPreviewJobs", recoveryPreviewJobType); + + // define recovery change object + recoveryChangeObjectType = model.AddEntityType(graphNamespace, "recoveryChangeObject"); + recoveryChangeObjectType.AddKeys( + recoveryChangeObjectType.AddStructuralProperty("id", EdmPrimitiveTypeKind.String) + ); + recoveryChangeObjectType.AddUnidirectionalNavigation(new EdmNavigationPropertyInfo + { + Name = "currentState", + Target = EdmCoreModel.Instance.GetEntityType(), + TargetMultiplicity = EdmMultiplicity.One + }); + recoveryChangeObjectType.AddUnidirectionalNavigation(new EdmNavigationPropertyInfo + { + Name = "deltaFromCurrent", + Target = EdmCoreModel.Instance.GetEntityType(), + TargetMultiplicity = EdmMultiplicity.One + }); + + // schema for graph types (loaded from graph csdl) + var directoryObjectType = model.AddEntityType(graphNamespace, "directoryObject"); + directoryObjectType.AddKeys( + directoryObjectType.AddStructuralProperty("id", EdmPrimitiveTypeKind.String) + ); + directoryObjectType.AddStructuralProperty("displayName",EdmPrimitiveTypeKind.String); + userType = model.AddEntityType(graphNamespace, "user", directoryObjectType); + userType.AddStructuralProperty("emailName", EdmPrimitiveTypeKind.String); + groupType = model.AddEntityType(graphNamespace, "group", directoryObjectType); + groupType.AddUnidirectionalNavigation(new EdmNavigationPropertyInfo + { + Name = "members", + Target = directoryObjectType, + TargetMultiplicity= EdmMultiplicity.Many + }); + + // add getChanges function + var recoveryChangeObjectCollectionType = new EdmCollectionTypeReference(new EdmCollectionType(new EdmEntityTypeReference(recoveryChangeObjectType,false))); + var getChangesFunction = new EdmFunction(graphNamespace, "getChanges", recoveryChangeObjectCollectionType, true, null, true); + getChangesFunction.AddParameter("recoveryPreviewJob", new EdmEntityTypeReference(recoveryPreviewJobType, false)); + + model.AddElement(getChangesFunction); + + // TODO: Temportary hack to provide navigation source when writing; get rid of this + entityContainer.AddEntitySet("recoveryChangeObjects", recoveryChangeObjectType); + model.SetAnnotationValue(getChangesFunction, new ReturnedEntitySetAnnotation("recoveryChangeObjects")); + + //jobs.EntityType.Function("getChanges").ReturnsCollectionFromEntitySet("recoveryChangeObjects"); + //jobs.EntityType.Function("getChanges").ReturnsCollectionViaEntitySetPath("getChanges"); + //jobs.EntityType.Function("getChanges").ReturnsCollection(); + + return model; + } +} diff --git a/sample/DeltaFunctions/Models/recoveryChangeObject.cs b/sample/DeltaFunctions/Models/recoveryChangeObject.cs new file mode 100644 index 000000000..abd5a9604 --- /dev/null +++ b/sample/DeltaFunctions/Models/recoveryChangeObject.cs @@ -0,0 +1,16 @@ +using Microsoft.OData.ModelBuilder; + +namespace microsoft.graph; + +/// +/// RecoveryChangeObject as entity +/// +public class recoveryChangeObject +{ + public string id { get; set; } + [AutoExpand] + public object currentState { get; set; } + [AutoExpand] + public object deltaFromCurrent { get; set; } +} + diff --git a/sample/DeltaFunctions/Models/recoveryPreviewJob.cs b/sample/DeltaFunctions/Models/recoveryPreviewJob.cs new file mode 100644 index 000000000..5a5515bd0 --- /dev/null +++ b/sample/DeltaFunctions/Models/recoveryPreviewJob.cs @@ -0,0 +1,9 @@ +namespace microsoft.graph; + +/// +/// RecoveryPreviewJob +/// +public class recoveryPreviewJob +{ + public string id { get; set; } +} diff --git a/sample/DeltaFunctions/Program.cs b/sample/DeltaFunctions/Program.cs new file mode 100644 index 000000000..cadfbd7c1 --- /dev/null +++ b/sample/DeltaFunctions/Program.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.OData; +using microsoft.graph; + + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddControllers(). + AddOData(opt => opt.EnableQueryFeatures() + .AddRouteComponents("", GraphModel.Model)); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. + +app.UseODataRouteDebug(); + +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); diff --git a/sample/DeltaFunctions/Properties/launchSettings.json b/sample/DeltaFunctions/Properties/launchSettings.json new file mode 100644 index 000000000..ffcad3ad8 --- /dev/null +++ b/sample/DeltaFunctions/Properties/launchSettings.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:48226", + "sslPort": 0 + } + }, + "profiles": { + "ODataAlternateKeySample": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "$odata", + "applicationUrl": "http://localhost:5219", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "weatherforecast", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/sample/DeltaFunctions/appsettings.Development.json b/sample/DeltaFunctions/appsettings.Development.json new file mode 100644 index 000000000..0c208ae91 --- /dev/null +++ b/sample/DeltaFunctions/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/sample/DeltaFunctions/appsettings.json b/sample/DeltaFunctions/appsettings.json new file mode 100644 index 000000000..10f68b8c8 --- /dev/null +++ b/sample/DeltaFunctions/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} From 7ea756f6a3ad9919f832da13c3f5b8d1a390acd4 Mon Sep 17 00:00:00 2001 From: mikepizzo Date: Tue, 3 Dec 2024 03:03:31 -0800 Subject: [PATCH 2/9] Fix Writing Delta Responses -only write properties in deltaset items that have been set -support writing additional properties for deleted entities -nested resource sets that aren't deltasets should be written w/o @delta -factored out ODataDeletedResourceSerializer -support writing delta payloads w/out knowing navigation source (i.e., when serializing results from a function) --- .../ODataDeletedResourceSerializer.cs | 432 ++++++++++++++++++ .../ODataDeltaResourceSetSerializer.cs | 51 ++- .../Serialization/ODataResourceSerializer.cs | 196 +++++--- .../ODataResourceSetSerializer.cs | 55 +-- .../Formatter/Value/EdmObjectExtensions.cs | 4 +- .../PublicAPI.Unshipped.txt | 11 + .../BulkOperation/BulkOperationTest.cs | 2 +- .../DeltaToken/DeltaTokenQueryTests.cs | 12 +- .../ODataDeltaResourceSetSerializerTests.cs | 6 +- ...rosoft.AspNetCore.OData.PublicApi.Net8.bsl | 26 ++ 10 files changed, 671 insertions(+), 124 deletions(-) create mode 100644 src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataDeletedResourceSerializer.cs diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataDeletedResourceSerializer.cs b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataDeletedResourceSerializer.cs new file mode 100644 index 000000000..a6efe2c5c --- /dev/null +++ b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataDeletedResourceSerializer.cs @@ -0,0 +1,432 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Contracts; +using System.Linq; +using System.Runtime.Serialization; +using Microsoft.AspNetCore.OData.Edm; +using Microsoft.AspNetCore.OData.Extensions; +using Microsoft.AspNetCore.OData.Formatter.Value; +using Microsoft.AspNetCore.OData.Routing; +using Microsoft.OData; +using Microsoft.OData.ModelBuilder; +using Microsoft.OData.Edm; +using Microsoft.OData.UriParser; +using System.Threading.Tasks; +using Microsoft.AspNetCore.OData.Deltas; + +namespace Microsoft.AspNetCore.OData.Formatter.Serialization; + +/// +/// ODataSerializer for serializing instances of /> +/// +[SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Justification = "Relies on many ODataLib classes.")] +public class ODataDeletedResourceSerializer : ODataEdmTypeSerializer +{ + private const string Resource = "DeletedResource"; + + /// + public ODataDeletedResourceSerializer(IODataSerializerProvider serializerProvider) + : base(ODataPayloadKind.Resource, serializerProvider) + { + } + + /// + public override async Task WriteObjectAsync(object graph, Type type, ODataMessageWriter messageWriter, + ODataSerializerContext writeContext) + { + if (messageWriter == null) + { + throw Error.ArgumentNull(nameof(messageWriter)); + } + + if (writeContext == null) + { + throw Error.ArgumentNull(nameof(writeContext)); + } + + bool isUntypedPath = writeContext.Path.IsUntypedPropertyPath(); + IEdmTypeReference edmType = writeContext.GetEdmType(graph, type, isUntypedPath); + Contract.Assert(edmType != null); + + IEdmNavigationSource navigationSource = writeContext.NavigationSource; + ODataWriter writer = await messageWriter.CreateODataResourceWriterAsync(navigationSource, edmType.ToStructuredType()) + .ConfigureAwait(false); + await WriteObjectInlineAsync(graph, edmType, writer, writeContext).ConfigureAwait(false); + } + + /// + public override async Task WriteObjectInlineAsync(object graph, IEdmTypeReference expectedType, ODataWriter writer, + ODataSerializerContext writeContext) + { + if (writer == null) + { + throw Error.ArgumentNull(nameof(writer)); + } + + if (writeContext == null) + { + throw Error.ArgumentNull(nameof(writeContext)); + } + + if (graph == null || graph is NullEdmComplexObject) + { + throw new SerializationException(Error.Format(SRResources.CannotSerializerNull, Resource)); + } + else + { + await WriteDeletedResourceAsync(graph, writer, writeContext, expectedType).ConfigureAwait(false); + } + } + + private async Task WriteDeletedResourceAsync(object graph, ODataWriter writer, ODataSerializerContext writeContext, + IEdmTypeReference expectedType) + { + Contract.Assert(writeContext != null); + + //TODO: do we need this? + //if (graph.GetType().IsDynamicTypeWrapper()) + //{ + // await new ODataResourceSerializer(SerializerProvider).WriteDynamicTypeResourceAsync(graph, writer, expectedType, writeContext).ConfigureAwait(false); + // return; + //} + + IEdmStructuredTypeReference structuredType = ODataResourceSerializer.GetResourceType(graph, writeContext); + ResourceContext resourceContext = new ResourceContext(writeContext, structuredType, graph); + + SelectExpandNode selectExpandNode = CreateSelectExpandNode(resourceContext); + if (selectExpandNode != null) + { + ODataDeletedResource odataDeletedResource; + + if (graph is EdmDeltaDeletedResourceObject edmDeltaDeletedEntity) + { + odataDeletedResource = CreateDeletedResource(edmDeltaDeletedEntity.Id, edmDeltaDeletedEntity.Reason ?? DeltaDeletedEntryReason.Deleted, selectExpandNode, resourceContext); + if (edmDeltaDeletedEntity.NavigationSource != null) + { + resourceContext.NavigationSource = edmDeltaDeletedEntity.NavigationSource; + ODataResourceSerializationInfo serializationInfo = new ODataResourceSerializationInfo + { + NavigationSourceName = edmDeltaDeletedEntity.NavigationSource.Name + }; + odataDeletedResource.SetSerializationInfo(serializationInfo); + } + } + else if (graph is IDeltaDeletedResource deltaDeletedResource) + { + odataDeletedResource = CreateDeletedResource(deltaDeletedResource.Id, deltaDeletedResource.Reason ?? DeltaDeletedEntryReason.Deleted, selectExpandNode, resourceContext); + } + else + { + throw new SerializationException(Error.Format(SRResources.CannotWriteType, GetType().Name, graph?.GetType().FullName)); + } + + bool isDelta = (graph is IDelta || graph is IEdmChangedObject); + await writer.WriteStartAsync(odataDeletedResource).ConfigureAwait(false); + await new ODataResourceSerializer(SerializerProvider).WriteResourceContent(writer, selectExpandNode, resourceContext, isDelta); + await writer.WriteEndAsync().ConfigureAwait(false); + } + } + + /// + /// Creates the that describes the set of properties and actions to select and expand while writing this entity. + /// + /// Contains the entity instance being written and the context. + /// + /// The that describes the set of properties and actions to select and expand while writing this entity. + /// + public virtual SelectExpandNode CreateSelectExpandNode(ResourceContext resourceContext) + { + if (resourceContext == null) + { + throw Error.ArgumentNull(nameof(resourceContext)); + } + + ODataSerializerContext writeContext = resourceContext.SerializerContext; + IEdmStructuredType structuredType = resourceContext.StructuredType; + + object selectExpandNode; + + Tuple key = Tuple.Create(writeContext.SelectExpandClause, structuredType); + if (!writeContext.Items.TryGetValue(key, out selectExpandNode)) + { + // cache the selectExpandNode so that if we are writing a feed we don't have to construct it again. + selectExpandNode = new SelectExpandNode(structuredType, writeContext); + writeContext.Items[key] = selectExpandNode; + } + + return selectExpandNode as SelectExpandNode; + } + + /// + /// Creates the to be written while writing this resource. + /// + /// The id of the Deleted Resource to be written (may be null if properties contains all key properties) + /// The for the removal of the resource. + /// The describing the response graph. + /// The context for the resource instance being written. + /// The created . + public virtual ODataDeletedResource CreateDeletedResource(Uri id, DeltaDeletedEntryReason reason, SelectExpandNode selectExpandNode, ResourceContext resourceContext) + { + if (selectExpandNode == null) + { + throw Error.ArgumentNull(nameof(selectExpandNode)); + } + + if (resourceContext == null) + { + throw Error.ArgumentNull(nameof(resourceContext)); + } + + string typeName = resourceContext.StructuredType.FullTypeName(); + + ODataDeletedResource resource = new ODataDeletedResource + { + Id = id ?? (resourceContext.NavigationSource == null ? null : resourceContext.GenerateSelfLink(false)), + TypeName = typeName ?? "Edm.Untyped", + Properties = CreateStructuralPropertyBag(selectExpandNode, resourceContext), + Reason = reason + }; + + ODataResourceSerializer.InitializeODataResource(selectExpandNode, resource, resourceContext); + + string etag = CreateETag(resourceContext); + if (etag != null) + { + resource.ETag = etag; + } + + // Try to add the dynamic properties if the structural type is open. + AppendDynamicProperties(resource, selectExpandNode, resourceContext); + + return resource; + } + + /// + /// Appends the dynamic properties of primitive, enum or the collection of them into the given . + /// If the dynamic property is a property of the complex or collection of complex, it will be saved into + /// the dynamic complex properties dictionary of and be written later. + /// + /// The describing the resource. + /// The describing the response graph. + /// The context for the resource instance being written. + [SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Justification = "Relies on many classes.")] + [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Justification = "These are simple conversion function and cannot be split up.")] + public virtual void AppendDynamicProperties(ODataDeletedResource resource, SelectExpandNode selectExpandNode, + ResourceContext resourceContext) + { + Contract.Assert(resource != null); + Contract.Assert(selectExpandNode != null); + Contract.Assert(resourceContext != null); + + ODataResourceSerializer.AppendDynamicPropertiesInternal(resource, selectExpandNode, resourceContext, SerializerProvider); + } + + /// + /// Creates the ETag for the given entity. + /// + /// The context for the resource instance being written. + /// The created ETag. + public virtual string CreateETag(ResourceContext resourceContext) + { + if (resourceContext == null) + { + throw Error.ArgumentNull(nameof(resourceContext)); + } + + if (resourceContext.Request != null) + { + IEdmModel model = resourceContext.EdmModel; + IEdmNavigationSource navigationSource = resourceContext.NavigationSource; + + IEnumerable concurrencyProperties; + if (model != null && navigationSource != null) + { + concurrencyProperties = model.GetConcurrencyProperties(navigationSource); + } + else + { + concurrencyProperties = Enumerable.Empty(); + } + + IDictionary properties = null; + foreach (IEdmStructuralProperty etagProperty in concurrencyProperties) + { + properties ??= new SortedDictionary(); + + properties.Add(etagProperty.Name, resourceContext.GetPropertyValue(etagProperty.Name)); + } + + if (properties != null) + { + return resourceContext.Request.CreateETag(properties, resourceContext.TimeZone); + } + } + + return null; + } + + //TODO: call method in ODataResourceSerializer + private IEnumerable CreateStructuralPropertyBag(SelectExpandNode selectExpandNode, ResourceContext resourceContext) + { + Contract.Assert(selectExpandNode != null); + Contract.Assert(resourceContext != null); + + int propertiesCount = (selectExpandNode.SelectedStructuralProperties?.Count ?? 0) + (selectExpandNode.SelectedComputedProperties?.Count ?? 0); + List properties = new List(propertiesCount); + + if (selectExpandNode.SelectedStructuralProperties != null) + { + IEnumerable structuralProperties = selectExpandNode.SelectedStructuralProperties; + + if (null != resourceContext.EdmObject && resourceContext.EdmObject.IsDeltaResource()) + { + IDelta deltaObject = null; + if (resourceContext.EdmObject is TypedEdmEntityObject obj) + { + deltaObject = obj.Instance as IDelta; + } + else + { + deltaObject = resourceContext.EdmObject as IDelta; + } + + if (deltaObject != null) + { + IEnumerable changedProperties = deltaObject.GetChangedPropertyNames(); + structuralProperties = structuralProperties.Where(p => changedProperties.Contains(p.Name) || p.IsKey()); + } + } + + foreach (IEdmStructuralProperty structuralProperty in structuralProperties) + { + if (structuralProperty.Type != null && structuralProperty.Type.IsStream()) + { + // skip the stream property, the stream property is written in its own logic + continue; + } + + if (structuralProperty.Type != null && + (structuralProperty.Type.IsUntyped() || structuralProperty.Type.IsCollectionUntyped())) + { + // skip it here, we use a different method to write all 'declared' untyped properties + continue; + } + + ODataProperty property = CreateStructuralProperty(structuralProperty, resourceContext); + if (property != null) + { + properties.Add(property); + } + } + } + + // Try to add computed properties + if (selectExpandNode.SelectedComputedProperties != null) + { + foreach (string propertyName in selectExpandNode.SelectedComputedProperties) + { + ODataProperty property = CreateComputedProperty(propertyName, resourceContext); + if (property != null) + { + properties.Add(property); + } + } + } + + return properties; + } + + /// + /// Creates the to be written for the given resource. + /// + /// The computed property being written. + /// The context for the resource instance being written. + /// The to write. + public virtual ODataProperty CreateComputedProperty(string propertyName, ResourceContext resourceContext) + { + if (string.IsNullOrWhiteSpace(propertyName)) + { + throw Error.ArgumentNullOrEmpty(nameof(propertyName)); + } + + if (resourceContext == null) + { + throw Error.ArgumentNull(nameof(resourceContext)); + } + + // The computed value is from the Linq expression binding. + object propertyValue = resourceContext.GetPropertyValue(propertyName); + if (propertyValue == null) + { + return new ODataProperty { Name = propertyName, Value = null }; + } + + ODataSerializerContext writeContext = resourceContext.SerializerContext; + + IEdmTypeReference edmTypeReference = resourceContext.SerializerContext.GetEdmType(propertyValue, propertyValue.GetType()); + if (edmTypeReference == null) + { + throw Error.NotSupported(SRResources.TypeOfDynamicPropertyNotSupported, propertyValue.GetType().FullName, propertyName); + } + + IODataEdmTypeSerializer serializer = SerializerProvider.GetEdmTypeSerializer(edmTypeReference); + if (serializer == null) + { + throw new SerializationException(Error.Format(SRResources.TypeCannotBeSerialized, edmTypeReference.FullName())); + } + + return serializer.CreateProperty(propertyValue, edmTypeReference, propertyName, writeContext); + } + + /// + /// Creates the to be written for the given entity and the structural property. + /// + /// The EDM structural property being written. + /// The context for the entity instance being written. + /// The to write. + public virtual ODataProperty CreateStructuralProperty(IEdmStructuralProperty structuralProperty, ResourceContext resourceContext) + { + if (structuralProperty == null) + { + throw Error.ArgumentNull(nameof(structuralProperty)); + } + if (resourceContext == null) + { + throw Error.ArgumentNull(nameof(resourceContext)); + } + + ODataSerializerContext writeContext = resourceContext.SerializerContext; + + IODataEdmTypeSerializer serializer = SerializerProvider.GetEdmTypeSerializer(structuralProperty.Type); + if (serializer == null) + { + throw new SerializationException( + Error.Format(SRResources.TypeCannotBeSerialized, structuralProperty.Type.FullName())); + } + + object propertyValue = resourceContext.GetPropertyValue(structuralProperty.Name); + + IEdmTypeReference propertyType = structuralProperty.Type; + if (propertyValue != null) + { + if (!propertyType.IsPrimitive() && !propertyType.IsEnum()) + { + IEdmTypeReference actualType = writeContext.GetEdmType(propertyValue, propertyValue.GetType()); + if (propertyType != null && propertyType != actualType) + { + propertyType = actualType; + } + } + } + + return serializer.CreateProperty(propertyValue, propertyType, structuralProperty.Name, writeContext); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataDeltaResourceSetSerializer.cs b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataDeltaResourceSetSerializer.cs index 25b700958..55112ca2f 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataDeltaResourceSetSerializer.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataDeltaResourceSetSerializer.cs @@ -8,6 +8,7 @@ using System; using System.Collections; using System.Diagnostics.Contracts; +using System.Reflection; using System.Runtime.Serialization; using System.Threading.Tasks; using Microsoft.AspNetCore.OData.Abstracts; @@ -56,10 +57,6 @@ public override async Task WriteObjectAsync(object graph, Type type, ODataMessag } IEdmEntitySetBase entitySet = writeContext.NavigationSource as IEdmEntitySetBase; - if (entitySet == null) - { - throw new SerializationException(SRResources.EntitySetMissingDuringSerialization); - } IEdmTypeReference feedType = writeContext.GetEdmType(graph, type); Contract.Assert(feedType != null); @@ -162,11 +159,19 @@ private async Task WriteDeltaResourceSetAsync(IEnumerable enumerable, IEdmTypeRe } lastResource = item; - DeltaItemKind kind = GetDelteItemKind(item); + DeltaItemKind kind = GetDeltaItemKind(item); switch (kind) { case DeltaItemKind.DeletedResource: - await WriteDeltaDeletedResourceAsync(item, writer, writeContext).ConfigureAwait(false); + // hack. if the WriteDeltaDeletedResourceAsync isn't overridden, call the new version + if (WriteDeltaDeletedResourceAsyncIsOverridden()) + { + await WriteDeltaDeletedResourceAsync(item, writer, writeContext).ConfigureAwait(false); + } + else + { + await WriteDeletedResourceAsync(item, elementType, writer, writeContext).ConfigureAwait(false); + } break; case DeltaItemKind.DeltaDeletedLink: await WriteDeltaDeletedLinkAsync(item, writer, writeContext).ConfigureAwait(false); @@ -212,7 +217,7 @@ await entrySerializer.WriteDeltaObjectInlineAsync(item, elementType, writer, wri /// The serializer context. /// The function that generates the NextLink from an object. /// - internal static Func GetNextLinkGenerator(ODataDeltaResourceSet deltaResourceSet, IEnumerable enumerable, ODataSerializerContext writeContext) + internal static Func GetNextLinkGenerator(ODataResourceSetBase deltaResourceSet, IEnumerable enumerable, ODataSerializerContext writeContext) { return ODataResourceSetSerializer.GetNextLinkGenerator(deltaResourceSet, enumerable, writeContext); } @@ -266,6 +271,8 @@ public virtual ODataDeltaResourceSet CreateODataDeltaResourceSet(IEnumerable fee /// The object to be written. /// The to be used for writing. /// The . + [Obsolete("WriteDeltaDeletedResourceAsync(object, ODataWriter, ODataSerializerContext) is Deprecated and will be removed in the next version." + + "Please use WriteDeletedResourceAsync(object, IEdmEntityTypeReference, ODataWriter, ODataSerializerContext)")] public virtual async Task WriteDeltaDeletedResourceAsync(object value, ODataWriter writer, ODataSerializerContext writeContext) { if (writer == null) @@ -304,6 +311,27 @@ public virtual async Task WriteDeltaDeletedResourceAsync(object value, ODataWrit } } + /// + /// Writes the given deltaDeletedEntry specified by the parameter graph as a part of an existing OData message using the given + /// messageWriter and the writeContext. + /// + /// The object to be written. + /// The expected type of the deleted resource. + /// The to be used for writing. + /// The . + public virtual async Task WriteDeletedResourceAsync(object value, IEdmStructuredTypeReference expectedType, ODataWriter writer, ODataSerializerContext writeContext) + { + if (writer == null) + { + throw Error.ArgumentNull(nameof(writer)); + } + + //todo:use serializer provider + ODataDeletedResourceSerializer deletedResourceSerializer = new ODataDeletedResourceSerializer(SerializerProvider); + + await deletedResourceSerializer.WriteObjectInlineAsync(value, expectedType, writer, writeContext); + } + /// /// Writes the given deltaDeletedLink specified by the parameter graph as a part of an existing OData message using the given /// messageWriter and the writeContext. @@ -382,7 +410,7 @@ public virtual async Task WriteDeltaLinkAsync(object value, ODataWriter writer, } } - internal DeltaItemKind GetDelteItemKind(object item) + internal DeltaItemKind GetDeltaItemKind(object item) { IEdmChangedObject edmChangedObject = item as IEdmChangedObject; if (edmChangedObject != null) @@ -414,4 +442,11 @@ private static IEdmStructuredTypeReference GetResourceType(IEdmTypeReference fee string message = Error.Format(SRResources.CannotWriteType, typeof(ODataDeltaResourceSetSerializer).Name, feedType.FullName()); throw new SerializationException(message); } + + private bool WriteDeltaDeletedResourceAsyncIsOverridden() + { + MethodInfo method = GetType().GetMethod("WriteDeltaDeletedResourceAsync", new Type[] { typeof(object), typeof(ODataWriter), typeof(ODataSerializerContext) }); + Contract.Assert(method != null, "WriteDeltaDeletedResourceAsync is not defined."); + return method.DeclaringType != typeof(ODataDeltaResourceSetSerializer); + } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataResourceSerializer.cs b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataResourceSerializer.cs index 9397fe89c..4f6e48ef5 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataResourceSerializer.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataResourceSerializer.cs @@ -117,35 +117,7 @@ public virtual async Task WriteDeltaObjectInlineAsync(object graph, IEdmTypeRefe } else { - await WriteDeltaResourceAsync(graph, writer, writeContext).ConfigureAwait(false); - } - } - - private async Task WriteDeltaResourceAsync(object graph, ODataWriter writer, ODataSerializerContext writeContext) - { - Contract.Assert(writeContext != null); - - IEdmStructuredTypeReference structuredType = GetResourceType(graph, writeContext); - ResourceContext resourceContext = new ResourceContext(writeContext, structuredType, graph); - EdmDeltaResourceObject deltaResource = graph as EdmDeltaResourceObject; - if (deltaResource != null && deltaResource.NavigationSource != null) - { - resourceContext.NavigationSource = deltaResource.NavigationSource; - } - - SelectExpandNode selectExpandNode = CreateSelectExpandNode(resourceContext); - if (selectExpandNode != null) - { - ODataResource resource = CreateResource(selectExpandNode, resourceContext); - - if (resource != null) - { - await writer.WriteStartAsync(resource).ConfigureAwait(false); - await WriteDeltaComplexPropertiesAsync(selectExpandNode, resourceContext, writer).ConfigureAwait(false); - await WriteDynamicComplexPropertiesAsync(resourceContext, writer).ConfigureAwait(false); - await WriteDeltaNavigationPropertiesAsync(selectExpandNode, resourceContext, writer).ConfigureAwait(false); - await writer.WriteEndAsync().ConfigureAwait(false); - } + await WriteResourceAsync(graph, writer, writeContext, expectedType).ConfigureAwait(false); } } @@ -159,11 +131,22 @@ private async Task WriteDeltaComplexPropertiesAsync(SelectExpandNode selectExpan { return; } + IEnumerable complexProperties = selectExpandNode.SelectedComplexProperties.Keys; if (null != resourceContext.EdmObject && resourceContext.EdmObject.IsDeltaResource()) { - if (resourceContext.EdmObject is TypedEdmEntityObject obj && obj.Instance is IDelta deltaObject) + IDelta deltaObject = null; + if (resourceContext.EdmObject is TypedEdmEntityObject obj) + { + deltaObject = obj.Instance as IDelta; + } + else + { + deltaObject = resourceContext.EdmObject as IDelta; + } + + if (deltaObject != null) { IEnumerable changedProperties = deltaObject.GetChangedPropertyNames(); complexProperties = complexProperties.Where(p => changedProperties.Contains(p.Name)); @@ -235,8 +218,21 @@ await writer.WriteStartAsync(new ODataResourceSet // } if (edmProperty.Type.IsCollection()) { - ODataDeltaResourceSetSerializer serializer = new ODataDeltaResourceSetSerializer(SerializerProvider); - await serializer.WriteObjectInlineAsync(propertyValue, edmProperty.Type, writer, nestedWriteContext).ConfigureAwait(false); + if (IsDeltaCollection(propertyValue)) + { + ODataEdmTypeSerializer serializer = new ODataDeltaResourceSetSerializer(SerializerProvider); + EdmCollectionTypeReference edmType = new EdmCollectionTypeReference(new EdmDeltaCollectionType(new EdmEntityTypeReference(edmProperty.Type.GetElementType() as IEdmEntityType, false))); + await serializer.WriteObjectInlineAsync( + propertyValue, + edmType, + writer, + nestedWriteContext).ConfigureAwait(false); + } + else + { + ODataEdmTypeSerializer serializer = new ODataResourceSetSerializer(SerializerProvider); + await serializer.WriteObjectInlineAsync(propertyValue, edmProperty.Type, writer, nestedWriteContext).ConfigureAwait(false); + } } else { @@ -276,11 +272,11 @@ internal async Task WriteDeltaNavigationPropertiesAsync(SelectExpandNode selectE private IEnumerable> GetNavigationPropertiesToWrite(SelectExpandNode selectExpandNode, ResourceContext resourceContext) { - ISet navigationProperties = selectExpandNode.SelectedNavigationProperties; + IEnumerable navigationProperties = selectExpandNode.ExpandedProperties?.Keys; if (navigationProperties == null) { - yield break; + navigationProperties = resourceContext.StructuredType.DeclaredNavigationProperties(); } if (resourceContext.EdmObject is IDelta changedObject) @@ -442,6 +438,12 @@ private async Task WriteResourceAsync(object graph, ODataWriter writer, ODataSer IEdmStructuredTypeReference structuredType = GetResourceType(graph, writeContext); ResourceContext resourceContext = new ResourceContext(writeContext, structuredType, graph); + IEdmNavigationSource originalNavigationSource = writeContext.NavigationSource; + if (graph is EdmDeltaResourceObject deltaResource && deltaResource?.NavigationSource != null) + { + resourceContext.NavigationSource = deltaResource.NavigationSource; + } + SelectExpandNode selectExpandNode = CreateSelectExpandNode(resourceContext); if (selectExpandNode != null) { @@ -457,18 +459,40 @@ await writer.WriteEntityReferenceLinkAsync(new ODataEntityReferenceLink } else { + bool isDelta = graph is IDelta || graph is IEdmChangedObject; await writer.WriteStartAsync(resource).ConfigureAwait(false); - await WriteUntypedPropertiesAsync(selectExpandNode, resourceContext, writer).ConfigureAwait(false); - await WriteStreamPropertiesAsync(selectExpandNode, resourceContext, writer).ConfigureAwait(false); - await WriteComplexPropertiesAsync(selectExpandNode, resourceContext, writer).ConfigureAwait(false); - await WriteDynamicComplexPropertiesAsync(resourceContext, writer).ConfigureAwait(false); - await WriteNavigationLinksAsync(selectExpandNode, resourceContext, writer).ConfigureAwait(false); - await WriteExpandedNavigationPropertiesAsync(selectExpandNode, resourceContext, writer).ConfigureAwait(false); - await WriteReferencedNavigationPropertiesAsync(selectExpandNode, resourceContext, writer).ConfigureAwait(false); + await WriteResourceContent(writer, selectExpandNode, resourceContext, isDelta); await writer.WriteEndAsync().ConfigureAwait(false); } } } + + writeContext.NavigationSource = originalNavigationSource; + } + + internal async Task WriteResourceContent(ODataWriter writer, SelectExpandNode selectExpandNode, ResourceContext resourceContext, bool isDelta) + { + // TODO: These should be aligned; do we need different methods for delta versus non-delta complex/navigation properties? + if (isDelta) + { + await WriteUntypedPropertiesAsync(selectExpandNode, resourceContext, writer).ConfigureAwait(false); + await WriteStreamPropertiesAsync(selectExpandNode, resourceContext, writer).ConfigureAwait(false); + await WriteDeltaComplexPropertiesAsync(selectExpandNode, resourceContext, writer).ConfigureAwait(false); + //await WriteComplexPropertiesAsync(selectExpandNode, resourceContext, writer).ConfigureAwait(false); + await WriteDynamicComplexPropertiesAsync(resourceContext, writer).ConfigureAwait(false); + await WriteDeltaNavigationPropertiesAsync(selectExpandNode, resourceContext, writer).ConfigureAwait(false); + //await WriteExpandedNavigationPropertiesAsync(selectExpandNode, resourceContext, writer).ConfigureAwait(false); + } + else + { + await WriteUntypedPropertiesAsync(selectExpandNode, resourceContext, writer).ConfigureAwait(false); + await WriteStreamPropertiesAsync(selectExpandNode, resourceContext, writer).ConfigureAwait(false); + await WriteComplexPropertiesAsync(selectExpandNode, resourceContext, writer).ConfigureAwait(false); + await WriteDynamicComplexPropertiesAsync(resourceContext, writer).ConfigureAwait(false); + await WriteNavigationLinksAsync(selectExpandNode, resourceContext, writer).ConfigureAwait(false); + await WriteExpandedNavigationPropertiesAsync(selectExpandNode, resourceContext, writer).ConfigureAwait(false); + await WriteReferencedNavigationPropertiesAsync(selectExpandNode, resourceContext, writer).ConfigureAwait(false); + } } /// @@ -535,17 +559,12 @@ public virtual ODataResource CreateResource(SelectExpandNode selectExpandNode, R Properties = CreateStructuralPropertyBag(selectExpandNode, resourceContext), }; - if (resourceContext.EdmObject is EdmDeltaResourceObject && resourceContext.NavigationSource != null) + InitializeODataResource(selectExpandNode, resource, resourceContext); + + string etag = CreateETag(resourceContext); + if (etag != null) { - ODataResourceSerializationInfo serializationInfo = new ODataResourceSerializationInfo(); - serializationInfo.NavigationSourceName = resourceContext.NavigationSource.Name; - serializationInfo.NavigationSourceKind = resourceContext.NavigationSource.NavigationSourceKind(); - IEdmEntityType sourceType = resourceContext.NavigationSource.EntityType; - if (sourceType != null) - { - serializationInfo.NavigationSourceEntityTypeName = sourceType.Name; - } - resource.SetSerializationInfo(serializationInfo); + resource.ETag = etag; } // Try to add the dynamic properties if the structural type is open. @@ -569,6 +588,24 @@ public virtual ODataResource CreateResource(SelectExpandNode selectExpandNode, R } } + return resource; + } + + internal static void InitializeODataResource(SelectExpandNode selectExpandNode, ODataResourceBase resource, ResourceContext resourceContext) + { + if ((resourceContext.EdmObject is EdmDeltaResourceObject || resourceContext.EdmObject is IEdmDeltaDeletedResourceObject) && resourceContext.NavigationSource != null) + { + ODataResourceSerializationInfo serializationInfo = new ODataResourceSerializationInfo(); + serializationInfo.NavigationSourceName = resourceContext.NavigationSource.Name; + serializationInfo.NavigationSourceKind = resourceContext.NavigationSource.NavigationSourceKind(); + IEdmEntityType sourceType = resourceContext.NavigationSource.EntityType; + if (sourceType != null) + { + serializationInfo.NavigationSourceEntityTypeName = sourceType.Name; + } + resource.SetSerializationInfo(serializationInfo); + } + IEdmStructuredType pathType = GetODataPathType(resourceContext.SerializerContext); if (resourceContext.StructuredType.TypeKind == EdmTypeKind.Complex) { @@ -598,31 +635,23 @@ public virtual ODataResource CreateResource(SelectExpandNode selectExpandNode, R NavigationSourceLinkBuilderAnnotation linkBuilder = EdmModelLinkBuilderExtensions.GetNavigationSourceLinkBuilder(model, resourceContext.NavigationSource); EntitySelfLinks selfLinks = linkBuilder.BuildEntitySelfLinks(resourceContext, resourceContext.SerializerContext.MetadataLevel); - if (selfLinks.IdLink != null) + if (resource.Id == null && selfLinks.IdLink != null) { resource.Id = selfLinks.IdLink; } - if (selfLinks.ReadLink != null) + if (resource.ReadLink == null && selfLinks.ReadLink != null) { resource.ReadLink = selfLinks.ReadLink; } - if (selfLinks.EditLink != null) + if (resource.EditLink == null && selfLinks.EditLink != null) { resource.EditLink = selfLinks.EditLink; } } - - string etag = CreateETag(resourceContext); - if (etag != null) - { - resource.ETag = etag; - } } - - return resource; - } + } /// /// Appends the dynamic properties of primitive, enum or the collection of them into the given . @@ -641,8 +670,14 @@ public virtual void AppendDynamicProperties(ODataResource resource, SelectExpand Contract.Assert(selectExpandNode != null); Contract.Assert(resourceContext != null); + AppendDynamicPropertiesInternal(resource,selectExpandNode, resourceContext, SerializerProvider); + } + + internal static void AppendDynamicPropertiesInternal(ODataResourceBase resource, SelectExpandNode selectExpandNode, + ResourceContext resourceContext, IODataSerializerProvider serializerProvider) + { if (!resourceContext.StructuredType.IsOpen || // non-open type - (!selectExpandNode.SelectAllDynamicProperties && selectExpandNode.SelectedDynamicProperties == null)) + (!selectExpandNode.SelectAllDynamicProperties && selectExpandNode.SelectedDynamicProperties == null)) { return; } @@ -735,7 +770,7 @@ public virtual void AppendDynamicProperties(ODataResource resource, SelectExpand } else { - IODataEdmTypeSerializer propertySerializer = SerializerProvider.GetEdmTypeSerializer(edmTypeReference); + IODataEdmTypeSerializer propertySerializer = serializerProvider.GetEdmTypeSerializer(edmTypeReference); if (propertySerializer == null) { throw Error.NotSupported(SRResources.DynamicPropertyCannotBeSerialized, dynamicProperty.Key, @@ -1252,7 +1287,7 @@ public virtual ODataNestedResourceInfo CreateNavigationLink(IEdmNavigationProper return navigationLink; } - private IEnumerable CreateStructuralPropertyBag(SelectExpandNode selectExpandNode, ResourceContext resourceContext) + internal IEnumerable CreateStructuralPropertyBag(SelectExpandNode selectExpandNode, ResourceContext resourceContext) { Contract.Assert(selectExpandNode != null); Contract.Assert(resourceContext != null); @@ -1266,10 +1301,20 @@ private IEnumerable CreateStructuralPropertyBag(SelectExpandNode if (null != resourceContext.EdmObject && resourceContext.EdmObject.IsDeltaResource()) { - if (resourceContext.EdmObject is TypedEdmEntityObject obj && obj.Instance is IDelta deltaObject) + IDelta deltaObject = null; + if (resourceContext.EdmObject is TypedEdmEntityObject obj) + { + deltaObject = obj.Instance as IDelta; + } + else { + deltaObject = resourceContext.EdmObject as IDelta; + } + + if(deltaObject != null) + { IEnumerable changedProperties = deltaObject.GetChangedPropertyNames(); - structuralProperties = structuralProperties.Where(p => changedProperties.Contains(p.Name)); + structuralProperties = structuralProperties.Where(p => changedProperties.Contains(p.Name) || p.IsKey()); } } @@ -1734,7 +1779,7 @@ private static IEdmStructuredType GetODataPathType(ODataSerializerContext serial } } - internal static void AddTypeNameAnnotationAsNeeded(ODataResource resource, IEdmStructuredType odataPathType, + internal static void AddTypeNameAnnotationAsNeeded(ODataResourceBase resource, IEdmStructuredType odataPathType, ODataMetadataLevel metadataLevel) { // ODataLib normally has the caller decide whether or not to serialize properties by leaving properties @@ -1759,7 +1804,7 @@ internal static void AddTypeNameAnnotationAsNeeded(ODataResource resource, IEdmS resource.TypeAnnotation = new ODataTypeAnnotation(typeName); } - internal static void AddTypeNameAnnotationAsNeededForComplex(ODataResource resource, ODataMetadataLevel metadataLevel) + internal static void AddTypeNameAnnotationAsNeededForComplex(ODataResourceBase resource, ODataMetadataLevel metadataLevel) { // ODataLib normally has the caller decide whether or not to serialize properties by leaving properties // null when values should not be serialized. The TypeName property is different and should always be @@ -1833,7 +1878,7 @@ internal static bool ShouldOmitOperation(IEdmOperation operation, OperationLinkB } } - internal static bool ShouldSuppressTypeNameSerialization(ODataResource resource, IEdmStructuredType edmType, + internal static bool ShouldSuppressTypeNameSerialization(ODataResourceBase resource, IEdmStructuredType edmType, ODataMetadataLevel metadataLevel) { Contract.Assert(resource != null); @@ -1856,7 +1901,7 @@ internal static bool ShouldSuppressTypeNameSerialization(ODataResource resource, } } - private IEdmStructuredTypeReference GetResourceType(object graph, ODataSerializerContext writeContext) + internal static IEdmStructuredTypeReference GetResourceType(object graph, ODataSerializerContext writeContext) { Contract.Assert(graph != null); @@ -1871,9 +1916,14 @@ private IEdmStructuredTypeReference GetResourceType(object graph, ODataSerialize if (!edmType.IsStructured()) { throw new SerializationException( - Error.Format(SRResources.CannotWriteType, GetType().Name, edmType.FullName())); + Error.Format(SRResources.CannotWriteType, typeof(ODataResourceSerializer).Name, edmType.FullName())); } return edmType.AsStructured(); } + + private bool IsDeltaCollection(object collection) + { + return (collection is IDeltaSet || collection is EdmChangedObjectCollection); + } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataResourceSetSerializer.cs b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataResourceSetSerializer.cs index bcb53069e..5afccc1ee 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataResourceSetSerializer.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataResourceSetSerializer.cs @@ -66,7 +66,7 @@ public override async Task WriteObjectAsync(object graph, Type type, ODataMessag Contract.Assert(resourceSetType != null); IEdmStructuredTypeReference resourceType = GetResourceType(resourceSetType); - + ODataWriter writer = await messageWriter.CreateODataResourceSetWriterAsync(entitySet, resourceType.StructuredDefinition()) .ConfigureAwait(false); await WriteObjectInlineAsync(graph, resourceSetType, writer, writeContext) @@ -167,7 +167,7 @@ private async Task WriteResourceSetAsync(IAsyncEnumerable asyncEnumerabl Func nextLinkGenerator = GetNextLinkGenerator(resourceSet, asyncEnumerable, writeContext); WriteResourceSetInternal(resourceSet, elementType, resourceSetType, writeContext, out bool isUntypedCollection, out IODataEdmTypeSerializer resourceSerializer); - + await writer.WriteStartAsync(resourceSet).ConfigureAwait(false); object lastResource = null; @@ -427,20 +427,7 @@ public virtual ODataResourceSet CreateResourceSet(IEnumerable resourceSetInstanc WriteEntityTypeOperations(resourceSet, resourceSetContext, structuredType, writeContext); } - if (writeContext.ExpandedResource == null) - { - // If we have more OData format specific information apply it now, only if we are the root feed. - PageResult odataResourceSetAnnotations = resourceSetInstance as PageResult; - ApplyODataResourceSetAnnotations(resourceSet, odataResourceSetAnnotations, writeContext); - } - else - { - ICountOptionCollection countOptionCollection = resourceSetInstance as ICountOptionCollection; - if (countOptionCollection != null && countOptionCollection.TotalCount != null) - { - resourceSet.Count = countOptionCollection.TotalCount; - } - } + WriteResourceSetInformation(resourceSet, resourceSetInstance, writeContext); return resourceSet; } @@ -472,20 +459,7 @@ public virtual ODataResourceSet CreateResourceSet(IAsyncEnumerable resou WriteEntityTypeOperations(resourceSet, resourceSetContext, structuredType, writeContext); } - if (writeContext.ExpandedResource == null) - { - // If we have more OData format specific information apply it now, only if we are the root feed. - PageResult odataResourceSetAnnotations = resourceSetInstance as PageResult; - ApplyODataResourceSetAnnotations(resourceSet, odataResourceSetAnnotations, writeContext); - } - else - { - ICountOptionCollection countOptionCollection = resourceSetInstance as ICountOptionCollection; - if (countOptionCollection != null && countOptionCollection.TotalCount != null) - { - resourceSet.Count = countOptionCollection.TotalCount; - } - } + WriteResourceSetInformation(resourceSet, resourceSetInstance, writeContext); return resourceSet; } @@ -513,6 +487,27 @@ private void WriteEntityTypeOperations( } } + private void WriteResourceSetInformation( + ODataResourceSet resourceSet, + object resourceSetInstance, + ODataSerializerContext writeContext) + { + if (writeContext.ExpandedResource == null) + { + // If we have more OData format specific information apply it now, only if we are the root feed. + PageResult odataResourceSetAnnotations = resourceSetInstance as PageResult; + ApplyODataResourceSetAnnotations(resourceSet, odataResourceSetAnnotations, writeContext); + } + else + { + ICountOptionCollection countOptionCollection = resourceSetInstance as ICountOptionCollection; + if (countOptionCollection != null && countOptionCollection.TotalCount != null) + { + resourceSet.Count = countOptionCollection.TotalCount; + } + } + } + private void ApplyODataResourceSetAnnotations( ODataResourceSet resourceSet, PageResult odataResourceSetAnnotations, diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmObjectExtensions.cs b/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmObjectExtensions.cs index 5082e15ec..50d306928 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmObjectExtensions.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmObjectExtensions.cs @@ -19,7 +19,7 @@ public static class EdmTypeExtensions /// Method to determine whether the current type is a Delta resource set. /// /// IEdmType to be compared - /// True or False if type is same as + /// True or False if type is a or a public static bool IsDeltaResourceSet(this IEdmType type) { if (type == null) @@ -27,7 +27,7 @@ public static bool IsDeltaResourceSet(this IEdmType type) throw Error.ArgumentNull(nameof(type)); } - return (type.GetType() == typeof(EdmDeltaCollectionType)); + return (type is EdmDeltaCollectionType || type is IDeltaSet); } /// diff --git a/src/Microsoft.AspNetCore.OData/PublicAPI.Unshipped.txt b/src/Microsoft.AspNetCore.OData/PublicAPI.Unshipped.txt index e69de29bb..16988f248 100644 --- a/src/Microsoft.AspNetCore.OData/PublicAPI.Unshipped.txt +++ b/src/Microsoft.AspNetCore.OData/PublicAPI.Unshipped.txt @@ -0,0 +1,11 @@ +Microsoft.AspNetCore.OData.Formatter.Serialization.ODataDeletedResourceSerializer +Microsoft.AspNetCore.OData.Formatter.Serialization.ODataDeletedResourceSerializer.ODataDeletedResourceSerializer(Microsoft.AspNetCore.OData.Formatter.Serialization.IODataSerializerProvider serializerProvider) -> void +override Microsoft.AspNetCore.OData.Formatter.Serialization.ODataDeletedResourceSerializer.WriteObjectAsync(object graph, System.Type type, Microsoft.OData.ODataMessageWriter messageWriter, Microsoft.AspNetCore.OData.Formatter.Serialization.ODataSerializerContext writeContext) -> System.Threading.Tasks.Task +override Microsoft.AspNetCore.OData.Formatter.Serialization.ODataDeletedResourceSerializer.WriteObjectInlineAsync(object graph, Microsoft.OData.Edm.IEdmTypeReference expectedType, Microsoft.OData.ODataWriter writer, Microsoft.AspNetCore.OData.Formatter.Serialization.ODataSerializerContext writeContext) -> System.Threading.Tasks.Task +virtual Microsoft.AspNetCore.OData.Formatter.Serialization.ODataDeletedResourceSerializer.AppendDynamicProperties(Microsoft.OData.ODataDeletedResource resource, Microsoft.AspNetCore.OData.Formatter.Serialization.SelectExpandNode selectExpandNode, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) -> void +virtual Microsoft.AspNetCore.OData.Formatter.Serialization.ODataDeletedResourceSerializer.CreateComputedProperty(string propertyName, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) -> Microsoft.OData.ODataProperty +virtual Microsoft.AspNetCore.OData.Formatter.Serialization.ODataDeletedResourceSerializer.CreateDeletedResource(System.Uri id, Microsoft.OData.DeltaDeletedEntryReason reason, Microsoft.AspNetCore.OData.Formatter.Serialization.SelectExpandNode selectExpandNode, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) -> Microsoft.OData.ODataDeletedResource +virtual Microsoft.AspNetCore.OData.Formatter.Serialization.ODataDeletedResourceSerializer.CreateETag(Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) -> string +virtual Microsoft.AspNetCore.OData.Formatter.Serialization.ODataDeletedResourceSerializer.CreateSelectExpandNode(Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) -> Microsoft.AspNetCore.OData.Formatter.Serialization.SelectExpandNode +virtual Microsoft.AspNetCore.OData.Formatter.Serialization.ODataDeletedResourceSerializer.CreateStructuralProperty(Microsoft.OData.Edm.IEdmStructuralProperty structuralProperty, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) -> Microsoft.OData.ODataProperty +virtual Microsoft.AspNetCore.OData.Formatter.Serialization.ODataDeltaResourceSetSerializer.WriteDeletedResourceAsync(object value, Microsoft.OData.Edm.IEdmStructuredTypeReference expectedType, Microsoft.OData.ODataWriter writer, Microsoft.AspNetCore.OData.Formatter.Serialization.ODataSerializerContext writeContext) -> System.Threading.Tasks.Task \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/BulkOperation/BulkOperationTest.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/BulkOperation/BulkOperationTest.cs index 2f31510d6..25df67e1d 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/BulkOperation/BulkOperationTest.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/BulkOperation/BulkOperationTest.cs @@ -75,7 +75,7 @@ public async Task DeltaSet_WithDeletedAndODataId_IsSerializedSuccessfully() {'ID':2,'Name':'Employee2','Friends@odata.delta':[{'@id':'Friends(1)'}]} ]}"; - string expectedResponse = "{\"@context\":\"http://localhost/convention/$metadata#Employees/$delta\",\"value\":[{\"ID\":1,\"Name\":\"Employee1\",\"Friends@delta\":[{\"@removed\":{\"reason\":\"changed\"},\"@id\":\"http://host/service/Friends(1)\"}]},{\"ID\":2,\"Name\":\"Employee2\",\"Friends@delta\":[{\"Id\":1}]}]}"; + string expectedResponse = "{\"@context\":\"http://localhost/convention/$metadata#Employees/$delta\",\"value\":[{\"ID\":1,\"Name\":\"Employee1\",\"Friends@delta\":[{\"@removed\":{\"reason\":\"changed\"},\"@id\":\"http://host/service/Friends(1)\",\"id\":1}]},{\"ID\":2,\"Name\":\"Employee2\",\"Friends@delta\":[{\"Id\":1}]}]}"; var requestForUpdate = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/DeltaToken/DeltaTokenQueryTests.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/DeltaToken/DeltaTokenQueryTests.cs index f04856b46..9db00d571 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/DeltaToken/DeltaTokenQueryTests.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/DeltaToken/DeltaTokenQueryTests.cs @@ -53,7 +53,7 @@ public async Task DeltaVerifyReslt() Assert.True(results.value.Count == 7, "There should be 7 entries in the response"); var changeEntity = results.value[0]; - Assert.True(((JToken)changeEntity).Count() == 9, "The changed customer should have 6 properties plus type written. But now it contains non-changed properties, it's regression bug?"); + Assert.True(((JToken)changeEntity).Count() == 7, "The changed customer should have 6 properties plus type written. But now it contains non-changed properties, it's regression bug?"); string changeEntityType = changeEntity["@type"].Value as string; Assert.True(changeEntityType != null, "The changed customer should have type written"); Assert.True(changeEntityType.Contains("#Microsoft.AspNetCore.OData.E2E.Tests.DeltaToken.TestCustomerWithAddress"), "The changed order should be a TestCustomerWithAddress"); @@ -61,7 +61,7 @@ public async Task DeltaVerifyReslt() Assert.True(changeEntity.OpenProperty.Value == 10, "The OpenProperty property of changed customer should be 10."); Assert.True(changeEntity.NullOpenProperty.Value == null, "The NullOpenProperty property of changed customer should be null."); Assert.True(changeEntity.Name.Value == "Name", "The Name of changed customer should be 'Name'"); - Assert.True(((JToken)changeEntity.Address).Count() == 3, "The changed entity's Address should have 2 properties written. But now it contains non-changed properties, it's regression bug?"); + Assert.True(((JToken)changeEntity.Address).Count() == 2, "The changed entity's Address should have 2 properties written. But now it contains non-changed properties, it's regression bug?"); Assert.True(changeEntity.Address.State.Value == "State", "The changed customer's Address.State should be 'State'."); Assert.True(changeEntity.Address.ZipCode.Value == (int?)null, "The changed customer's Address.ZipCode should be null."); @@ -71,7 +71,7 @@ public async Task DeltaVerifyReslt() Assert.True(phoneNumbers[1].Value == "765-4321", "The second phone number should be '765-4321'"); var newCustomer = results.value[1]; - Assert.True(((JToken)newCustomer).Count() == 5, "The new customer should have 3 properties written, But now it contains 2 non-changed properties, it's regression bug?"); + Assert.True(((JToken)newCustomer).Count() == 3, "The new customer should have 3 properties written, But now it contains 2 non-changed properties, it's regression bug?"); Assert.True(newCustomer.Id.Value == 10, "The ID of the new customer should be 10"); Assert.True(newCustomer.Name.Value == "NewCustomer", "The name of the new customer should be 'NewCustomer'"); @@ -79,7 +79,7 @@ public async Task DeltaVerifyReslt() Assert.True(((JToken)places).Count() == 2, "The new customer should have 2 favorite places"); var place1 = places[0]; - Assert.True(((JToken)place1).Count() == 3, "The first favorite place should have 2 properties written.But now it contains non-changed properties, it's regression bug?"); + Assert.True(((JToken)place1).Count() == 2, "The first favorite place should have 2 properties written.But now it contains non-changed properties, it's regression bug?"); Assert.True(place1.State.Value == "State", "The first favorite place's state should be 'State'."); Assert.True(place1.ZipCode.Value == (int?)null, "The first favorite place's Address.ZipCode should be null."); @@ -92,7 +92,7 @@ public async Task DeltaVerifyReslt() Assert.True(place2.NullOpenProperty.Value == null, "The second favorite place's Address.NullOpenProperty should be null."); var newOrder = results.value[2]; - Assert.True(((JToken)newOrder).Count() == 4, "The new order should have 2 properties plus context written, , But now it contains one non-changed properties, it's regression bug?"); + Assert.True(((JToken)newOrder).Count() == 3, "The new order should have 2 properties plus context written, , But now it contains one non-changed properties, it's regression bug?"); string newOrderContext = newOrder["@context"].Value as string; Assert.True(newOrderContext != null, "The new order should have a context written"); Assert.True(newOrderContext.Contains("$metadata#TestOrders"), "The new order should come from the TestOrders entity set"); @@ -139,13 +139,11 @@ public async Task DeltaVerifyReslt_ContainsDynamicComplexProperties() "\"Amount\":42," + "\"Location\":{" + "\"State\":\"State\"," + - "\"City\":null," + "\"ZipCode\":null," + "\"OpenProperty\":10," + "\"key-samplelist\":{" + "\"@type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.DeltaToken.TestAddress\"," + "\"State\":\"sample state\"," + - "\"City\":null," + "\"ZipCode\":9," + "\"title\":\"sample title\"" + "}" + diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataDeltaResourceSetSerializerTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataDeltaResourceSetSerializerTests.cs index 12d5eb378..20b57601c 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataDeltaResourceSetSerializerTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataDeltaResourceSetSerializerTests.cs @@ -111,7 +111,7 @@ await ExceptionAssert.ThrowsArgumentNullAsync( } [Fact] - public async Task WriteObjectAsync_Throws_EntitySetMissingDuringSerialization() + public async Task WriteObjectAsync_Throws_ModelMissingDuringSerialization() { // Arrange object graph = new object(); @@ -119,9 +119,9 @@ public async Task WriteObjectAsync_Throws_EntitySetMissingDuringSerialization() ODataDeltaResourceSetSerializer serializer = new ODataDeltaResourceSetSerializer(provider.Object); // Act & Assert - await ExceptionAssert.ThrowsAsync( + await ExceptionAssert.ThrowsAsync( () => serializer.WriteObjectAsync(graph: graph, type: null, messageWriter: ODataTestUtil.GetMockODataMessageWriter(), writeContext: new ODataSerializerContext()), - "The related entity set could not be found from the OData path. The related entity set is required to serialize the payload."); + "The request must have an associated EDM model. Consider registering Edm model calling AddOData()."); } [Fact] diff --git a/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.Net8.bsl b/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.Net8.bsl index 60b3e8ea4..1b34a3a4b 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.Net8.bsl +++ b/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.Net8.bsl @@ -2166,16 +2166,42 @@ public class Microsoft.AspNetCore.OData.Formatter.Serialization.ODataCollectionS public virtual System.Threading.Tasks.Task WriteObjectAsync (object graph, System.Type type, Microsoft.OData.ODataMessageWriter messageWriter, Microsoft.AspNetCore.OData.Formatter.Serialization.ODataSerializerContext writeContext) } +public class Microsoft.AspNetCore.OData.Formatter.Serialization.ODataDeletedResourceSerializer : Microsoft.AspNetCore.OData.Formatter.Serialization.ODataEdmTypeSerializer, IODataEdmTypeSerializer, IODataSerializer { + public ODataDeletedResourceSerializer (Microsoft.AspNetCore.OData.Formatter.Serialization.IODataSerializerProvider serializerProvider) + + public virtual void AppendDynamicProperties (Microsoft.OData.ODataDeletedResource resource, Microsoft.AspNetCore.OData.Formatter.Serialization.SelectExpandNode selectExpandNode, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) + public virtual Microsoft.OData.ODataProperty CreateComputedProperty (string propertyName, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) + public virtual Microsoft.OData.ODataDeletedResource CreateDeletedResource (System.Uri id, Microsoft.OData.DeltaDeletedEntryReason reason, Microsoft.AspNetCore.OData.Formatter.Serialization.SelectExpandNode selectExpandNode, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) + public virtual string CreateETag (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.ODataProperty CreateStructuralProperty (Microsoft.OData.Edm.IEdmStructuralProperty structuralProperty, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) + [ + AsyncStateMachineAttribute(), + ] + public virtual System.Threading.Tasks.Task WriteObjectAsync (object graph, System.Type type, Microsoft.OData.ODataMessageWriter messageWriter, Microsoft.AspNetCore.OData.Formatter.Serialization.ODataSerializerContext writeContext) + + [ + AsyncStateMachineAttribute(), + ] + public virtual System.Threading.Tasks.Task WriteObjectInlineAsync (object graph, Microsoft.OData.Edm.IEdmTypeReference expectedType, Microsoft.OData.ODataWriter writer, Microsoft.AspNetCore.OData.Formatter.Serialization.ODataSerializerContext writeContext) +} + public class Microsoft.AspNetCore.OData.Formatter.Serialization.ODataDeltaResourceSetSerializer : Microsoft.AspNetCore.OData.Formatter.Serialization.ODataEdmTypeSerializer, IODataEdmTypeSerializer, IODataSerializer { public ODataDeltaResourceSetSerializer (Microsoft.AspNetCore.OData.Formatter.Serialization.IODataSerializerProvider serializerProvider) public virtual Microsoft.OData.ODataDeltaResourceSet CreateODataDeltaResourceSet (System.Collections.IEnumerable feedInstance, Microsoft.OData.Edm.IEdmCollectionTypeReference feedType, Microsoft.AspNetCore.OData.Formatter.Serialization.ODataSerializerContext writeContext) + [ + AsyncStateMachineAttribute(), + ] + public virtual System.Threading.Tasks.Task WriteDeletedResourceAsync (object value, Microsoft.OData.Edm.IEdmStructuredTypeReference expectedType, Microsoft.OData.ODataWriter writer, Microsoft.AspNetCore.OData.Formatter.Serialization.ODataSerializerContext writeContext) + [ AsyncStateMachineAttribute(), ] public virtual System.Threading.Tasks.Task WriteDeltaDeletedLinkAsync (object value, Microsoft.OData.ODataWriter writer, Microsoft.AspNetCore.OData.Formatter.Serialization.ODataSerializerContext writeContext) [ + ObsoleteAttribute(), AsyncStateMachineAttribute(), ] public virtual System.Threading.Tasks.Task WriteDeltaDeletedResourceAsync (object value, Microsoft.OData.ODataWriter writer, Microsoft.AspNetCore.OData.Formatter.Serialization.ODataSerializerContext writeContext) From 3760b8e371f2ca28d31f292eb0833915f083d148 Mon Sep 17 00:00:00 2001 From: mikepizzo Date: Tue, 3 Dec 2024 11:42:35 -0800 Subject: [PATCH 3/9] Remove DeltaFunctions test project --- AspNetCoreOData.sln | 7 - .../RecoveryPreviewJobsController.cs | 131 ------------------ .../DeltaFunctionsSample.csproj | 12 -- .../DeltaFunctions/Models/EdmModelBuilder.cs | 85 ------------ .../Models/recoveryChangeObject.cs | 16 --- .../Models/recoveryPreviewJob.cs | 9 -- sample/DeltaFunctions/Program.cs | 21 --- .../Properties/launchSettings.json | 31 ----- .../appsettings.Development.json | 8 -- sample/DeltaFunctions/appsettings.json | 9 -- .../Microsoft.AspNetCore.OData.xml | 80 ++++++++++- 11 files changed, 78 insertions(+), 331 deletions(-) delete mode 100644 sample/DeltaFunctions/Controllers/RecoveryPreviewJobsController.cs delete mode 100644 sample/DeltaFunctions/DeltaFunctionsSample.csproj delete mode 100644 sample/DeltaFunctions/Models/EdmModelBuilder.cs delete mode 100644 sample/DeltaFunctions/Models/recoveryChangeObject.cs delete mode 100644 sample/DeltaFunctions/Models/recoveryPreviewJob.cs delete mode 100644 sample/DeltaFunctions/Program.cs delete mode 100644 sample/DeltaFunctions/Properties/launchSettings.json delete mode 100644 sample/DeltaFunctions/appsettings.Development.json delete mode 100644 sample/DeltaFunctions/appsettings.json diff --git a/AspNetCoreOData.sln b/AspNetCoreOData.sln index 398eff15f..06a9b8f0a 100644 --- a/AspNetCoreOData.sln +++ b/AspNetCoreOData.sln @@ -29,8 +29,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ODataAlternateKeySample", " EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BenchmarkServer", "sample\BenchmarkServer\BenchmarkServer.csproj", "{8346AD1B-00E3-462D-B6B1-9AA3C2FB2850}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DeltaFunctionsSample", "sample\DeltaFunctions\DeltaFunctionsSample.csproj", "{B4D2B1EE-14DA-47E2-AFAD-11087C8508D2}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -77,10 +75,6 @@ Global {8346AD1B-00E3-462D-B6B1-9AA3C2FB2850}.Debug|Any CPU.Build.0 = Debug|Any CPU {8346AD1B-00E3-462D-B6B1-9AA3C2FB2850}.Release|Any CPU.ActiveCfg = Release|Any CPU {8346AD1B-00E3-462D-B6B1-9AA3C2FB2850}.Release|Any CPU.Build.0 = Release|Any CPU - {B4D2B1EE-14DA-47E2-AFAD-11087C8508D2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B4D2B1EE-14DA-47E2-AFAD-11087C8508D2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B4D2B1EE-14DA-47E2-AFAD-11087C8508D2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B4D2B1EE-14DA-47E2-AFAD-11087C8508D2}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -96,7 +90,6 @@ Global {647EFCFA-55A7-4F0A-AD40-4B6EB1BFCFFA} = {B1F86961-6958-4617-ACA4-C231F95AE099} {7B153669-A42F-4511-8BDB-587B3B27B2F3} = {B1F86961-6958-4617-ACA4-C231F95AE099} {8346AD1B-00E3-462D-B6B1-9AA3C2FB2850} = {B1F86961-6958-4617-ACA4-C231F95AE099} - {B4D2B1EE-14DA-47E2-AFAD-11087C8508D2} = {B1F86961-6958-4617-ACA4-C231F95AE099} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {540C9752-AAC0-49EA-BA60-78490C90FF86} diff --git a/sample/DeltaFunctions/Controllers/RecoveryPreviewJobsController.cs b/sample/DeltaFunctions/Controllers/RecoveryPreviewJobsController.cs deleted file mode 100644 index 3e8b04682..000000000 --- a/sample/DeltaFunctions/Controllers/RecoveryPreviewJobsController.cs +++ /dev/null @@ -1,131 +0,0 @@ -//----------------------------------------------------------------------------- -// -// Copyright (c) .NET Foundation and Contributors. All rights reserved. -// See License.txt in the project root for license information. -// -//------------------------------------------------------------------------------ - -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.OData.Formatter.Value; -using Microsoft.AspNetCore.OData.Query; -using Microsoft.AspNetCore.OData.Routing.Controllers; -using Microsoft.OData.Edm; - -namespace microsoft.graph; - -public class recoveryPreviewJobsController : ODataController -{ - private recoveryPreviewJob[] recoveryPreviewJobs = - { - new recoveryPreviewJob() - }; - - [HttpGet] - public recoveryPreviewJob Get(string id) - { - return recoveryPreviewJobs[0]; - } - - [HttpGet] - public IActionResult Get() - { - return Ok(recoveryPreviewJobs); - } - - [HttpGet] - public IActionResult getChanges([FromRoute] string key, ODataQueryOptions queryOptions) - { - var result = new EdmChangedObjectCollection(GraphModel.recoveryChangeObjectType); - //could alternatively use DeltaSet - //var result = new DeltaSet(); - - // Get IQueryable of the changes and apply filter, orderby, etc. to that queryable - // IQueryable changeObjects = getChangeObjects(); - // var filteredChanges = queryOptions.ApplyTo(changeObjects); - // Loop through the recoveryChangeObject instances and generate the delta payload - // foreach(recoveryChangeObject changeObject in filteredChanges)... - - // example: create recoveryChangeObject for changed user - var recoveryChangeObject = new EdmEntityObject(GraphModel.recoveryChangeObjectType); - // could alternatively do Delta - // var recoveryChangeObject = new Delta(); - // Set properties - // recoveryChangeObject.TrySetPropertyValue("id", "1"); - recoveryChangeObject.TrySetPropertyValue("currentState", getUser()); - recoveryChangeObject.TrySetPropertyValue("deltaFromCurrent", getChangedUser()); - // Add to result - result.Add(recoveryChangeObject); - - // Create recoveryChangeObject for group w/members - recoveryChangeObject = new EdmEntityObject(GraphModel.recoveryChangeObjectType); - // alternatively: - // recoveryChangeObject = new Delta(); - // Set Properties - recoveryChangeObject.TrySetPropertyValue("id", "2"); - recoveryChangeObject.TrySetPropertyValue("currentState", getGroup()); - recoveryChangeObject.TrySetPropertyValue("deltaFromCurrent", getChangedGroup()); - // Add to result - result.Add(recoveryChangeObject); - - return Ok(result); - } - - //TODO: getChanges isn't found as a valid segment - //[HttpGet("recoveryChangeObjects/{id}/microsoft.graph.getChanges()/{key}/deltaFromCurrent")] - [EnableQuery] - public IActionResult getChangesFromRecoveryChangeObject([FromRoute] string id, [FromRoute] string key) - { - return Ok(getChangedUser()); - } - - // Example function to get a user - private EdmEntityObject getUser() - { - EdmEntityObject user = new EdmEntityObject(GraphModel.userType); - user.TrySetPropertyValue("id", "user1"); - user.TrySetPropertyValue("displayName", "William"); - return user; - } - - // Example function to get a group - private EdmEntityObject getGroup() - { - EdmEntityObject user = new EdmEntityObject(GraphModel.groupType); - user.TrySetPropertyValue("id", "group1"); - var members = new EdmEntityObjectCollection(new EdmCollectionTypeReference(new EdmCollectionType(new EdmEntityTypeReference(EdmCoreModel.Instance.GetEntityType(),false)))); - var member1 = new EdmEntityObject(GraphModel.userType); - member1.TrySetPropertyValue("id", "user7"); - member1.TrySetPropertyValue("email", "user7@users.com"); - members.Add(member1); - user.TrySetPropertyValue("members",members); - return user; - } - - // Example function showing creating a changed user - private EdmDeltaResourceObject getChangedUser() - { - var changedUser = new EdmDeltaResourceObject(GraphModel.userType); - changedUser.TrySetPropertyValue("id", "user1"); - changedUser.TrySetPropertyValue("displayName", "Bill"); - return changedUser; - } - - // Example function showing creating a changed group - private EdmDeltaResourceObject getChangedGroup() { - var members = new EdmChangedObjectCollection(EdmCoreModel.Instance.GetEntityType()); - - var addedMember = new EdmDeltaResourceObject(GraphModel.userType); - addedMember.TrySetPropertyValue("id", "user3"); - members.Add(addedMember); - - var deletedMember = new EdmDeltaDeletedResourceObject(GraphModel.userType); - deletedMember.Id = new Uri("https://graph.microsoft.com/v1.0/users/4"); - deletedMember.TrySetPropertyValue("id", "user4"); - members.Add(deletedMember); - - var group = new EdmDeltaResourceObject(GraphModel.groupType); - group.TrySetPropertyValue("id", "group1"); - group.TrySetPropertyValue("members", members); - return group; - } -} diff --git a/sample/DeltaFunctions/DeltaFunctionsSample.csproj b/sample/DeltaFunctions/DeltaFunctionsSample.csproj deleted file mode 100644 index 504ee1849..000000000 --- a/sample/DeltaFunctions/DeltaFunctionsSample.csproj +++ /dev/null @@ -1,12 +0,0 @@ - - - - net8.0 - enable - - - - - - - diff --git a/sample/DeltaFunctions/Models/EdmModelBuilder.cs b/sample/DeltaFunctions/Models/EdmModelBuilder.cs deleted file mode 100644 index 6563ea75b..000000000 --- a/sample/DeltaFunctions/Models/EdmModelBuilder.cs +++ /dev/null @@ -1,85 +0,0 @@ -//----------------------------------------------------------------------------- -// -// Copyright (c) .NET Foundation and Contributors. All rights reserved. -// See License.txt in the project root for license information. -// -//------------------------------------------------------------------------------ - -using Microsoft.AspNetCore.OData.Formatter.Value; -using Microsoft.OData.Edm; -using Microsoft.OData.ModelBuilder; -using System.ComponentModel; - -namespace microsoft.graph; - -public class GraphModel -{ - public static EdmModel Model = GetEdmModel(); - public static EdmEntityType userType; - public static EdmEntityType groupType; - public static EdmEntityType recoveryChangeObjectType; - public static EdmModel GetEdmModel() - { - var model = new EdmModel(); - string graphNamespace = "microsoft.graph"; - var entityContainer = model.AddEntityContainer(graphNamespace, "graphService"); - - // define recovery preview job - var recoveryPreviewJobType = model.AddEntityType(graphNamespace,"recoveryPreviewJob"); - recoveryPreviewJobType.AddKeys( - recoveryPreviewJobType.AddStructuralProperty("id", EdmPrimitiveTypeKind.String) - ); - entityContainer.AddEntitySet("recoveryPreviewJobs", recoveryPreviewJobType); - - // define recovery change object - recoveryChangeObjectType = model.AddEntityType(graphNamespace, "recoveryChangeObject"); - recoveryChangeObjectType.AddKeys( - recoveryChangeObjectType.AddStructuralProperty("id", EdmPrimitiveTypeKind.String) - ); - recoveryChangeObjectType.AddUnidirectionalNavigation(new EdmNavigationPropertyInfo - { - Name = "currentState", - Target = EdmCoreModel.Instance.GetEntityType(), - TargetMultiplicity = EdmMultiplicity.One - }); - recoveryChangeObjectType.AddUnidirectionalNavigation(new EdmNavigationPropertyInfo - { - Name = "deltaFromCurrent", - Target = EdmCoreModel.Instance.GetEntityType(), - TargetMultiplicity = EdmMultiplicity.One - }); - - // schema for graph types (loaded from graph csdl) - var directoryObjectType = model.AddEntityType(graphNamespace, "directoryObject"); - directoryObjectType.AddKeys( - directoryObjectType.AddStructuralProperty("id", EdmPrimitiveTypeKind.String) - ); - directoryObjectType.AddStructuralProperty("displayName",EdmPrimitiveTypeKind.String); - userType = model.AddEntityType(graphNamespace, "user", directoryObjectType); - userType.AddStructuralProperty("emailName", EdmPrimitiveTypeKind.String); - groupType = model.AddEntityType(graphNamespace, "group", directoryObjectType); - groupType.AddUnidirectionalNavigation(new EdmNavigationPropertyInfo - { - Name = "members", - Target = directoryObjectType, - TargetMultiplicity= EdmMultiplicity.Many - }); - - // add getChanges function - var recoveryChangeObjectCollectionType = new EdmCollectionTypeReference(new EdmCollectionType(new EdmEntityTypeReference(recoveryChangeObjectType,false))); - var getChangesFunction = new EdmFunction(graphNamespace, "getChanges", recoveryChangeObjectCollectionType, true, null, true); - getChangesFunction.AddParameter("recoveryPreviewJob", new EdmEntityTypeReference(recoveryPreviewJobType, false)); - - model.AddElement(getChangesFunction); - - // TODO: Temportary hack to provide navigation source when writing; get rid of this - entityContainer.AddEntitySet("recoveryChangeObjects", recoveryChangeObjectType); - model.SetAnnotationValue(getChangesFunction, new ReturnedEntitySetAnnotation("recoveryChangeObjects")); - - //jobs.EntityType.Function("getChanges").ReturnsCollectionFromEntitySet("recoveryChangeObjects"); - //jobs.EntityType.Function("getChanges").ReturnsCollectionViaEntitySetPath("getChanges"); - //jobs.EntityType.Function("getChanges").ReturnsCollection(); - - return model; - } -} diff --git a/sample/DeltaFunctions/Models/recoveryChangeObject.cs b/sample/DeltaFunctions/Models/recoveryChangeObject.cs deleted file mode 100644 index abd5a9604..000000000 --- a/sample/DeltaFunctions/Models/recoveryChangeObject.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Microsoft.OData.ModelBuilder; - -namespace microsoft.graph; - -/// -/// RecoveryChangeObject as entity -/// -public class recoveryChangeObject -{ - public string id { get; set; } - [AutoExpand] - public object currentState { get; set; } - [AutoExpand] - public object deltaFromCurrent { get; set; } -} - diff --git a/sample/DeltaFunctions/Models/recoveryPreviewJob.cs b/sample/DeltaFunctions/Models/recoveryPreviewJob.cs deleted file mode 100644 index 5a5515bd0..000000000 --- a/sample/DeltaFunctions/Models/recoveryPreviewJob.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace microsoft.graph; - -/// -/// RecoveryPreviewJob -/// -public class recoveryPreviewJob -{ - public string id { get; set; } -} diff --git a/sample/DeltaFunctions/Program.cs b/sample/DeltaFunctions/Program.cs deleted file mode 100644 index cadfbd7c1..000000000 --- a/sample/DeltaFunctions/Program.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Microsoft.AspNetCore.OData; -using microsoft.graph; - - -var builder = WebApplication.CreateBuilder(args); - -builder.Services.AddControllers(). - AddOData(opt => opt.EnableQueryFeatures() - .AddRouteComponents("", GraphModel.Model)); - -var app = builder.Build(); - -// Configure the HTTP request pipeline. - -app.UseODataRouteDebug(); - -app.UseAuthorization(); - -app.MapControllers(); - -app.Run(); diff --git a/sample/DeltaFunctions/Properties/launchSettings.json b/sample/DeltaFunctions/Properties/launchSettings.json deleted file mode 100644 index ffcad3ad8..000000000 --- a/sample/DeltaFunctions/Properties/launchSettings.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/launchsettings.json", - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:48226", - "sslPort": 0 - } - }, - "profiles": { - "ODataAlternateKeySample": { - "commandName": "Project", - "dotnetRunMessages": true, - "launchBrowser": true, - "launchUrl": "$odata", - "applicationUrl": "http://localhost:5219", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": true, - "launchUrl": "weatherforecast", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - } - } -} diff --git a/sample/DeltaFunctions/appsettings.Development.json b/sample/DeltaFunctions/appsettings.Development.json deleted file mode 100644 index 0c208ae91..000000000 --- a/sample/DeltaFunctions/appsettings.Development.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - } -} diff --git a/sample/DeltaFunctions/appsettings.json b/sample/DeltaFunctions/appsettings.json deleted file mode 100644 index 10f68b8c8..000000000 --- a/sample/DeltaFunctions/appsettings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - }, - "AllowedHosts": "*" -} diff --git a/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml b/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml index 2ec713bde..160691733 100644 --- a/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml +++ b/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml @@ -4507,6 +4507,72 @@ The collection value for which the annotations have to be added. The OData metadata level of the response. + + + ODataSerializer for serializing instances of /> + + + + + + + + + + + + + + Creates the that describes the set of properties and actions to select and expand while writing this entity. + + Contains the entity instance being written and the context. + + The that describes the set of properties and actions to select and expand while writing this entity. + + + + + Creates the to be written while writing this resource. + + The id of the Deleted Resource to be written (may be null if properties contains all key properties) + The for the removal of the resource. + The describing the response graph. + The context for the resource instance being written. + The created . + + + + Appends the dynamic properties of primitive, enum or the collection of them into the given . + If the dynamic property is a property of the complex or collection of complex, it will be saved into + the dynamic complex properties dictionary of and be written later. + + The describing the resource. + The describing the response graph. + The context for the resource instance being written. + + + + Creates the ETag for the given entity. + + The context for the resource instance being written. + The created ETag. + + + + Creates the to be written for the given resource. + + The computed property being written. + The context for the resource instance being written. + The to write. + + + + Creates the to be written for the given entity and the structural property. + + The EDM structural property being written. + The context for the entity instance being written. + The to write. + OData serializer for serializing a collection of @@ -4525,7 +4591,7 @@ - + Creates a function that takes in an object and generates nextlink uri. @@ -4553,6 +4619,16 @@ The to be used for writing. The . + + + Writes the given deltaDeletedEntry specified by the parameter graph as a part of an existing OData message using the given + messageWriter and the writeContext. + + The object to be written. + The expected type of the deleted resource. + The to be used for writing. + The . + Writes the given deltaDeletedLink specified by the parameter graph as a part of an existing OData message using the given @@ -5799,7 +5875,7 @@ Method to determine whether the current type is a Delta resource set. IEdmType to be compared - True or False if type is same as + True or False if type is a or a From b0505e017f5335f42fcc1e1b23e9ce117c234538 Mon Sep 17 00:00:00 2001 From: mikepizzo Date: Tue, 3 Dec 2024 13:11:17 -0800 Subject: [PATCH 4/9] Code Clean-up. Leverage more common functionality in ODataResourceSerializer. --- .../ODataDeletedResourceSerializer.cs | 216 +----------------- .../ODataDeltaResourceSetSerializer.cs | 2 +- .../Serialization/ODataResourceSerializer.cs | 53 +++-- 3 files changed, 53 insertions(+), 218 deletions(-) diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataDeletedResourceSerializer.cs b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataDeletedResourceSerializer.cs index a6efe2c5c..58e67271c 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataDeletedResourceSerializer.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataDeletedResourceSerializer.cs @@ -6,19 +6,13 @@ //------------------------------------------------------------------------------ using System; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; -using System.Linq; using System.Runtime.Serialization; -using Microsoft.AspNetCore.OData.Edm; -using Microsoft.AspNetCore.OData.Extensions; using Microsoft.AspNetCore.OData.Formatter.Value; using Microsoft.AspNetCore.OData.Routing; using Microsoft.OData; -using Microsoft.OData.ModelBuilder; using Microsoft.OData.Edm; -using Microsoft.OData.UriParser; using System.Threading.Tasks; using Microsoft.AspNetCore.OData.Deltas; @@ -128,9 +122,14 @@ private async Task WriteDeletedResourceAsync(object graph, ODataWriter writer, O throw new SerializationException(Error.Format(SRResources.CannotWriteType, GetType().Name, graph?.GetType().FullName)); } - bool isDelta = (graph is IDelta || graph is IEdmChangedObject); await writer.WriteStartAsync(odataDeletedResource).ConfigureAwait(false); - await new ODataResourceSerializer(SerializerProvider).WriteResourceContent(writer, selectExpandNode, resourceContext, isDelta); + ODataResourceSerializer serializer = SerializerProvider.GetEdmTypeSerializer(expectedType) as ODataResourceSerializer; + if (serializer == null) + { + throw new SerializationException( + Error.Format(SRResources.TypeCannotBeSerialized, expectedType.ToTraceString())); + } + await serializer.WriteResourceContent(writer, selectExpandNode, resourceContext, /*isDelta*/ true); await writer.WriteEndAsync().ConfigureAwait(false); } } @@ -144,25 +143,7 @@ private async Task WriteDeletedResourceAsync(object graph, ODataWriter writer, O /// public virtual SelectExpandNode CreateSelectExpandNode(ResourceContext resourceContext) { - if (resourceContext == null) - { - throw Error.ArgumentNull(nameof(resourceContext)); - } - - ODataSerializerContext writeContext = resourceContext.SerializerContext; - IEdmStructuredType structuredType = resourceContext.StructuredType; - - object selectExpandNode; - - Tuple key = Tuple.Create(writeContext.SelectExpandClause, structuredType); - if (!writeContext.Items.TryGetValue(key, out selectExpandNode)) - { - // cache the selectExpandNode so that if we are writing a feed we don't have to construct it again. - selectExpandNode = new SelectExpandNode(structuredType, writeContext); - writeContext.Items[key] = selectExpandNode; - } - - return selectExpandNode as SelectExpandNode; + return ODataResourceSerializer.CreateSelectExpandNodeInternal(resourceContext); } /// @@ -191,7 +172,7 @@ public virtual ODataDeletedResource CreateDeletedResource(Uri id, DeltaDeletedEn { Id = id ?? (resourceContext.NavigationSource == null ? null : resourceContext.GenerateSelfLink(false)), TypeName = typeName ?? "Edm.Untyped", - Properties = CreateStructuralPropertyBag(selectExpandNode, resourceContext), + Properties = ODataResourceSerializer.CreateStructuralPropertyBag(selectExpandNode, resourceContext, this.CreateStructuralProperty, this.CreateComputedProperty), Reason = reason }; @@ -222,10 +203,6 @@ public virtual ODataDeletedResource CreateDeletedResource(Uri id, DeltaDeletedEn public virtual void AppendDynamicProperties(ODataDeletedResource resource, SelectExpandNode selectExpandNode, ResourceContext resourceContext) { - Contract.Assert(resource != null); - Contract.Assert(selectExpandNode != null); - Contract.Assert(resourceContext != null); - ODataResourceSerializer.AppendDynamicPropertiesInternal(resource, selectExpandNode, resourceContext, SerializerProvider); } @@ -236,112 +213,7 @@ public virtual void AppendDynamicProperties(ODataDeletedResource resource, Selec /// The created ETag. public virtual string CreateETag(ResourceContext resourceContext) { - if (resourceContext == null) - { - throw Error.ArgumentNull(nameof(resourceContext)); - } - - if (resourceContext.Request != null) - { - IEdmModel model = resourceContext.EdmModel; - IEdmNavigationSource navigationSource = resourceContext.NavigationSource; - - IEnumerable concurrencyProperties; - if (model != null && navigationSource != null) - { - concurrencyProperties = model.GetConcurrencyProperties(navigationSource); - } - else - { - concurrencyProperties = Enumerable.Empty(); - } - - IDictionary properties = null; - foreach (IEdmStructuralProperty etagProperty in concurrencyProperties) - { - properties ??= new SortedDictionary(); - - properties.Add(etagProperty.Name, resourceContext.GetPropertyValue(etagProperty.Name)); - } - - if (properties != null) - { - return resourceContext.Request.CreateETag(properties, resourceContext.TimeZone); - } - } - - return null; - } - - //TODO: call method in ODataResourceSerializer - private IEnumerable CreateStructuralPropertyBag(SelectExpandNode selectExpandNode, ResourceContext resourceContext) - { - Contract.Assert(selectExpandNode != null); - Contract.Assert(resourceContext != null); - - int propertiesCount = (selectExpandNode.SelectedStructuralProperties?.Count ?? 0) + (selectExpandNode.SelectedComputedProperties?.Count ?? 0); - List properties = new List(propertiesCount); - - if (selectExpandNode.SelectedStructuralProperties != null) - { - IEnumerable structuralProperties = selectExpandNode.SelectedStructuralProperties; - - if (null != resourceContext.EdmObject && resourceContext.EdmObject.IsDeltaResource()) - { - IDelta deltaObject = null; - if (resourceContext.EdmObject is TypedEdmEntityObject obj) - { - deltaObject = obj.Instance as IDelta; - } - else - { - deltaObject = resourceContext.EdmObject as IDelta; - } - - if (deltaObject != null) - { - IEnumerable changedProperties = deltaObject.GetChangedPropertyNames(); - structuralProperties = structuralProperties.Where(p => changedProperties.Contains(p.Name) || p.IsKey()); - } - } - - foreach (IEdmStructuralProperty structuralProperty in structuralProperties) - { - if (structuralProperty.Type != null && structuralProperty.Type.IsStream()) - { - // skip the stream property, the stream property is written in its own logic - continue; - } - - if (structuralProperty.Type != null && - (structuralProperty.Type.IsUntyped() || structuralProperty.Type.IsCollectionUntyped())) - { - // skip it here, we use a different method to write all 'declared' untyped properties - continue; - } - - ODataProperty property = CreateStructuralProperty(structuralProperty, resourceContext); - if (property != null) - { - properties.Add(property); - } - } - } - - // Try to add computed properties - if (selectExpandNode.SelectedComputedProperties != null) - { - foreach (string propertyName in selectExpandNode.SelectedComputedProperties) - { - ODataProperty property = CreateComputedProperty(propertyName, resourceContext); - if (property != null) - { - properties.Add(property); - } - } - } - - return properties; + return ODataResourceSerializer.CreateETagInternal(resourceContext); } /// @@ -352,38 +224,7 @@ private IEnumerable CreateStructuralPropertyBag(SelectExpandNode /// The to write. public virtual ODataProperty CreateComputedProperty(string propertyName, ResourceContext resourceContext) { - if (string.IsNullOrWhiteSpace(propertyName)) - { - throw Error.ArgumentNullOrEmpty(nameof(propertyName)); - } - - if (resourceContext == null) - { - throw Error.ArgumentNull(nameof(resourceContext)); - } - - // The computed value is from the Linq expression binding. - object propertyValue = resourceContext.GetPropertyValue(propertyName); - if (propertyValue == null) - { - return new ODataProperty { Name = propertyName, Value = null }; - } - - ODataSerializerContext writeContext = resourceContext.SerializerContext; - - IEdmTypeReference edmTypeReference = resourceContext.SerializerContext.GetEdmType(propertyValue, propertyValue.GetType()); - if (edmTypeReference == null) - { - throw Error.NotSupported(SRResources.TypeOfDynamicPropertyNotSupported, propertyValue.GetType().FullName, propertyName); - } - - IODataEdmTypeSerializer serializer = SerializerProvider.GetEdmTypeSerializer(edmTypeReference); - if (serializer == null) - { - throw new SerializationException(Error.Format(SRResources.TypeCannotBeSerialized, edmTypeReference.FullName())); - } - - return serializer.CreateProperty(propertyValue, edmTypeReference, propertyName, writeContext); + return ODataResourceSerializer.CreateComputedPropertyInternal(propertyName, resourceContext, SerializerProvider); } /// @@ -394,39 +235,6 @@ public virtual ODataProperty CreateComputedProperty(string propertyName, Resourc /// The to write. public virtual ODataProperty CreateStructuralProperty(IEdmStructuralProperty structuralProperty, ResourceContext resourceContext) { - if (structuralProperty == null) - { - throw Error.ArgumentNull(nameof(structuralProperty)); - } - if (resourceContext == null) - { - throw Error.ArgumentNull(nameof(resourceContext)); - } - - ODataSerializerContext writeContext = resourceContext.SerializerContext; - - IODataEdmTypeSerializer serializer = SerializerProvider.GetEdmTypeSerializer(structuralProperty.Type); - if (serializer == null) - { - throw new SerializationException( - Error.Format(SRResources.TypeCannotBeSerialized, structuralProperty.Type.FullName())); - } - - object propertyValue = resourceContext.GetPropertyValue(structuralProperty.Name); - - IEdmTypeReference propertyType = structuralProperty.Type; - if (propertyValue != null) - { - if (!propertyType.IsPrimitive() && !propertyType.IsEnum()) - { - IEdmTypeReference actualType = writeContext.GetEdmType(propertyValue, propertyValue.GetType()); - if (propertyType != null && propertyType != actualType) - { - propertyType = actualType; - } - } - } - - return serializer.CreateProperty(propertyValue, propertyType, structuralProperty.Name, writeContext); + return ODataResourceSerializer.CreateStructuralPropertyInternal(structuralProperty, resourceContext, SerializerProvider); } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataDeltaResourceSetSerializer.cs b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataDeltaResourceSetSerializer.cs index 55112ca2f..9f37e2adf 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataDeltaResourceSetSerializer.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataDeltaResourceSetSerializer.cs @@ -326,7 +326,7 @@ public virtual async Task WriteDeletedResourceAsync(object value, IEdmStructured throw Error.ArgumentNull(nameof(writer)); } - //todo:use serializer provider + //TODO:in future, use SerializerProvider -- requires differentiating between resource serializer and deleted resource serializer for same EdmType ODataDeletedResourceSerializer deletedResourceSerializer = new ODataDeletedResourceSerializer(SerializerProvider); await deletedResourceSerializer.WriteObjectInlineAsync(value, expectedType, writer, writeContext); diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataResourceSerializer.cs b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataResourceSerializer.cs index 4f6e48ef5..83ceeb8d5 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataResourceSerializer.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataResourceSerializer.cs @@ -221,7 +221,14 @@ await writer.WriteStartAsync(new ODataResourceSet if (IsDeltaCollection(propertyValue)) { ODataEdmTypeSerializer serializer = new ODataDeltaResourceSetSerializer(SerializerProvider); - EdmCollectionTypeReference edmType = new EdmCollectionTypeReference(new EdmDeltaCollectionType(new EdmEntityTypeReference(edmProperty.Type.GetElementType() as IEdmEntityType, false))); + IEdmEntityType itemType = edmProperty.Type.GetElementType() as IEdmEntityType; + if(itemType == null) + { + throw new SerializationException( + Error.Format(SRResources.TypeCannotBeSerialized, edmProperty.Type.ToTraceString())); + } + + EdmCollectionTypeReference edmType = new EdmCollectionTypeReference(new EdmDeltaCollectionType(new EdmEntityTypeReference(itemType, false))); await serializer.WriteObjectInlineAsync( propertyValue, edmType, @@ -503,6 +510,11 @@ internal async Task WriteResourceContent(ODataWriter writer, SelectExpandNode se /// The that describes the set of properties and actions to select and expand while writing this entity. /// public virtual SelectExpandNode CreateSelectExpandNode(ResourceContext resourceContext) + { + return CreateSelectExpandNodeInternal(resourceContext); + } + + internal static SelectExpandNode CreateSelectExpandNodeInternal(ResourceContext resourceContext) { if (resourceContext == null) { @@ -556,7 +568,7 @@ public virtual ODataResource CreateResource(SelectExpandNode selectExpandNode, R ODataResource resource = new ODataResource { TypeName = typeName ?? "Edm.Untyped", - Properties = CreateStructuralPropertyBag(selectExpandNode, resourceContext), + Properties = CreateStructuralPropertyBag(selectExpandNode, resourceContext, this.CreateStructuralProperty, this.CreateComputedProperty), }; InitializeODataResource(selectExpandNode, resource, resourceContext); @@ -666,16 +678,16 @@ internal static void InitializeODataResource(SelectExpandNode selectExpandNode, public virtual void AppendDynamicProperties(ODataResource resource, SelectExpandNode selectExpandNode, ResourceContext resourceContext) { - Contract.Assert(resource != null); - Contract.Assert(selectExpandNode != null); - Contract.Assert(resourceContext != null); - AppendDynamicPropertiesInternal(resource,selectExpandNode, resourceContext, SerializerProvider); } internal static void AppendDynamicPropertiesInternal(ODataResourceBase resource, SelectExpandNode selectExpandNode, ResourceContext resourceContext, IODataSerializerProvider serializerProvider) { + Contract.Assert(resource != null); + Contract.Assert(selectExpandNode != null); + Contract.Assert(resourceContext != null); + if (!resourceContext.StructuredType.IsOpen || // non-open type (!selectExpandNode.SelectAllDynamicProperties && selectExpandNode.SelectedDynamicProperties == null)) { @@ -794,6 +806,11 @@ internal static void AppendDynamicPropertiesInternal(ODataResourceBase resource, /// The context for the resource instance being written. /// The created ETag. public virtual string CreateETag(ResourceContext resourceContext) + { + return CreateETagInternal(resourceContext); + } + + internal static string CreateETagInternal(ResourceContext resourceContext) { if (resourceContext == null) { @@ -819,7 +836,7 @@ public virtual string CreateETag(ResourceContext resourceContext) foreach (IEdmStructuralProperty etagProperty in concurrencyProperties) { properties ??= new SortedDictionary(); - + properties.Add(etagProperty.Name, resourceContext.GetPropertyValue(etagProperty.Name)); } @@ -832,7 +849,6 @@ public virtual string CreateETag(ResourceContext resourceContext) return null; } - /// /// Write the navigation link for the select navigation properties. /// @@ -1287,7 +1303,8 @@ public virtual ODataNestedResourceInfo CreateNavigationLink(IEdmNavigationProper return navigationLink; } - internal IEnumerable CreateStructuralPropertyBag(SelectExpandNode selectExpandNode, ResourceContext resourceContext) + internal static IEnumerable CreateStructuralPropertyBag(SelectExpandNode selectExpandNode, ResourceContext resourceContext, + Func createStructuralProperty, Func createComputedProperty) { Contract.Assert(selectExpandNode != null); Contract.Assert(resourceContext != null); @@ -1333,7 +1350,7 @@ internal IEnumerable CreateStructuralPropertyBag(SelectExpandNode continue; } - ODataProperty property = CreateStructuralProperty(structuralProperty, resourceContext); + ODataProperty property = createStructuralProperty(structuralProperty, resourceContext); if (property != null) { properties.Add(property); @@ -1346,7 +1363,7 @@ internal IEnumerable CreateStructuralPropertyBag(SelectExpandNode { foreach (string propertyName in selectExpandNode.SelectedComputedProperties) { - ODataProperty property = CreateComputedProperty(propertyName, resourceContext); + ODataProperty property = createComputedProperty(propertyName, resourceContext); if (property != null) { properties.Add(property); @@ -1364,6 +1381,11 @@ internal IEnumerable CreateStructuralPropertyBag(SelectExpandNode /// The context for the resource instance being written. /// The to write. public virtual ODataProperty CreateComputedProperty(string propertyName, ResourceContext resourceContext) + { + return CreateComputedPropertyInternal(propertyName, resourceContext, SerializerProvider); + } + + internal static ODataProperty CreateComputedPropertyInternal(string propertyName, ResourceContext resourceContext, IODataSerializerProvider serializerProvider) { if (string.IsNullOrWhiteSpace(propertyName)) { @@ -1390,7 +1412,7 @@ public virtual ODataProperty CreateComputedProperty(string propertyName, Resourc throw Error.NotSupported(SRResources.TypeOfDynamicPropertyNotSupported, propertyValue.GetType().FullName, propertyName); } - IODataEdmTypeSerializer serializer = SerializerProvider.GetEdmTypeSerializer(edmTypeReference); + IODataEdmTypeSerializer serializer = serializerProvider.GetEdmTypeSerializer(edmTypeReference); if (serializer == null) { throw new SerializationException(Error.Format(SRResources.TypeCannotBeSerialized, edmTypeReference.FullName())); @@ -1522,6 +1544,11 @@ public virtual object CreateUntypedPropertyValue(IEdmStructuralProperty structur /// The context for the entity instance being written. /// The to write. public virtual ODataProperty CreateStructuralProperty(IEdmStructuralProperty structuralProperty, ResourceContext resourceContext) + { + return CreateStructuralPropertyInternal(structuralProperty, resourceContext, SerializerProvider); + } + + internal static ODataProperty CreateStructuralPropertyInternal(IEdmStructuralProperty structuralProperty, ResourceContext resourceContext, IODataSerializerProvider serializerProvider) { if (structuralProperty == null) { @@ -1534,7 +1561,7 @@ public virtual ODataProperty CreateStructuralProperty(IEdmStructuralProperty str ODataSerializerContext writeContext = resourceContext.SerializerContext; - IODataEdmTypeSerializer serializer = SerializerProvider.GetEdmTypeSerializer(structuralProperty.Type); + IODataEdmTypeSerializer serializer = serializerProvider.GetEdmTypeSerializer(structuralProperty.Type); if (serializer == null) { throw new SerializationException( From a50d2c7ed54074b076cea51e779023aea6a98f8d Mon Sep 17 00:00:00 2001 From: mikepizzo Date: Tue, 3 Dec 2024 16:27:50 -0800 Subject: [PATCH 5/9] Apply review feedback Change ODataDeletedResourceSerializer to derive from ODataResourceSerializer --- .../ODataDeletedResourceSerializer.cs | 77 ++--------------- .../Serialization/ODataResourceSerializer.cs | 83 +++++++++++-------- .../Microsoft.AspNetCore.OData.xml | 62 +++++--------- .../PublicAPI.Unshipped.txt | 4 + ...rosoft.AspNetCore.OData.PublicApi.Net8.bsl | 15 ++-- 5 files changed, 86 insertions(+), 155 deletions(-) diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataDeletedResourceSerializer.cs b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataDeletedResourceSerializer.cs index 58e67271c..392575f23 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataDeletedResourceSerializer.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataDeletedResourceSerializer.cs @@ -22,13 +22,13 @@ namespace Microsoft.AspNetCore.OData.Formatter.Serialization; /// ODataSerializer for serializing instances of /> /// [SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Justification = "Relies on many ODataLib classes.")] -public class ODataDeletedResourceSerializer : ODataEdmTypeSerializer +public class ODataDeletedResourceSerializer : ODataResourceSerializer { private const string Resource = "DeletedResource"; /// public ODataDeletedResourceSerializer(IODataSerializerProvider serializerProvider) - : base(ODataPayloadKind.Resource, serializerProvider) + : base(serializerProvider) { } @@ -85,13 +85,6 @@ private async Task WriteDeletedResourceAsync(object graph, ODataWriter writer, O { Contract.Assert(writeContext != null); - //TODO: do we need this? - //if (graph.GetType().IsDynamicTypeWrapper()) - //{ - // await new ODataResourceSerializer(SerializerProvider).WriteDynamicTypeResourceAsync(graph, writer, expectedType, writeContext).ConfigureAwait(false); - // return; - //} - IEdmStructuredTypeReference structuredType = ODataResourceSerializer.GetResourceType(graph, writeContext); ResourceContext resourceContext = new ResourceContext(writeContext, structuredType, graph); @@ -134,18 +127,6 @@ private async Task WriteDeletedResourceAsync(object graph, ODataWriter writer, O } } - /// - /// Creates the that describes the set of properties and actions to select and expand while writing this entity. - /// - /// Contains the entity instance being written and the context. - /// - /// The that describes the set of properties and actions to select and expand while writing this entity. - /// - public virtual SelectExpandNode CreateSelectExpandNode(ResourceContext resourceContext) - { - return ODataResourceSerializer.CreateSelectExpandNodeInternal(resourceContext); - } - /// /// Creates the to be written while writing this resource. /// @@ -172,11 +153,11 @@ public virtual ODataDeletedResource CreateDeletedResource(Uri id, DeltaDeletedEn { Id = id ?? (resourceContext.NavigationSource == null ? null : resourceContext.GenerateSelfLink(false)), TypeName = typeName ?? "Edm.Untyped", - Properties = ODataResourceSerializer.CreateStructuralPropertyBag(selectExpandNode, resourceContext, this.CreateStructuralProperty, this.CreateComputedProperty), + Properties = CreateStructuralPropertyBag(selectExpandNode, resourceContext), Reason = reason }; - ODataResourceSerializer.InitializeODataResource(selectExpandNode, resource, resourceContext); + InitializeODataResource(selectExpandNode, resource, resourceContext); string etag = CreateETag(resourceContext); if (etag != null) @@ -185,56 +166,8 @@ public virtual ODataDeletedResource CreateDeletedResource(Uri id, DeltaDeletedEn } // Try to add the dynamic properties if the structural type is open. - AppendDynamicProperties(resource, selectExpandNode, resourceContext); + AppendDynamicPropertiesInternal(resource, selectExpandNode, resourceContext); return resource; } - - /// - /// Appends the dynamic properties of primitive, enum or the collection of them into the given . - /// If the dynamic property is a property of the complex or collection of complex, it will be saved into - /// the dynamic complex properties dictionary of and be written later. - /// - /// The describing the resource. - /// The describing the response graph. - /// The context for the resource instance being written. - [SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Justification = "Relies on many classes.")] - [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Justification = "These are simple conversion function and cannot be split up.")] - public virtual void AppendDynamicProperties(ODataDeletedResource resource, SelectExpandNode selectExpandNode, - ResourceContext resourceContext) - { - ODataResourceSerializer.AppendDynamicPropertiesInternal(resource, selectExpandNode, resourceContext, SerializerProvider); - } - - /// - /// Creates the ETag for the given entity. - /// - /// The context for the resource instance being written. - /// The created ETag. - public virtual string CreateETag(ResourceContext resourceContext) - { - return ODataResourceSerializer.CreateETagInternal(resourceContext); - } - - /// - /// Creates the to be written for the given resource. - /// - /// The computed property being written. - /// The context for the resource instance being written. - /// The to write. - public virtual ODataProperty CreateComputedProperty(string propertyName, ResourceContext resourceContext) - { - return ODataResourceSerializer.CreateComputedPropertyInternal(propertyName, resourceContext, SerializerProvider); - } - - /// - /// Creates the to be written for the given entity and the structural property. - /// - /// The EDM structural property being written. - /// The context for the entity instance being written. - /// The to write. - public virtual ODataProperty CreateStructuralProperty(IEdmStructuralProperty structuralProperty, ResourceContext resourceContext) - { - return ODataResourceSerializer.CreateStructuralPropertyInternal(structuralProperty, resourceContext, SerializerProvider); - } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataResourceSerializer.cs b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataResourceSerializer.cs index 83ceeb8d5..72d8a2ffc 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataResourceSerializer.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataResourceSerializer.cs @@ -237,7 +237,13 @@ await serializer.WriteObjectInlineAsync( } else { - ODataEdmTypeSerializer serializer = new ODataResourceSetSerializer(SerializerProvider); + IODataEdmTypeSerializer serializer = SerializerProvider.GetEdmTypeSerializer(edmProperty.Type); + if (serializer == null) + { + throw new SerializationException( + Error.Format(SRResources.TypeCannotBeSerialized, edmProperty.Type.ToTraceString())); + } + await serializer.WriteObjectInlineAsync(propertyValue, edmProperty.Type, writer, nestedWriteContext).ConfigureAwait(false); } } @@ -256,7 +262,7 @@ await serializer.WriteObjectInlineAsync( /// The resource context for the resource being written. /// The ODataWriter. /// A task that represents the asynchronous write operation - internal async Task WriteDeltaNavigationPropertiesAsync(SelectExpandNode selectExpandNode, ResourceContext resourceContext, ODataWriter writer) + private async Task WriteDeltaNavigationPropertiesAsync(SelectExpandNode selectExpandNode, ResourceContext resourceContext, ODataWriter writer) { Contract.Assert(resourceContext != null, "The ResourceContext cannot be null"); Contract.Assert(writer != null, "The ODataWriter cannot be null"); @@ -477,7 +483,14 @@ await writer.WriteEntityReferenceLinkAsync(new ODataEntityReferenceLink writeContext.NavigationSource = originalNavigationSource; } - internal async Task WriteResourceContent(ODataWriter writer, SelectExpandNode selectExpandNode, ResourceContext resourceContext, bool isDelta) + /// + /// Writes the context of a Resource + /// + /// The to use to write the resource contents + /// The describing the response graph. + /// The context for the resource instance being written. + /// Whether to only write changed properties of the resource + protected internal async Task WriteResourceContent(ODataWriter writer, SelectExpandNode selectExpandNode, ResourceContext resourceContext, bool isDelta) { // TODO: These should be aligned; do we need different methods for delta versus non-delta complex/navigation properties? if (isDelta) @@ -510,11 +523,6 @@ internal async Task WriteResourceContent(ODataWriter writer, SelectExpandNode se /// The that describes the set of properties and actions to select and expand while writing this entity. /// public virtual SelectExpandNode CreateSelectExpandNode(ResourceContext resourceContext) - { - return CreateSelectExpandNodeInternal(resourceContext); - } - - internal static SelectExpandNode CreateSelectExpandNodeInternal(ResourceContext resourceContext) { if (resourceContext == null) { @@ -568,7 +576,7 @@ public virtual ODataResource CreateResource(SelectExpandNode selectExpandNode, R ODataResource resource = new ODataResource { TypeName = typeName ?? "Edm.Untyped", - Properties = CreateStructuralPropertyBag(selectExpandNode, resourceContext, this.CreateStructuralProperty, this.CreateComputedProperty), + Properties = CreateStructuralPropertyBag(selectExpandNode, resourceContext), }; InitializeODataResource(selectExpandNode, resource, resourceContext); @@ -603,7 +611,13 @@ public virtual ODataResource CreateResource(SelectExpandNode selectExpandNode, R return resource; } - internal static void InitializeODataResource(SelectExpandNode selectExpandNode, ODataResourceBase resource, ResourceContext resourceContext) + /// + /// Initializes an ODataResource to be written + /// + /// The describing the response graph. + /// The resource that will be initialized + /// The context for the resource instance being written. + protected internal void InitializeODataResource(SelectExpandNode selectExpandNode, ODataResourceBase resource, ResourceContext resourceContext) { if ((resourceContext.EdmObject is EdmDeltaResourceObject || resourceContext.EdmObject is IEdmDeltaDeletedResourceObject) && resourceContext.NavigationSource != null) { @@ -678,11 +692,19 @@ internal static void InitializeODataResource(SelectExpandNode selectExpandNode, public virtual void AppendDynamicProperties(ODataResource resource, SelectExpandNode selectExpandNode, ResourceContext resourceContext) { - AppendDynamicPropertiesInternal(resource,selectExpandNode, resourceContext, SerializerProvider); + AppendDynamicPropertiesInternal(resource, selectExpandNode, resourceContext); } - internal static void AppendDynamicPropertiesInternal(ODataResourceBase resource, SelectExpandNode selectExpandNode, - ResourceContext resourceContext, IODataSerializerProvider serializerProvider) + /// + /// Appends the dynamic properties of primitive, enum or the collection of them into the given . + /// If the dynamic property is a property of the complex or collection of complex, it will be saved into + /// the dynamic complex properties dictionary of and be written later. + /// + /// The describing the resource. + /// The describing the response graph. + /// The context for the resource instance being written. + protected internal void AppendDynamicPropertiesInternal(ODataResourceBase resource, SelectExpandNode selectExpandNode, + ResourceContext resourceContext) { Contract.Assert(resource != null); Contract.Assert(selectExpandNode != null); @@ -782,7 +804,7 @@ internal static void AppendDynamicPropertiesInternal(ODataResourceBase resource, } else { - IODataEdmTypeSerializer propertySerializer = serializerProvider.GetEdmTypeSerializer(edmTypeReference); + IODataEdmTypeSerializer propertySerializer =SerializerProvider.GetEdmTypeSerializer(edmTypeReference); if (propertySerializer == null) { throw Error.NotSupported(SRResources.DynamicPropertyCannotBeSerialized, dynamicProperty.Key, @@ -806,11 +828,6 @@ internal static void AppendDynamicPropertiesInternal(ODataResourceBase resource, /// The context for the resource instance being written. /// The created ETag. public virtual string CreateETag(ResourceContext resourceContext) - { - return CreateETagInternal(resourceContext); - } - - internal static string CreateETagInternal(ResourceContext resourceContext) { if (resourceContext == null) { @@ -1303,8 +1320,14 @@ public virtual ODataNestedResourceInfo CreateNavigationLink(IEdmNavigationProper return navigationLink; } - internal static IEnumerable CreateStructuralPropertyBag(SelectExpandNode selectExpandNode, ResourceContext resourceContext, - Func createStructuralProperty, Func createComputedProperty) + /// + /// Creates the s to be written while writing this entity. + /// + /// The to determine the properties to be written + /// The context for the entity instance being written. + /// The navigation link to be written. + /// ODataProperties to be written + protected internal IEnumerable CreateStructuralPropertyBag(SelectExpandNode selectExpandNode, ResourceContext resourceContext) { Contract.Assert(selectExpandNode != null); Contract.Assert(resourceContext != null); @@ -1350,7 +1373,7 @@ internal static IEnumerable CreateStructuralPropertyBag(SelectExp continue; } - ODataProperty property = createStructuralProperty(structuralProperty, resourceContext); + ODataProperty property = CreateStructuralProperty(structuralProperty, resourceContext); if (property != null) { properties.Add(property); @@ -1363,7 +1386,7 @@ internal static IEnumerable CreateStructuralPropertyBag(SelectExp { foreach (string propertyName in selectExpandNode.SelectedComputedProperties) { - ODataProperty property = createComputedProperty(propertyName, resourceContext); + ODataProperty property = CreateComputedProperty(propertyName, resourceContext); if (property != null) { properties.Add(property); @@ -1381,11 +1404,6 @@ internal static IEnumerable CreateStructuralPropertyBag(SelectExp /// The context for the resource instance being written. /// The to write. public virtual ODataProperty CreateComputedProperty(string propertyName, ResourceContext resourceContext) - { - return CreateComputedPropertyInternal(propertyName, resourceContext, SerializerProvider); - } - - internal static ODataProperty CreateComputedPropertyInternal(string propertyName, ResourceContext resourceContext, IODataSerializerProvider serializerProvider) { if (string.IsNullOrWhiteSpace(propertyName)) { @@ -1412,7 +1430,7 @@ internal static ODataProperty CreateComputedPropertyInternal(string propertyName throw Error.NotSupported(SRResources.TypeOfDynamicPropertyNotSupported, propertyValue.GetType().FullName, propertyName); } - IODataEdmTypeSerializer serializer = serializerProvider.GetEdmTypeSerializer(edmTypeReference); + IODataEdmTypeSerializer serializer = SerializerProvider.GetEdmTypeSerializer(edmTypeReference); if (serializer == null) { throw new SerializationException(Error.Format(SRResources.TypeCannotBeSerialized, edmTypeReference.FullName())); @@ -1544,11 +1562,6 @@ public virtual object CreateUntypedPropertyValue(IEdmStructuralProperty structur /// The context for the entity instance being written. /// The to write. public virtual ODataProperty CreateStructuralProperty(IEdmStructuralProperty structuralProperty, ResourceContext resourceContext) - { - return CreateStructuralPropertyInternal(structuralProperty, resourceContext, SerializerProvider); - } - - internal static ODataProperty CreateStructuralPropertyInternal(IEdmStructuralProperty structuralProperty, ResourceContext resourceContext, IODataSerializerProvider serializerProvider) { if (structuralProperty == null) { @@ -1561,7 +1574,7 @@ internal static ODataProperty CreateStructuralPropertyInternal(IEdmStructuralPro ODataSerializerContext writeContext = resourceContext.SerializerContext; - IODataEdmTypeSerializer serializer = serializerProvider.GetEdmTypeSerializer(structuralProperty.Type); + IODataEdmTypeSerializer serializer = SerializerProvider.GetEdmTypeSerializer(structuralProperty.Type); if (serializer == null) { throw new SerializationException( diff --git a/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml b/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml index 160691733..ea13def7d 100644 --- a/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml +++ b/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml @@ -4521,15 +4521,6 @@ - - - Creates the that describes the set of properties and actions to select and expand while writing this entity. - - Contains the entity instance being written and the context. - - The that describes the set of properties and actions to select and expand while writing this entity. - - Creates the to be written while writing this resource. @@ -4540,39 +4531,6 @@ The context for the resource instance being written. The created . - - - Appends the dynamic properties of primitive, enum or the collection of them into the given . - If the dynamic property is a property of the complex or collection of complex, it will be saved into - the dynamic complex properties dictionary of and be written later. - - The describing the resource. - The describing the response graph. - The context for the resource instance being written. - - - - Creates the ETag for the given entity. - - The context for the resource instance being written. - The created ETag. - - - - Creates the to be written for the given resource. - - The computed property being written. - The context for the resource instance being written. - The to write. - - - - Creates the to be written for the given entity and the structural property. - - The EDM structural property being written. - The context for the entity instance being written. - The to write. - OData serializer for serializing a collection of @@ -4840,6 +4798,7 @@ The ODataWriter. A task that represents the asynchronous write operation + Creates the that describes the set of properties and actions to select and expand while writing this entity. @@ -4857,6 +4816,14 @@ The context for the resource instance being written. The created . + + + Initializes an ODataResource to be written + + The describing the response graph. + The resource that will be initialized + The context for the resource instance being written. + Appends the dynamic properties of primitive, enum or the collection of them into the given . @@ -4867,6 +4834,16 @@ The describing the response graph. The context for the resource instance being written. + + + Appends the dynamic properties of primitive, enum or the collection of them into the given . + If the dynamic property is a property of the complex or collection of complex, it will be saved into + the dynamic complex properties dictionary of and be written later. + + The describing the resource. + The describing the response graph. + The context for the resource instance being written. + Creates the ETag for the given entity. @@ -4933,6 +4910,7 @@ The context for the entity instance being written. The navigation link to be written. + Creates the to be written for the given resource. diff --git a/src/Microsoft.AspNetCore.OData/PublicAPI.Unshipped.txt b/src/Microsoft.AspNetCore.OData/PublicAPI.Unshipped.txt index 16988f248..ae1569e03 100644 --- a/src/Microsoft.AspNetCore.OData/PublicAPI.Unshipped.txt +++ b/src/Microsoft.AspNetCore.OData/PublicAPI.Unshipped.txt @@ -1,5 +1,9 @@ Microsoft.AspNetCore.OData.Formatter.Serialization.ODataDeletedResourceSerializer Microsoft.AspNetCore.OData.Formatter.Serialization.ODataDeletedResourceSerializer.ODataDeletedResourceSerializer(Microsoft.AspNetCore.OData.Formatter.Serialization.IODataSerializerProvider serializerProvider) -> void +Microsoft.AspNetCore.OData.Formatter.Serialization.ODataResourceSerializer.AppendDynamicPropertiesInternal(Microsoft.OData.ODataResourceBase resource, Microsoft.AspNetCore.OData.Formatter.Serialization.SelectExpandNode selectExpandNode, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) -> void +Microsoft.AspNetCore.OData.Formatter.Serialization.ODataResourceSerializer.CreateStructuralPropertyBag(Microsoft.AspNetCore.OData.Formatter.Serialization.SelectExpandNode selectExpandNode, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext, System.Func createStructuralProperty, System.Func createComputedProperty) -> System.Collections.Generic.IEnumerable +Microsoft.AspNetCore.OData.Formatter.Serialization.ODataResourceSerializer.InitializeODataResource(Microsoft.AspNetCore.OData.Formatter.Serialization.SelectExpandNode selectExpandNode, Microsoft.OData.ODataResourceBase resource, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) -> void +Microsoft.AspNetCore.OData.Formatter.Serialization.ODataResourceSerializer.WriteResourceContent(Microsoft.OData.ODataWriter writer, Microsoft.AspNetCore.OData.Formatter.Serialization.SelectExpandNode selectExpandNode, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext, bool isDelta) -> System.Threading.Tasks.Task override Microsoft.AspNetCore.OData.Formatter.Serialization.ODataDeletedResourceSerializer.WriteObjectAsync(object graph, System.Type type, Microsoft.OData.ODataMessageWriter messageWriter, Microsoft.AspNetCore.OData.Formatter.Serialization.ODataSerializerContext writeContext) -> System.Threading.Tasks.Task override Microsoft.AspNetCore.OData.Formatter.Serialization.ODataDeletedResourceSerializer.WriteObjectInlineAsync(object graph, Microsoft.OData.Edm.IEdmTypeReference expectedType, Microsoft.OData.ODataWriter writer, Microsoft.AspNetCore.OData.Formatter.Serialization.ODataSerializerContext writeContext) -> System.Threading.Tasks.Task virtual Microsoft.AspNetCore.OData.Formatter.Serialization.ODataDeletedResourceSerializer.AppendDynamicProperties(Microsoft.OData.ODataDeletedResource resource, Microsoft.AspNetCore.OData.Formatter.Serialization.SelectExpandNode selectExpandNode, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) -> void diff --git a/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.Net8.bsl b/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.Net8.bsl index 1b34a3a4b..c518305aa 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.Net8.bsl +++ b/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.Net8.bsl @@ -2166,15 +2166,10 @@ public class Microsoft.AspNetCore.OData.Formatter.Serialization.ODataCollectionS public virtual System.Threading.Tasks.Task WriteObjectAsync (object graph, System.Type type, Microsoft.OData.ODataMessageWriter messageWriter, Microsoft.AspNetCore.OData.Formatter.Serialization.ODataSerializerContext writeContext) } -public class Microsoft.AspNetCore.OData.Formatter.Serialization.ODataDeletedResourceSerializer : Microsoft.AspNetCore.OData.Formatter.Serialization.ODataEdmTypeSerializer, IODataEdmTypeSerializer, IODataSerializer { +public class Microsoft.AspNetCore.OData.Formatter.Serialization.ODataDeletedResourceSerializer : Microsoft.AspNetCore.OData.Formatter.Serialization.ODataResourceSerializer, IODataEdmTypeSerializer, IODataSerializer { public ODataDeletedResourceSerializer (Microsoft.AspNetCore.OData.Formatter.Serialization.IODataSerializerProvider serializerProvider) - public virtual void AppendDynamicProperties (Microsoft.OData.ODataDeletedResource resource, Microsoft.AspNetCore.OData.Formatter.Serialization.SelectExpandNode selectExpandNode, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) - public virtual Microsoft.OData.ODataProperty CreateComputedProperty (string propertyName, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) public virtual Microsoft.OData.ODataDeletedResource CreateDeletedResource (System.Uri id, Microsoft.OData.DeltaDeletedEntryReason reason, Microsoft.AspNetCore.OData.Formatter.Serialization.SelectExpandNode selectExpandNode, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) - public virtual string CreateETag (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.ODataProperty CreateStructuralProperty (Microsoft.OData.Edm.IEdmStructuralProperty structuralProperty, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) [ AsyncStateMachineAttribute(), ] @@ -2293,6 +2288,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) + protected void AppendDynamicPropertiesInternal (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) @@ -2304,8 +2300,10 @@ public class Microsoft.AspNetCore.OData.Formatter.Serialization.ODataResourceSer 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) public virtual Microsoft.OData.ODataProperty CreateStructuralProperty (Microsoft.OData.Edm.IEdmStructuralProperty structuralProperty, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) + protected System.Collections.Generic.IEnumerable`1[[Microsoft.OData.ODataProperty]] CreateStructuralPropertyBag (Microsoft.AspNetCore.OData.Formatter.Serialization.SelectExpandNode selectExpandNode, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) public virtual Microsoft.OData.ODataNestedResourceInfo CreateUntypedNestedResourceInfo (Microsoft.OData.Edm.IEdmStructuralProperty structuralProperty, object propertyValue, Microsoft.OData.Edm.IEdmTypeReference valueType, Microsoft.OData.UriParser.PathSelectItem pathSelectItem, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) public virtual object CreateUntypedPropertyValue (Microsoft.OData.Edm.IEdmStructuralProperty structuralProperty, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext, out Microsoft.OData.Edm.IEdmTypeReference& actualType) + protected void InitializeODataResource (Microsoft.AspNetCore.OData.Formatter.Serialization.SelectExpandNode selectExpandNode, Microsoft.OData.ODataResourceBase resource, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) protected virtual bool ShouldWriteNavigation (Microsoft.OData.ODataNestedResourceInfo navigationLink, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) [ AsyncStateMachineAttribute(), @@ -2321,6 +2319,11 @@ public class Microsoft.AspNetCore.OData.Formatter.Serialization.ODataResourceSer AsyncStateMachineAttribute(), ] public virtual System.Threading.Tasks.Task WriteObjectInlineAsync (object graph, Microsoft.OData.Edm.IEdmTypeReference expectedType, Microsoft.OData.ODataWriter writer, Microsoft.AspNetCore.OData.Formatter.Serialization.ODataSerializerContext writeContext) + + [ + AsyncStateMachineAttribute(), + ] + protected System.Threading.Tasks.Task WriteResourceContent (Microsoft.OData.ODataWriter writer, Microsoft.AspNetCore.OData.Formatter.Serialization.SelectExpandNode selectExpandNode, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext, bool isDelta) } public class Microsoft.AspNetCore.OData.Formatter.Serialization.ODataResourceSetSerializer : Microsoft.AspNetCore.OData.Formatter.Serialization.ODataEdmTypeSerializer, IODataEdmTypeSerializer, IODataSerializer { From d9e78b865d49ccdef59fb9fb44d83fb472dcafec Mon Sep 17 00:00:00 2001 From: mikepizzo Date: Thu, 5 Dec 2024 08:54:51 -0800 Subject: [PATCH 6/9] Preserve setting of OmitODataPrefix --- .../Formatter/ODataOutputFormatterHelper.cs | 8 +++++++- .../BulkOperation/BulkOperationTest.cs | 4 ++-- .../Typeless/TypelessDeltaSerializationTests.cs | 1 + 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/Microsoft.AspNetCore.OData/Formatter/ODataOutputFormatterHelper.cs b/src/Microsoft.AspNetCore.OData/Formatter/ODataOutputFormatterHelper.cs index 8c778adce..baa00320f 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/ODataOutputFormatterHelper.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/ODataOutputFormatterHelper.cs @@ -89,8 +89,14 @@ internal static async Task WriteToStreamAsync( writerSettings.BaseUri = baseAddress; //use v401 to write delta payloads. - if (serializer.ODataPayloadKind == ODataPayloadKind.Delta) + if (serializer.ODataPayloadKind == ODataPayloadKind.Delta && version < ODataVersion.V401) { + // Preserve setting of OmitODataPrefix + if (writerSettings.Version.GetValueOrDefault() == ODataVersion.V4) + { + writerSettings.SetOmitODataPrefix(writerSettings.GetOmitODataPrefix(ODataVersion.V4), ODataVersion.V401); + } + writerSettings.Version = ODataVersion.V401; } else diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/BulkOperation/BulkOperationTest.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/BulkOperation/BulkOperationTest.cs index 25df67e1d..f66ec18cd 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/BulkOperation/BulkOperationTest.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/BulkOperation/BulkOperationTest.cs @@ -47,7 +47,7 @@ public async Task DeltaSet_WithNestedFriends_WithNestedOrders_IsSerializedSucces ]}"; string expectedResponse = "{" + - "\"@context\":\"http://localhost/convention/$metadata#Employees/$delta\"," + + "\"@odata.context\":\"http://localhost/convention/$metadata#Employees/$delta\"," + "\"value\":[" + "{\"ID\":1,\"Name\":\"Employee1\",\"Friends@delta\":[{\"Id\":1,\"Name\":\"Friend1\",\"Orders@delta\":[{\"Id\":1,\"Price\":10},{\"Id\":2,\"Price\":20}]},{\"Id\":2,\"Name\":\"Friend2\"}]}," + "{\"ID\":2,\"Name\":\"Employee2\",\"Friends@delta\":[{\"Id\":3,\"Name\":\"Friend3\",\"Orders@delta\":[{\"Id\":3,\"Price\":30},{\"Id\":4,\"Price\":40}]},{\"Id\":4,\"Name\":\"Friend4\"}]}]}"; @@ -75,7 +75,7 @@ public async Task DeltaSet_WithDeletedAndODataId_IsSerializedSuccessfully() {'ID':2,'Name':'Employee2','Friends@odata.delta':[{'@id':'Friends(1)'}]} ]}"; - string expectedResponse = "{\"@context\":\"http://localhost/convention/$metadata#Employees/$delta\",\"value\":[{\"ID\":1,\"Name\":\"Employee1\",\"Friends@delta\":[{\"@removed\":{\"reason\":\"changed\"},\"@id\":\"http://host/service/Friends(1)\",\"id\":1}]},{\"ID\":2,\"Name\":\"Employee2\",\"Friends@delta\":[{\"Id\":1}]}]}"; + string expectedResponse = "{\"@odata.context\":\"http://localhost/convention/$metadata#Employees/$delta\",\"value\":[{\"ID\":1,\"Name\":\"Employee1\",\"Friends@delta\":[{\"@odata.removed\":{\"reason\":\"changed\"},\"@odata.id\":\"http://host/service/Friends(1)\",\"id\":1}]},{\"ID\":2,\"Name\":\"Employee2\",\"Friends@delta\":[{\"Id\":1}]}]}"; var requestForUpdate = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Typeless/TypelessDeltaSerializationTests.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Typeless/TypelessDeltaSerializationTests.cs index b5572fd93..cecca1bd2 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Typeless/TypelessDeltaSerializationTests.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Typeless/TypelessDeltaSerializationTests.cs @@ -59,6 +59,7 @@ public async Task TypelessDeltaWorksInAllFormats(string acceptHeader) HttpClient client = CreateClient(); HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, url); request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(acceptHeader)); + request.Headers.Add("OData-Version", "4.01"); // Act HttpResponseMessage response = await client.SendAsync(request); From 350161dd5fea6873da741f766adfb2fc5a0b2104 Mon Sep 17 00:00:00 2001 From: mikepizzo Date: Thu, 19 Dec 2024 17:47:39 -0800 Subject: [PATCH 7/9] Support deleted resources in ODataResourceSerializer, rather than separate serializers Add tests for deleted resources --- .../Formatter/LinkGenerationHelpers.cs | 5 + .../ODataDeletedResourceSerializer.cs | 173 -- .../ODataDeltaResourceSetSerializer.cs | 14 +- .../Serialization/ODataResourceSerializer.cs | 153 +- .../Microsoft.AspNetCore.OData.csproj | 6 +- .../Microsoft.AspNetCore.OData.xml | 39 +- .../Properties/SRResources.Designer.cs | 11 +- .../Properties/SRResources.resx | 3 - .../Formatter/ODataFormatterHelpers.cs | 2 +- .../Formatter/ODataTestUtil.cs | 10 +- .../ODataDeletedResourceSerializerTests.cs | 2678 +++++++++++++++++ .../ODataResourceSerializerTests.cs | 21 +- .../SerializationTestsHelpers.cs | 7 + ...rosoft.AspNetCore.OData.PublicApi.Net8.bsl | 25 +- 14 files changed, 2851 insertions(+), 296 deletions(-) delete mode 100644 src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataDeletedResourceSerializer.cs create mode 100644 test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataDeletedResourceSerializerTests.cs diff --git a/src/Microsoft.AspNetCore.OData/Formatter/LinkGenerationHelpers.cs b/src/Microsoft.AspNetCore.OData/Formatter/LinkGenerationHelpers.cs index 5dc4593e5..ea52de72a 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/LinkGenerationHelpers.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/LinkGenerationHelpers.cs @@ -37,6 +37,11 @@ public static Uri GenerateSelfLink(this ResourceContext resourceContext, bool in throw Error.ArgumentNull(nameof(resourceContext)); } + if (resourceContext.Request == null) + { + return null; + } + IList idLinkPathSegments = resourceContext.GenerateBaseODataPathSegments(); bool isSameType = resourceContext.StructuredType == resourceContext.NavigationSource?.EntityType; diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataDeletedResourceSerializer.cs b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataDeletedResourceSerializer.cs deleted file mode 100644 index 392575f23..000000000 --- a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataDeletedResourceSerializer.cs +++ /dev/null @@ -1,173 +0,0 @@ -//----------------------------------------------------------------------------- -// -// Copyright (c) .NET Foundation and Contributors. All rights reserved. -// See License.txt in the project root for license information. -// -//------------------------------------------------------------------------------ - -using System; -using System.Diagnostics.CodeAnalysis; -using System.Diagnostics.Contracts; -using System.Runtime.Serialization; -using Microsoft.AspNetCore.OData.Formatter.Value; -using Microsoft.AspNetCore.OData.Routing; -using Microsoft.OData; -using Microsoft.OData.Edm; -using System.Threading.Tasks; -using Microsoft.AspNetCore.OData.Deltas; - -namespace Microsoft.AspNetCore.OData.Formatter.Serialization; - -/// -/// ODataSerializer for serializing instances of /> -/// -[SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Justification = "Relies on many ODataLib classes.")] -public class ODataDeletedResourceSerializer : ODataResourceSerializer -{ - private const string Resource = "DeletedResource"; - - /// - public ODataDeletedResourceSerializer(IODataSerializerProvider serializerProvider) - : base(serializerProvider) - { - } - - /// - public override async Task WriteObjectAsync(object graph, Type type, ODataMessageWriter messageWriter, - ODataSerializerContext writeContext) - { - if (messageWriter == null) - { - throw Error.ArgumentNull(nameof(messageWriter)); - } - - if (writeContext == null) - { - throw Error.ArgumentNull(nameof(writeContext)); - } - - bool isUntypedPath = writeContext.Path.IsUntypedPropertyPath(); - IEdmTypeReference edmType = writeContext.GetEdmType(graph, type, isUntypedPath); - Contract.Assert(edmType != null); - - IEdmNavigationSource navigationSource = writeContext.NavigationSource; - ODataWriter writer = await messageWriter.CreateODataResourceWriterAsync(navigationSource, edmType.ToStructuredType()) - .ConfigureAwait(false); - await WriteObjectInlineAsync(graph, edmType, writer, writeContext).ConfigureAwait(false); - } - - /// - public override async Task WriteObjectInlineAsync(object graph, IEdmTypeReference expectedType, ODataWriter writer, - ODataSerializerContext writeContext) - { - if (writer == null) - { - throw Error.ArgumentNull(nameof(writer)); - } - - if (writeContext == null) - { - throw Error.ArgumentNull(nameof(writeContext)); - } - - if (graph == null || graph is NullEdmComplexObject) - { - throw new SerializationException(Error.Format(SRResources.CannotSerializerNull, Resource)); - } - else - { - await WriteDeletedResourceAsync(graph, writer, writeContext, expectedType).ConfigureAwait(false); - } - } - - private async Task WriteDeletedResourceAsync(object graph, ODataWriter writer, ODataSerializerContext writeContext, - IEdmTypeReference expectedType) - { - Contract.Assert(writeContext != null); - - IEdmStructuredTypeReference structuredType = ODataResourceSerializer.GetResourceType(graph, writeContext); - ResourceContext resourceContext = new ResourceContext(writeContext, structuredType, graph); - - SelectExpandNode selectExpandNode = CreateSelectExpandNode(resourceContext); - if (selectExpandNode != null) - { - ODataDeletedResource odataDeletedResource; - - if (graph is EdmDeltaDeletedResourceObject edmDeltaDeletedEntity) - { - odataDeletedResource = CreateDeletedResource(edmDeltaDeletedEntity.Id, edmDeltaDeletedEntity.Reason ?? DeltaDeletedEntryReason.Deleted, selectExpandNode, resourceContext); - if (edmDeltaDeletedEntity.NavigationSource != null) - { - resourceContext.NavigationSource = edmDeltaDeletedEntity.NavigationSource; - ODataResourceSerializationInfo serializationInfo = new ODataResourceSerializationInfo - { - NavigationSourceName = edmDeltaDeletedEntity.NavigationSource.Name - }; - odataDeletedResource.SetSerializationInfo(serializationInfo); - } - } - else if (graph is IDeltaDeletedResource deltaDeletedResource) - { - odataDeletedResource = CreateDeletedResource(deltaDeletedResource.Id, deltaDeletedResource.Reason ?? DeltaDeletedEntryReason.Deleted, selectExpandNode, resourceContext); - } - else - { - throw new SerializationException(Error.Format(SRResources.CannotWriteType, GetType().Name, graph?.GetType().FullName)); - } - - await writer.WriteStartAsync(odataDeletedResource).ConfigureAwait(false); - ODataResourceSerializer serializer = SerializerProvider.GetEdmTypeSerializer(expectedType) as ODataResourceSerializer; - if (serializer == null) - { - throw new SerializationException( - Error.Format(SRResources.TypeCannotBeSerialized, expectedType.ToTraceString())); - } - await serializer.WriteResourceContent(writer, selectExpandNode, resourceContext, /*isDelta*/ true); - await writer.WriteEndAsync().ConfigureAwait(false); - } - } - - /// - /// Creates the to be written while writing this resource. - /// - /// The id of the Deleted Resource to be written (may be null if properties contains all key properties) - /// The for the removal of the resource. - /// The describing the response graph. - /// The context for the resource instance being written. - /// The created . - public virtual ODataDeletedResource CreateDeletedResource(Uri id, DeltaDeletedEntryReason reason, SelectExpandNode selectExpandNode, ResourceContext resourceContext) - { - if (selectExpandNode == null) - { - throw Error.ArgumentNull(nameof(selectExpandNode)); - } - - if (resourceContext == null) - { - throw Error.ArgumentNull(nameof(resourceContext)); - } - - string typeName = resourceContext.StructuredType.FullTypeName(); - - ODataDeletedResource resource = new ODataDeletedResource - { - Id = id ?? (resourceContext.NavigationSource == null ? null : resourceContext.GenerateSelfLink(false)), - TypeName = typeName ?? "Edm.Untyped", - Properties = CreateStructuralPropertyBag(selectExpandNode, resourceContext), - Reason = reason - }; - - InitializeODataResource(selectExpandNode, resource, resourceContext); - - string etag = CreateETag(resourceContext); - if (etag != null) - { - resource.ETag = etag; - } - - // Try to add the dynamic properties if the structural type is open. - AppendDynamicPropertiesInternal(resource, selectExpandNode, resourceContext); - - return resource; - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataDeltaResourceSetSerializer.cs b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataDeltaResourceSetSerializer.cs index 9f37e2adf..89e3cbd1d 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataDeltaResourceSetSerializer.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataDeltaResourceSetSerializer.cs @@ -326,8 +326,12 @@ public virtual async Task WriteDeletedResourceAsync(object value, IEdmStructured throw Error.ArgumentNull(nameof(writer)); } - //TODO:in future, use SerializerProvider -- requires differentiating between resource serializer and deleted resource serializer for same EdmType - ODataDeletedResourceSerializer deletedResourceSerializer = new ODataDeletedResourceSerializer(SerializerProvider); + IODataEdmTypeSerializer deletedResourceSerializer = SerializerProvider.GetEdmTypeSerializer(expectedType); + if (deletedResourceSerializer == null) + { + throw new SerializationException( + Error.Format(SRResources.TypeCannotBeSerialized, expectedType.FullName())); + } await deletedResourceSerializer.WriteObjectInlineAsync(value, expectedType, writer, writeContext); } @@ -443,6 +447,12 @@ private static IEdmStructuredTypeReference GetResourceType(IEdmTypeReference fee throw new SerializationException(message); } + // Discover whether or not WriteDeltaDeletedResourceAsync is overridden in a derived class. + // WriteDeltaDeletedResourceAsync is deprecated in favor of WriteDeletedResourceAsync, but + // to avoid breaking changes, this retains the behavior of calling a custom + // WriteDeltaDeletedResourceAsync method for the case that the service has overriden that + // method with a custom implementation. In the next breaking change, WriteDeltaDeletedResourceAsync + // should be removed, and this private method can be deleted. private bool WriteDeltaDeletedResourceAsyncIsOverridden() { MethodInfo method = GetType().GetMethod("WriteDeltaDeletedResourceAsync", new Type[] { typeof(object), typeof(ODataWriter), typeof(ODataSerializerContext) }); diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataResourceSerializer.cs b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataResourceSerializer.cs index 72d8a2ffc..b98e44b34 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataResourceSerializer.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataResourceSerializer.cs @@ -155,14 +155,11 @@ private async Task WriteDeltaComplexPropertiesAsync(SelectExpandNode selectExpan foreach (IEdmStructuralProperty complexProperty in complexProperties) { - ODataNestedResourceInfo nestedResourceInfo = new ODataNestedResourceInfo - { - IsCollection = complexProperty.Type.IsCollection(), - Name = complexProperty.Name - }; + PathSelectItem selectItem = selectExpandNode.SelectedComplexProperties[complexProperty]; + ODataNestedResourceInfo nestedResourceInfo = CreateComplexNestedResourceInfo(complexProperty, selectExpandNode.SelectedComplexProperties[complexProperty], resourceContext); await writer.WriteStartAsync(nestedResourceInfo).ConfigureAwait(false); - await WriteDeltaComplexAndExpandedNavigationPropertyAsync(complexProperty, null, resourceContext, writer) + await WriteDeltaComplexAndExpandedNavigationPropertyAsync(complexProperty, selectItem, resourceContext, writer) .ConfigureAwait(false); await writer.WriteEndAsync().ConfigureAwait(false); } @@ -170,7 +167,7 @@ await WriteDeltaComplexAndExpandedNavigationPropertyAsync(complexProperty, null, private async Task WriteDeltaComplexAndExpandedNavigationPropertyAsync( IEdmProperty edmProperty, - SelectExpandClause selectExpandClause, + SelectItem selectItem, ResourceContext resourceContext, ODataWriter writer, Type navigationPropertyType = null) @@ -205,21 +202,21 @@ await writer.WriteStartAsync(new ODataResourceSet else { // create the serializer context for the complex and expanded item. - ODataSerializerContext nestedWriteContext = new ODataSerializerContext(resourceContext, selectExpandClause, edmProperty); + ODataSerializerContext nestedWriteContext = new ODataSerializerContext(resourceContext, edmProperty, resourceContext.SerializerContext.QueryContext, selectItem); nestedWriteContext.Type = navigationPropertyType; - // write object. - // TODO: enable overriding serializer based on type. Currently requires serializer supports WriteDeltaObjectinline, because it takes an ODataDeltaWriter - // ODataEdmTypeSerializer serializer = SerializerProvider.GetEdmTypeSerializer(edmProperty.Type); - // if (serializer == null) - // { - // throw new SerializationException( - // Error.Format(SRResources.TypeCannotBeSerialized, edmProperty.Type.ToTraceString())); - // } + // write object. if (edmProperty.Type.IsCollection()) { if (IsDeltaCollection(propertyValue)) { + // TODO: enable overriding serializer based on type. Currently requires serializer supports WriteDeltaObjectinline, because it takes an ODataDeltaWriter + // ODataEdmTypeSerializer serializer = SerializerProvider.GetEdmTypeSerializer(edmProperty.Type); + // if (serializer == null) + // { + // throw new SerializationException( + // Error.Format(SRResources.TypeCannotBeSerialized, edmProperty.Type.ToTraceString())); + // } ODataEdmTypeSerializer serializer = new ODataDeltaResourceSetSerializer(SerializerProvider); IEdmEntityType itemType = edmProperty.Type.GetElementType() as IEdmEntityType; if(itemType == null) @@ -228,10 +225,9 @@ await writer.WriteStartAsync(new ODataResourceSet Error.Format(SRResources.TypeCannotBeSerialized, edmProperty.Type.ToTraceString())); } - EdmCollectionTypeReference edmType = new EdmCollectionTypeReference(new EdmDeltaCollectionType(new EdmEntityTypeReference(itemType, false))); await serializer.WriteObjectInlineAsync( propertyValue, - edmType, + edmProperty.Type, writer, nestedWriteContext).ConfigureAwait(false); } @@ -249,8 +245,7 @@ await serializer.WriteObjectInlineAsync( } else { - ODataResourceSerializer serializer = new ODataResourceSerializer(SerializerProvider); - await serializer.WriteDeltaObjectInlineAsync(propertyValue, edmProperty.Type, writer, nestedWriteContext).ConfigureAwait(false); + await WriteDeltaObjectInlineAsync(propertyValue, edmProperty.Type, writer, nestedWriteContext).ConfigureAwait(false); } } } @@ -271,12 +266,7 @@ private async Task WriteDeltaNavigationPropertiesAsync(SelectExpandNode selectEx foreach (KeyValuePair navigationProperty in navigationProperties) { - ODataNestedResourceInfo nestedResourceInfo = new ODataNestedResourceInfo - { - IsCollection = navigationProperty.Key.Type.IsCollection(), - Name = navigationProperty.Key.Name - }; - + ODataNestedResourceInfo nestedResourceInfo = CreateNavigationLink(navigationProperty.Key, resourceContext); await writer.WriteStartAsync(nestedResourceInfo).ConfigureAwait(false); await WriteDeltaComplexAndExpandedNavigationPropertyAsync(navigationProperty.Key, null, resourceContext, writer, navigationProperty.Value).ConfigureAwait(false); await writer.WriteEndAsync().ConfigureAwait(false); @@ -460,27 +450,60 @@ private async Task WriteResourceAsync(object graph, ODataWriter writer, ODataSer SelectExpandNode selectExpandNode = CreateSelectExpandNode(resourceContext); if (selectExpandNode != null) { - ODataResource resource = CreateResource(selectExpandNode, resourceContext); - if (resource != null) + if (graph is IDeltaDeletedResource || graph is IEdmDeltaDeletedResourceObject) { - if (resourceContext.SerializerContext.ExpandReference) + ODataDeletedResource odataDeletedResource; + + if (graph is EdmDeltaDeletedResourceObject edmDeltaDeletedEntity) { - await writer.WriteEntityReferenceLinkAsync(new ODataEntityReferenceLink + odataDeletedResource = CreateDeletedResource(edmDeltaDeletedEntity.Id, edmDeltaDeletedEntity.Reason ?? DeltaDeletedEntryReason.Deleted, selectExpandNode, resourceContext); + if (edmDeltaDeletedEntity.NavigationSource != null) { - Url = resource.Id - }).ConfigureAwait(false); + resourceContext.NavigationSource = edmDeltaDeletedEntity.NavigationSource; + ODataResourceSerializationInfo serializationInfo = new ODataResourceSerializationInfo + { + NavigationSourceName = edmDeltaDeletedEntity.NavigationSource.Name + }; + odataDeletedResource.SetSerializationInfo(serializationInfo); + } + } + else if (graph is IDeltaDeletedResource deltaDeletedResource) + { + odataDeletedResource = CreateDeletedResource(deltaDeletedResource.Id, deltaDeletedResource.Reason ?? DeltaDeletedEntryReason.Deleted, selectExpandNode, resourceContext); } else { - bool isDelta = graph is IDelta || graph is IEdmChangedObject; - await writer.WriteStartAsync(resource).ConfigureAwait(false); - await WriteResourceContent(writer, selectExpandNode, resourceContext, isDelta); - await writer.WriteEndAsync().ConfigureAwait(false); + throw new SerializationException(Error.Format(SRResources.CannotWriteType, GetType().Name, graph?.GetType().FullName)); + } + + await writer.WriteStartAsync(odataDeletedResource).ConfigureAwait(false); + await WriteResourceContent(writer, selectExpandNode, resourceContext, /*isDelta*/ true); + await writer.WriteEndAsync().ConfigureAwait(false); + } + else + { + ODataResource resource = CreateResource(selectExpandNode, resourceContext); + if (resource != null) + { + if (resourceContext.SerializerContext.ExpandReference) + { + await writer.WriteEntityReferenceLinkAsync(new ODataEntityReferenceLink + { + Url = resource.Id + }).ConfigureAwait(false); + } + else + { + bool isDelta = graph is IDelta || graph is IEdmChangedObject; + await writer.WriteStartAsync(resource).ConfigureAwait(false); + await WriteResourceContent(writer, selectExpandNode, resourceContext, isDelta); + await writer.WriteEndAsync().ConfigureAwait(false); + } } } - } - writeContext.NavigationSource = originalNavigationSource; + writeContext.NavigationSource = originalNavigationSource; + } } /// @@ -490,7 +513,7 @@ await writer.WriteEntityReferenceLinkAsync(new ODataEntityReferenceLink /// The describing the response graph. /// The context for the resource instance being written. /// Whether to only write changed properties of the resource - protected internal async Task WriteResourceContent(ODataWriter writer, SelectExpandNode selectExpandNode, ResourceContext resourceContext, bool isDelta) + private async Task WriteResourceContent(ODataWriter writer, SelectExpandNode selectExpandNode, ResourceContext resourceContext, bool isDelta) { // TODO: These should be aligned; do we need different methods for delta versus non-delta complex/navigation properties? if (isDelta) @@ -611,13 +634,57 @@ public virtual ODataResource CreateResource(SelectExpandNode selectExpandNode, R return resource; } + /// + /// Creates the to be written while writing this resource. + /// + /// The id of the Deleted Resource to be written (may be null if properties contains all key properties) + /// The for the removal of the resource. + /// The describing the response graph. + /// The context for the resource instance being written. + /// The created . + public virtual ODataDeletedResource CreateDeletedResource(Uri id, DeltaDeletedEntryReason reason, SelectExpandNode selectExpandNode, ResourceContext resourceContext) + { + if (selectExpandNode == null) + { + throw Error.ArgumentNull(nameof(selectExpandNode)); + } + + if (resourceContext == null) + { + throw Error.ArgumentNull(nameof(resourceContext)); + } + + string typeName = resourceContext.StructuredType.FullTypeName(); + + ODataDeletedResource resource = new ODataDeletedResource + { + Id = id ?? (resourceContext.NavigationSource == null ? null : resourceContext.GenerateSelfLink(false)), + TypeName = typeName ?? "Edm.Untyped", + Properties = CreateStructuralPropertyBag(selectExpandNode, resourceContext), + Reason = reason + }; + + InitializeODataResource(selectExpandNode, resource, resourceContext); + + string etag = CreateETag(resourceContext); + if (etag != null) + { + resource.ETag = etag; + } + + // Try to add the dynamic properties if the structural type is open. + AppendDynamicPropertiesInternal(resource, selectExpandNode, resourceContext); + + return resource; + } + /// /// Initializes an ODataResource to be written /// /// The describing the response graph. /// The resource that will be initialized /// The context for the resource instance being written. - protected internal void InitializeODataResource(SelectExpandNode selectExpandNode, ODataResourceBase resource, ResourceContext resourceContext) + private void InitializeODataResource(SelectExpandNode selectExpandNode, ODataResourceBase resource, ResourceContext resourceContext) { if ((resourceContext.EdmObject is EdmDeltaResourceObject || resourceContext.EdmObject is IEdmDeltaDeletedResourceObject) && resourceContext.NavigationSource != null) { @@ -703,7 +770,7 @@ public virtual void AppendDynamicProperties(ODataResource resource, SelectExpand /// The describing the resource. /// The describing the response graph. /// The context for the resource instance being written. - protected internal void AppendDynamicPropertiesInternal(ODataResourceBase resource, SelectExpandNode selectExpandNode, + private void AppendDynamicPropertiesInternal(ODataResourceBase resource, SelectExpandNode selectExpandNode, ResourceContext resourceContext) { Contract.Assert(resource != null); @@ -1210,7 +1277,7 @@ private IEnumerable CreateNavigationLinks( /// The navigation link to be written. /// The resource context for the resource whose navigation link is being written. /// true if navigation link should be written; otherwise false. - protected virtual bool ShouldWriteNavigation(ODataNestedResourceInfo navigationLink, ResourceContext resourceContext) + private bool ShouldWriteNavigation(ODataNestedResourceInfo navigationLink, ResourceContext resourceContext) { if (navigationLink?.Url != null || (navigationLink != null && resourceContext.SerializerContext.MetadataLevel == ODataMetadataLevel.Full)) { @@ -1327,7 +1394,7 @@ public virtual ODataNestedResourceInfo CreateNavigationLink(IEdmNavigationProper /// The context for the entity instance being written. /// The navigation link to be written. /// ODataProperties to be written - protected internal IEnumerable CreateStructuralPropertyBag(SelectExpandNode selectExpandNode, ResourceContext resourceContext) + private IEnumerable CreateStructuralPropertyBag(SelectExpandNode selectExpandNode, ResourceContext resourceContext) { Contract.Assert(selectExpandNode != null); Contract.Assert(resourceContext != null); diff --git a/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.csproj b/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.csproj index 0b955c1c7..d35fc5ef1 100644 --- a/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.csproj +++ b/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.csproj @@ -28,9 +28,9 @@ 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 ea13def7d..a569624f9 100644 --- a/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml +++ b/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml @@ -4507,30 +4507,6 @@ The collection value for which the annotations have to be added. The OData metadata level of the response. - - - ODataSerializer for serializing instances of /> - - - - - - - - - - - - - - Creates the to be written while writing this resource. - - The id of the Deleted Resource to be written (may be null if properties contains all key properties) - The for the removal of the resource. - The describing the response graph. - The context for the resource instance being written. - The created . - OData serializer for serializing a collection of @@ -4816,6 +4792,16 @@ The context for the resource instance being written. The created . + + + Creates the to be written while writing this resource. + + The id of the Deleted Resource to be written (may be null if properties contains all key properties) + The for the removal of the resource. + The describing the response graph. + The context for the resource instance being written. + The created . + Initializes an ODataResource to be written @@ -7107,11 +7093,6 @@ Looks up a localized string similar to The Uri '{0}' in the parameter is invalid.. - - - Looks up a localized string similar to The related entity set could not be found from the OData path. The related entity set is required to serialize the payload.. - - Looks up a localized string similar to The entity type '{0}' does not match the expected entity type '{1}' as set on the query context.. diff --git a/src/Microsoft.AspNetCore.OData/Properties/SRResources.Designer.cs b/src/Microsoft.AspNetCore.OData/Properties/SRResources.Designer.cs index 8602389e2..bb997de6a 100644 --- a/src/Microsoft.AspNetCore.OData/Properties/SRResources.Designer.cs +++ b/src/Microsoft.AspNetCore.OData/Properties/SRResources.Designer.cs @@ -627,15 +627,6 @@ internal static string EntityReferenceMustHasKeySegment { } } - /// - /// Looks up a localized string similar to The related entity set could not be found from the OData path. The related entity set is required to serialize the payload.. - /// - internal static string EntitySetMissingDuringSerialization { - get { - return ResourceManager.GetString("EntitySetMissingDuringSerialization", resourceCulture); - } - } - /// /// Looks up a localized string similar to The entity type '{0}' does not match the expected entity type '{1}' as set on the query context.. /// @@ -1805,7 +1796,7 @@ internal static string TypeMustBeOpenType { return ResourceManager.GetString("TypeMustBeOpenType", resourceCulture); } } - + /// /// Looks up a localized string similar to The type '{0}' does not inherit from and is not a base type of '{1}'.. /// diff --git a/src/Microsoft.AspNetCore.OData/Properties/SRResources.resx b/src/Microsoft.AspNetCore.OData/Properties/SRResources.resx index e7981a72f..fb2f3f44e 100644 --- a/src/Microsoft.AspNetCore.OData/Properties/SRResources.resx +++ b/src/Microsoft.AspNetCore.OData/Properties/SRResources.resx @@ -597,9 +597,6 @@ The type '{0}' is not supported by the ODataErrorSerializer. The type must be ODataError or HttpError. - - The related entity set could not be found from the OData path. The related entity set is required to serialize the payload. - A segment '{0}' within the select or expand query option is not supported. diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/ODataFormatterHelpers.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/ODataFormatterHelpers.cs index 10e04854e..ed7a6c6fa 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/ODataFormatterHelpers.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/ODataFormatterHelpers.cs @@ -157,7 +157,7 @@ private static IServiceProvider BuildServiceProvider() services.AddSingleton(); services.AddSingleton(); - // services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/ODataTestUtil.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/ODataTestUtil.cs index 03b0153df..1971fdc7a 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/ODataTestUtil.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/ODataTestUtil.cs @@ -18,16 +18,18 @@ namespace Microsoft.AspNetCore.OData.Tests.Formatter; internal static class ODataTestUtil { - public static ODataMessageWriter GetMockODataMessageWriter() + public static ODataMessageWriter GetMockODataMessageWriter(ODataVersion version = ODataVersion.V4) { MockODataRequestMessage requestMessage = new MockODataRequestMessage(); - return new ODataMessageWriter(requestMessage); + ODataMessageWriterSettings settings = new ODataMessageWriterSettings(version); + return new ODataMessageWriter(requestMessage, settings); } - public static ODataMessageReader GetMockODataMessageReader() + public static ODataMessageReader GetMockODataMessageReader(ODataVersion version = ODataVersion.V4) { MockODataRequestMessage requestMessage = new MockODataRequestMessage(); - return new ODataMessageReader(requestMessage); + ODataMessageReaderSettings settings = new ODataMessageReaderSettings(version); + return new ODataMessageReader(requestMessage, settings); } public static IODataSerializerProvider GetMockODataSerializerProvider(ODataEdmTypeSerializer serializer) diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataDeletedResourceSerializerTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataDeletedResourceSerializerTests.cs new file mode 100644 index 000000000..1fb5a99cb --- /dev/null +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataDeletedResourceSerializerTests.cs @@ -0,0 +1,2678 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.Serialization; +using System.Threading.Tasks; +using Microsoft.AspNetCore.OData.Abstracts; +using Microsoft.AspNetCore.OData.Deltas; +using Microsoft.AspNetCore.OData.Edm; +using Microsoft.AspNetCore.OData.Extensions; +using Microsoft.AspNetCore.OData.Formatter; +using Microsoft.AspNetCore.OData.Formatter.Serialization; +using Microsoft.AspNetCore.OData.Formatter.Value; +using Microsoft.AspNetCore.OData.Tests.Commons; +using Microsoft.AspNetCore.OData.Tests.Edm; +using Microsoft.AspNetCore.OData.Tests.Extensions; +using Microsoft.AspNetCore.OData.Tests.Models; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Net.Http.Headers; +using Microsoft.OData; +using Microsoft.OData.Edm; +using Microsoft.OData.Edm.Vocabularies; +using Microsoft.OData.ModelBuilder; +using Microsoft.OData.UriParser; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.OData.Tests.Formatter.Serialization; + +public class ODataDeletedResourceSerializerTests +{ + private IEdmModel _model; + private IEdmEntitySet _customerSet; + private IEdmEntitySet _orderSet; + private DeltaDeletedResource _customer; + private DeltaDeletedResource _order; + private ODataResourceSerializer _serializer; + private ODataSerializerContext _writeContext; + private ResourceContext _entityContext; + private IODataSerializerProvider _serializerProvider; + private IEdmEntityTypeReference _customerType; + private IEdmEntityTypeReference _orderType; + private IEdmEntityTypeReference _specialCustomerType; + private IEdmEntityTypeReference _specialOrderType; + private IEdmComplexTypeReference _sizeType; + private IEdmNavigationProperty _ordersNavigation; + private DeltaSet _orders; + private Size _size; + private ODataPath _path; + + public ODataDeletedResourceSerializerTests() + { + _model = SerializationTestsHelpers.SimpleCustomerOrderModel(); + + _model.SetAnnotationValue(_model.FindType("Default.Customer"), new ClrTypeAnnotation(typeof(Customer))); + _model.SetAnnotationValue(_model.FindType("Default.Order"), new ClrTypeAnnotation(typeof(Order))); + _model.SetAnnotationValue(_model.FindType("Default.SpecialCustomer"), new ClrTypeAnnotation(typeof(SpecialCustomer))); + _model.SetAnnotationValue(_model.FindType("Default.SpecialOrder"), new ClrTypeAnnotation(typeof(SpecialOrder))); + _model.SetAnnotationValue(_model.FindType("Default.Size"), new ClrTypeAnnotation(typeof(Size))); + + _customerSet = _model.EntityContainer.FindEntitySet("Customers"); + _customer = new DeltaDeletedResource(); + _customer.TrySetPropertyValue("ID", 10); + _customer.TrySetPropertyValue("FirstName", "Foo"); + _customer.TrySetPropertyValue("LastName", "Bar"); + _customer.Id = new Uri("http://customers/10"); + + _size = new Size { Height = 72, Weight = 180 }; + _customer.TrySetPropertyValue("Size", _size); + + _orderSet = _model.EntityContainer.FindEntitySet("Orders"); + _order = new DeltaDeletedResource(); + _order.TrySetPropertyValue("ID", 20); + + _orders = new DeltaSet{ _order }; + _customer.TrySetPropertyValue("Orders", _orders); + + _serializerProvider = GetServiceProvider().GetService(); + _customerType = _model.GetEdmTypeReference(typeof(Customer)).AsEntity(); + _orderType = _model.GetEdmTypeReference(typeof(Order)).AsEntity(); + _specialCustomerType = _model.GetEdmTypeReference(typeof(SpecialCustomer)).AsEntity(); + _specialOrderType = _model.GetEdmTypeReference(typeof(SpecialOrder)).AsEntity(); + _sizeType = _model.GetEdmTypeReference(typeof(Size)).AsComplex(); + _ordersNavigation = _customerType.FindNavigationProperty("Orders"); + _serializer = new ODataResourceSerializer(_serializerProvider); + _path = new ODataPath(new EntitySetSegment(_customerSet)); + _writeContext = new ODataSerializerContext() { NavigationSource = _customerSet, Model = _model, Path = _path, Type = typeof(DeltaDeletedResource)}; + _entityContext = new ResourceContext(_writeContext, _customerSet.EntityType.AsReference(), _customer); + } + + [Fact] + public void Ctor_ThrowsArgumentNull_SerializerProvider() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new ODataResourceSerializer(serializerProvider: null), "serializerProvider"); + } + + [Fact] + public async Task WriteObjectAsync_ThrowsArgumentNull_MessageWriter() + { + // Arrange & Act + Mock serializerProvider = new Mock(); + ODataResourceSerializer serializer = new ODataResourceSerializer(serializerProvider.Object); + + // Assert + await ExceptionAssert.ThrowsArgumentNullAsync( + () => serializer.WriteObjectAsync(graph: _customer, type: typeof(Customer), messageWriter: null, writeContext: null), + "messageWriter"); + } + + [Fact] + public async Task WriteObjectAsync_ThrowsArgumentNull_WriteContext() + { + // Arrange & Act + Mock serializerProvider = new Mock(); + ODataResourceSerializer serializer = new ODataResourceSerializer(serializerProvider.Object); + ODataMessageWriter messageWriter = new ODataMessageWriter(new Mock().Object); + + // Assert + await ExceptionAssert.ThrowsArgumentNullAsync( + () => serializer.WriteObjectAsync(graph: _customer, type: typeof(Customer), messageWriter: messageWriter, writeContext: null), + "writeContext"); + } + + [Fact] + public async Task WriteObjectAsync_Calls_WriteObjectInline_WithRightEntityType() + { + // Arrange + Mock serializerProvider = new Mock(); + serializerProvider.Setup(s => + s.GetEdmTypeSerializer(It.Is(e => _orderType.Definition == e.Definition))).Returns(new ODataResourceSerializer(serializerProvider.Object)); + serializerProvider.Setup(s => + s.GetEdmTypeSerializer(It.IsAny())).Returns(new ODataPrimitiveSerializer()); + Mock serializer = new Mock(serializerProvider.Object); + serializer + .Setup(s => s.WriteObjectInlineAsync(_customer, It.Is(e => _customerType.Definition == e.Definition), + It.IsAny(), _writeContext)) + .Verifiable(); + serializer.Setup(s => s.CreateSelectExpandNode(It.IsAny())).Returns(new SelectExpandNode()); + serializer.CallBase = true; + + // Act + await serializer.Object.WriteObjectAsync(_customer, typeof(Customer), ODataTestUtil.GetMockODataMessageWriter(ODataVersion.V401), _writeContext); + + // Assert + serializer.Verify(); + } + + [Fact] + public async Task WriteObjectInlineAsync_ThrowsArgumentNull_Writer() + { + // Arrange & Act + Mock serializerProvider = new Mock(); + ODataResourceSerializer serializer = new ODataResourceSerializer(serializerProvider.Object); + + // Assert + await ExceptionAssert.ThrowsArgumentNullAsync( + () => serializer.WriteObjectInlineAsync(graph: null, expectedType: null, writer: null, writeContext: new ODataSerializerContext()), + "writer"); + } + + [Fact] + public async Task WriteObjectInlineAsync_ThrowsArgumentNull_WriteContext() + { + // Arrange & Act & Assert + await ExceptionAssert.ThrowsArgumentNullAsync( + () => _serializer.WriteObjectInlineAsync(graph: null, expectedType: null, writer: new Mock().Object, writeContext: null), + "writeContext"); + } + + [Fact] + public async Task WriteObjectInlineAsync_ThrowsSerializationException_WhenGraphIsNull() + { + // Arrange & Act + Mock serializerProvider = new Mock(); + ODataResourceSerializer serializer = new ODataResourceSerializer(serializerProvider.Object); + ODataWriter messageWriter = new Mock().Object; + + // Assert + await ExceptionAssert.ThrowsAsync( + () => serializer.WriteObjectInlineAsync(graph: null, expectedType: null, writer: messageWriter, writeContext: new ODataSerializerContext()), + "Cannot serialize a null 'Resource'."); + } + + [Fact] + public async Task WriteObjectInlineAsync_Calls_CreateSelectExpandNode() + { + // Arrange + Mock serializerProvider = new Mock(); + serializerProvider.Setup(s => s.GetEdmTypeSerializer(It.IsAny())).Returns(new ODataResourceSerializer(serializerProvider.Object)); + serializerProvider.Setup(s => s.GetEdmTypeSerializer(It.IsAny())).Returns(new ODataPrimitiveSerializer()); + Mock serializer = new Mock(serializerProvider.Object); + ODataWriter writer = new Mock().Object; + + serializer.Setup(s => s.CreateSelectExpandNode(It.Is(e => + e.StructuredType == _customer ? Verify(e, _customer, _writeContext) : + e.StructuredType == _sizeType ? Verify(e, _size, _writeContext) : true))).Verifiable(); + serializer.CallBase = true; + + // Act + await serializer.Object.WriteObjectInlineAsync(_customer, _customerType, writer, _writeContext); + + // Assert + serializer.Verify(); + } + + [Fact] + public async Task WriteObjectInlineAsync_Calls_CreateDeletedResource() + { + // Arrange + SelectExpandNode selectExpandNode = new SelectExpandNode(); + Mock serializerProvider = new Mock(); + serializerProvider.Setup(s => s.GetEdmTypeSerializer(It.IsAny())).Returns(new ODataResourceSerializer(serializerProvider.Object)); + serializerProvider.Setup(s => s.GetEdmTypeSerializer(It.IsAny())).Returns(new ODataPrimitiveSerializer()); + Mock serializer = new Mock(serializerProvider.Object); + ODataWriter writer = new Mock().Object; + + serializer.Setup(s => s.CreateSelectExpandNode(It.IsAny())).Returns(selectExpandNode); + serializer.Setup(s => s.CreateDeletedResource(It.IsAny(), DeltaDeletedEntryReason.Deleted, selectExpandNode, It.Is(e => Verify(e, _customer, _writeContext)))).Verifiable(); + serializer.CallBase = true; + + // Act + await serializer.Object.WriteObjectInlineAsync(_customer, _customerType, writer, _writeContext); + + // Assert + serializer.Verify(); + } + + [Fact] + public async Task WriteObjectInlineAsync_WritesODataResourceFrom_CreateResource() + { + // Arrange + ODataDeletedResource entry = new ODataDeletedResource(); + Mock serializerProvider = new Mock(); + serializerProvider.Setup(s => + s.GetEdmTypeSerializer(It.Is(e => _orderType.Definition == e.Definition))).Returns(new ODataResourceSerializer(serializerProvider.Object)); + serializerProvider.Setup(s => + s.GetEdmTypeSerializer(It.IsAny())).Returns(new ODataPrimitiveSerializer()); + Mock serializer = new Mock(serializerProvider.Object); + Mock writer = new Mock(); + + serializer.Setup(s => s.CreateDeletedResource(It.IsAny(), DeltaDeletedEntryReason.Deleted, It.IsAny(), It.IsAny())).Returns(entry); + serializer.CallBase = true; + + writer.Setup(s => s.WriteStartAsync(entry)).Verifiable(); + + // Act + await serializer.Object.WriteObjectInlineAsync(_customer, _customerType, writer.Object, _writeContext); + + // Assert + writer.Verify(); + } + + [Fact] + public async Task WriteObjectInlineAsync_Calls_CreateUntypedPropertyValue_ForUntypedProperty() + { + // Arrange + Mock untyped = new Mock(); + untyped.Setup(u => u.TypeKind).Returns(EdmTypeKind.Untyped); + + Mock untypedRef = new Mock(); + untypedRef.Setup(p => p.Definition).Returns(untyped.Object); + + Mock property1 = new Mock(); + property1.Setup(p => p.Type).Returns(untypedRef.Object); + + SelectExpandNode selectExpandNode = new SelectExpandNode + { + SelectedStructuralProperties = new HashSet + { + property1.Object + } + }; + + Mock writer = new Mock(); + Mock serializer = new Mock(_serializerProvider); + serializer.Setup(s => s.CreateSelectExpandNode(It.IsAny())).Returns(selectExpandNode); + serializer.CallBase = true; + IEdmTypeReference a; + + serializer.Setup(s => s.CreateUntypedPropertyValue(property1.Object, It.IsAny(), out a)).Returns("a").Verifiable(); + + // Act + await serializer.Object.WriteObjectInlineAsync(_customer, _customerType, writer.Object, _writeContext); + + // Assert + serializer.Verify(); + } + + [Fact] + public async Task WriteObjectInlineAsync_Calls_CreateComplexNestedResourceInfo_ForEachSelectedComplexProperty() + { + // Arrange + SelectExpandNode selectExpandNode = new SelectExpandNode + { + SelectedComplexProperties = new Dictionary + { + { new Mock( + (IEdmStructuredType)_customerType.Definition, + "Size", + _sizeType).Object, + null + }, + } + }; + + Mock writer = new Mock(); + Mock serializer = new Mock(_serializerProvider); + serializer.Setup(s => s.CreateSelectExpandNode(It.Is(c=>c.StructuredType == _customerType.Definition))).Returns(selectExpandNode); + serializer.CallBase = true; + + serializer.Setup(s => s.CreateComplexNestedResourceInfo(selectExpandNode.SelectedComplexProperties.ElementAt(0).Key, null, It.IsAny())).Verifiable(); + + // Act + await serializer.Object.WriteObjectInlineAsync(_customer, _customerType, writer.Object, _writeContext); + + // Assert + serializer.Verify(); + } + + [Fact] + public async Task WriteObjectInlineAsync_Calls_CreateNavigationLink_ForEachSelectedNavigationProperty() + { + // Arrange + SelectExpandNode selectExpandNode = new SelectExpandNode + { + SelectedNavigationProperties = new HashSet + { + _ordersNavigation + } + }; + Mock writer = new Mock(); + Mock serializer = new Mock(_serializerProvider); + serializer.Setup(s => s.CreateSelectExpandNode(It.IsAny())).Returns(selectExpandNode); + serializer.CallBase = true; + + serializer.Setup(s => s.CreateNavigationLink(selectExpandNode.SelectedNavigationProperties.ElementAt(0), It.IsAny())).Verifiable(); + + ODataSerializerContext writeContext = new ODataSerializerContext() { NavigationSource = _customerSet, Model = _model, Path = _path, Type = typeof(DeltaDeletedResource) }; + writeContext.MetadataLevel = ODataMetadataLevel.Full; + + // Act + await serializer.Object.WriteObjectInlineAsync(_customer, _customerType, writer.Object, _writeContext); + + // Assert + serializer.Verify(); + } + + [Fact] + public async Task WriteObjectInlineAsync_WritesNavigationLinksReturnedBy_CreateNavigationLink_ForEachSelectedNavigationProperty() + { + // Arrange + SelectExpandNode selectExpandNode = new SelectExpandNode + { + SelectedNavigationProperties = new HashSet + { + _ordersNavigation + } + }; + ODataNestedResourceInfo[] navigationLinks = new[] + { + new ODataNestedResourceInfo() + }; + Mock serializer = new Mock(_serializerProvider); + serializer.Setup(s => s.CreateSelectExpandNode(It.IsAny())).Returns(selectExpandNode); + serializer + .Setup(s => s.CreateNavigationLink(selectExpandNode.SelectedNavigationProperties.ElementAt(0), It.IsAny())) + .Returns(navigationLinks[0]); + serializer.CallBase = true; + + Mock writer = new Mock(); + writer.Setup(w => w.WriteStartAsync(navigationLinks[0])).Verifiable(); + + ODataSerializerContext writeContext = new ODataSerializerContext() { NavigationSource = _customerSet, Model = _model, Path = _path, Type = typeof(DeltaDeletedResource) }; + writeContext.MetadataLevel = ODataMetadataLevel.Full; + + // Act + await serializer.Object.WriteObjectInlineAsync(_customer, _customerType, writer.Object, writeContext); + + // Assert + writer.Verify(); + } + + [Fact] + public async Task WriteObjectInlineAsync_Calls_CreateNavigationLink_ForEachExpandedNavigationProperty() + { + // Arrange + SelectExpandNode selectExpandNode = new SelectExpandNode + { + ExpandedProperties = new Dictionary + { + { _ordersNavigation, null }, + } + }; + Mock writer = new Mock(); + Mock serializer = new Mock(_serializerProvider); + serializer.Setup(s => s.CreateSelectExpandNode(It.IsAny())).Returns(selectExpandNode); + var expandedNavigationProperties = selectExpandNode.ExpandedProperties.Keys; + + serializer.Setup(s => s.CreateNavigationLink(expandedNavigationProperties.First(), It.IsAny())).Verifiable(); + serializer.CallBase = true; + + // Act + await serializer.Object.WriteObjectInlineAsync(_customer, _customerType, writer.Object, _writeContext); + + // Assert + serializer.Verify(); + } + + [Fact] + public async Task WriteObjectInlineAsync_ExpandsUsingInnerSerializerUsingRightContext_ExpandedNavigationProperties() + { + // Arrange + IEdmEntityType customerType = _customerSet.EntityType; + IEdmNavigationProperty ordersProperty = customerType.NavigationProperties().Single(p => p.Name == "Orders"); + + ODataQueryOptionParser parser = new ODataQueryOptionParser(_model, customerType, _customerSet, + new Dictionary { { "$select", "Orders" }, { "$expand", "Orders" } }); + SelectExpandClause selectExpandClause = parser.ParseSelectAndExpand(); + + SelectExpandNode selectExpandNode = new SelectExpandNode + { + ExpandedProperties = new Dictionary + { + { _ordersNavigation, selectExpandClause.SelectedItems.OfType().Single() } + }, + }; + Mock writer = new Mock(); + + Mock innerSerializer = new Mock(ODataPayloadKind.Resource); + + //innerSerializer + // .Setup(s => s.WriteObjectInlineAsync(_orders, ordersProperty.Type, writer.Object, It.IsAny())) + // .Callback((object o, IEdmTypeReference t, ODataWriter w, ODataSerializerContext context) => + // { + // Assert.Same(context.NavigationSource.Name, "Orders"); + // Assert.Same(context.SelectExpandClause, selectExpandNode.ExpandedProperties.Single().Value.SelectAndExpand); + // }) + // .Returns(Task.CompletedTask) + // .Verifiable(); + + Mock serializerProvider = new Mock(); + serializerProvider.Setup( + p => p.GetEdmTypeSerializer(It.IsAny())) + .Returns(innerSerializer.Object); + Mock serializer = new Mock(serializerProvider.Object); + serializer.Setup(s => s.CreateSelectExpandNode(It.IsAny())).Returns(selectExpandNode); + serializer.CallBase = true; + _writeContext.SelectExpandClause = selectExpandClause; + + // Act + await serializer.Object.WriteObjectInlineAsync(_customer, _customerType, writer.Object, _writeContext); + + // Assert + innerSerializer.Verify(); + // check that the context is rolled back + Assert.Same(_writeContext.NavigationSource.Name, "Customers"); + Assert.Same(_writeContext.SelectExpandClause, selectExpandClause); + } + + [Fact] + public async Task WriteObjectInlineAsync_CanExpandNavigationProperty_ContainingEdmObject() + { + // Arrange + IEdmEntityType customerType = _customerSet.EntityType; + IEdmNavigationProperty ordersProperty = customerType.NavigationProperties().Single(p => p.Name == "Orders"); + + Mock orders = new Mock(); + orders.Setup(o => o.GetEdmType()).Returns(ordersProperty.Type); + object ordersValue = orders.Object; + + Mock customer = new Mock(); + customer.Setup(c => c.TryGetPropertyValue("Orders", out ordersValue)).Returns(true); + customer.Setup(c => c.GetEdmType()).Returns(customerType.AsReference()); + + ODataQueryOptionParser parser = new ODataQueryOptionParser(_model, customerType, _customerSet, + new Dictionary { { "$select", "Orders" }, { "$expand", "Orders" } }); + SelectExpandClause selectExpandClause = parser.ParseSelectAndExpand(); + + SelectExpandNode selectExpandNode = new SelectExpandNode + { + ExpandedProperties = new Dictionary() + }; + selectExpandNode.ExpandedProperties[ordersProperty] = selectExpandClause.SelectedItems.OfType().Single(); + + Mock writer = new Mock(); + + Mock ordersSerializer = new Mock(ODataPayloadKind.Resource); + ordersSerializer.Setup(s => s.WriteObjectInlineAsync(ordersValue, ordersProperty.Type, writer.Object, It.IsAny())).Verifiable(); + + Mock serializerProvider = new Mock(); + serializerProvider.Setup(p => p.GetEdmTypeSerializer(ordersProperty.Type)).Returns(ordersSerializer.Object); + + Mock serializer = new Mock(serializerProvider.Object); + serializer.Setup(s => s.CreateSelectExpandNode(It.IsAny())).Returns(selectExpandNode); + serializer.CallBase = true; + + // Act + await serializer.Object.WriteObjectInlineAsync(customer.Object, _customerType, writer.Object, _writeContext); + + //Assert + ordersSerializer.Verify(); + } + + [Fact] + public async Task WriteObjectInlineAsync_CanWriteExpandedNavigationProperty_ExpandedCollectionValuedNavigationPropertyIsNull() + { + // Arrange + IEdmEntityType customerType = _customerSet.EntityType; + IEdmNavigationProperty ordersProperty = customerType.NavigationProperties().Single(p => p.Name == "Orders"); + + Mock customer = new Mock(); + object ordersValue = null; + customer.Setup(c => c.TryGetPropertyValue("Orders", out ordersValue)).Returns(true); + customer.Setup(c => c.GetEdmType()).Returns(customerType.AsReference()); + + ODataQueryOptionParser parser = new ODataQueryOptionParser(_model, customerType, _customerSet, + new Dictionary { { "$select", "Orders" }, { "$expand", "Orders" } }); + SelectExpandClause selectExpandClause = parser.ParseSelectAndExpand(); + + SelectExpandNode selectExpandNode = new SelectExpandNode + { + ExpandedProperties = new Dictionary() + }; + selectExpandNode.ExpandedProperties[ordersProperty] = + selectExpandClause.SelectedItems.OfType().Single(); + + Mock writer = new Mock(); + writer.Setup(w => w.WriteStartAsync(It.IsAny())).Callback( + (ODataResourceSet feed) => + { + Assert.Null(feed.Count); + Assert.Null(feed.DeltaLink); + Assert.Null(feed.Id); + Assert.Empty(feed.InstanceAnnotations); + Assert.Null(feed.NextPageLink); + }).Returns(Task.CompletedTask).Verifiable(); + Mock ordersSerializer = new Mock(ODataPayloadKind.Resource); + Mock serializerProvider = new Mock(); + serializerProvider.Setup(p => p.GetEdmTypeSerializer(ordersProperty.Type)).Returns(ordersSerializer.Object); + + Mock serializer = new Mock(serializerProvider.Object); + serializer.Setup(s => s.CreateSelectExpandNode(It.IsAny())).Returns(selectExpandNode); + serializer.CallBase = true; + + // Act + await serializer.Object.WriteObjectInlineAsync(customer.Object, _customerType, writer.Object, _writeContext); + + // Assert + writer.Verify(); + } + + [Fact] + public async Task WriteObjectInlineAsync_CanWriteExpandedNavigationProperty_ExpandedSingleValuedNavigationPropertyIsNull() + { + // Arrange + IEdmEntityType orderType = _orderSet.EntityType; + IEdmNavigationProperty customerProperty = orderType.NavigationProperties().Single(p => p.Name == "Customer"); + + Mock order = new Mock(); + object customerValue = null; + order.Setup(c => c.TryGetPropertyValue("Customer", out customerValue)).Returns(true); + order.Setup(c => c.GetEdmType()).Returns(orderType.AsReference()); + + ODataQueryOptionParser parser = new ODataQueryOptionParser(_model, orderType, _orderSet, + new Dictionary { { "$select", "Customer" }, { "$expand", "Customer" } }); + SelectExpandClause selectExpandClause = parser.ParseSelectAndExpand(); + + SelectExpandNode selectExpandNode = new SelectExpandNode + { + ExpandedProperties = new Dictionary() + }; + selectExpandNode.ExpandedProperties[customerProperty] = + selectExpandClause.SelectedItems.OfType().Single(); + + Mock writer = new Mock(); + + writer.Setup(w => w.WriteStartAsync(null as ODataResource)).Verifiable(); + Mock ordersSerializer = new Mock(ODataPayloadKind.Resource); + Mock serializerProvider = new Mock(); + serializerProvider.Setup(p => p.GetEdmTypeSerializer(customerProperty.Type)) + .Returns(ordersSerializer.Object); + + Mock serializer = new Mock(serializerProvider.Object); + serializer.Setup(s => s.CreateSelectExpandNode(It.IsAny())).Returns(selectExpandNode); + serializer.CallBase = true; + + // Act + await serializer.Object.WriteObjectInlineAsync(order.Object, _orderType, writer.Object, _writeContext); + + // Assert + writer.Verify(); + } + + [Fact] + public async Task WriteObjectInlineAsync_CanWriteExpandedNavigationProperty_DerivedExpandedCollectionValuedNavigationPropertyIsNull() + { + // Arrange + IEdmEntityType specialCustomerType = (IEdmEntityType)_specialCustomerType.Definition; + IEdmNavigationProperty specialOrdersProperty = + specialCustomerType.NavigationProperties().Single(p => p.Name == "SpecialOrders"); + + Mock customer = new Mock(); + object specialOrdersValue = null; + customer.Setup(c => c.TryGetPropertyValue("SpecialOrders", out specialOrdersValue)).Returns(true); + customer.Setup(c => c.GetEdmType()).Returns(_specialCustomerType); + + IEdmEntityType customerType = _customerSet.EntityType; + ODataQueryOptionParser parser = new ODataQueryOptionParser( + _model, + customerType, + _customerSet, + new Dictionary + { + { "$select", "Default.SpecialCustomer/SpecialOrders" }, + { "$expand", "Default.SpecialCustomer/SpecialOrders" } + }); + SelectExpandClause selectExpandClause = parser.ParseSelectAndExpand(); + + SelectExpandNode selectExpandNode = new SelectExpandNode + { + ExpandedProperties = new Dictionary() + }; + selectExpandNode.ExpandedProperties[specialOrdersProperty] = + selectExpandClause.SelectedItems.OfType().Single(); + + Mock writer = new Mock(); + writer.Setup(w => w.WriteStartAsync(It.IsAny())).Callback( + (ODataResourceSet feed) => + { + Assert.Null(feed.Count); + Assert.Null(feed.DeltaLink); + Assert.Null(feed.Id); + Assert.Empty(feed.InstanceAnnotations); + Assert.Null(feed.NextPageLink); + }).Returns(Task.CompletedTask).Verifiable(); + Mock ordersSerializer = new Mock(ODataPayloadKind.Resource); + Mock serializerProvider = new Mock(); + serializerProvider.Setup(p => p.GetEdmTypeSerializer(specialOrdersProperty.Type)) + .Returns(ordersSerializer.Object); + + Mock serializer = new Mock(serializerProvider.Object); + serializer.Setup(s => s.CreateSelectExpandNode(It.IsAny())).Returns(selectExpandNode); + serializer.CallBase = true; + + // Act + await serializer.Object.WriteObjectInlineAsync(customer.Object, _customerType, writer.Object, _writeContext); + + // Assert + writer.Verify(); + } + + [Fact] + public async Task WriteObjectInlineAsync_CanWriteExpandedNavigationProperty_DerivedExpandedSingleValuedNavigationPropertyIsNull() + { + // Arrange + IEdmEntityType specialOrderType = (IEdmEntityType)_specialOrderType.Definition; + IEdmNavigationProperty customerProperty = + specialOrderType.NavigationProperties().Single(p => p.Name == "SpecialCustomer"); + + Mock order = new Mock(); + object customerValue = null; + order.Setup(c => c.TryGetPropertyValue("SpecialCustomer", out customerValue)).Returns(true); + order.Setup(c => c.GetEdmType()).Returns(_specialOrderType); + + IEdmEntityType orderType = (IEdmEntityType)_orderType.Definition; + ODataQueryOptionParser parser = new ODataQueryOptionParser( + _model, + orderType, + _orderSet, + new Dictionary + { + { "$select", "Default.SpecialOrder/SpecialCustomer" }, + { "$expand", "Default.SpecialOrder/SpecialCustomer" } + }); + SelectExpandClause selectExpandClause = parser.ParseSelectAndExpand(); + + SelectExpandNode selectExpandNode = new SelectExpandNode + { + ExpandedProperties = new Dictionary() + }; + selectExpandNode.ExpandedProperties[customerProperty] = + selectExpandClause.SelectedItems.OfType().Single(); + + Mock writer = new Mock(); + + writer.Setup(w => w.WriteStartAsync(null as ODataResource)).Verifiable(); + Mock ordersSerializer = new Mock(ODataPayloadKind.Resource); + Mock serializerProvider = new Mock(); + serializerProvider.Setup(p => p.GetEdmTypeSerializer(customerProperty.Type)) + .Returns(ordersSerializer.Object); + + Mock serializer = new Mock(serializerProvider.Object); + serializer.Setup(s => s.CreateSelectExpandNode(It.IsAny())).Returns(selectExpandNode); + serializer.CallBase = true; + + // Act + await serializer.Object.WriteObjectInlineAsync(order.Object, _orderType, writer.Object, _writeContext); + + // Assert + writer.Verify(); + } + + [Fact] + public void CreateResource_ThrowsArgumentNull_SelectExpandNode() + { + // Arrange + Mock serializerProvider = new Mock(); + ODataResourceSerializer serializer = new ODataResourceSerializer(serializerProvider.Object); + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull( + () => serializer.CreateResource(selectExpandNode: null, resourceContext: _entityContext), + "selectExpandNode"); + } + + [Fact] + public void CreateResource_ThrowsArgumentNull_EntityContext() + { + // Arrange + Mock serializerProvider = new Mock(); + ODataResourceSerializer serializer = new ODataResourceSerializer(serializerProvider.Object); + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull( + () => serializer.CreateResource(new SelectExpandNode(), resourceContext: null), + "resourceContext"); + } + + [Fact] + public void CreateComputedProperty_ThrowsArgumentNull_ForInputs() + { + // Arrange + Mock serializerProvider = new Mock(); + ODataResourceSerializer serializer = new ODataResourceSerializer(serializerProvider.Object); + + // Act & Assert + ExceptionAssert.ThrowsArgumentNullOrEmpty(() => serializer.CreateComputedProperty(null, null), "propertyName"); + ExceptionAssert.ThrowsArgumentNull(() => serializer.CreateComputedProperty("any", null), "resourceContext"); + } + + [Fact] + public void CreateResource_Calls_CreateComputedProperty_ForEachSelectComputedProperty() + { + // Arrange + SelectExpandNode selectExpandNode = new SelectExpandNode(); + selectExpandNode.SelectedComputedProperties.Add("Computed1"); + selectExpandNode.SelectedComputedProperties.Add("Computed2"); + + ODataProperty[] properties = new ODataProperty[] { new ODataProperty(), new ODataProperty() }; + Mock serializer = new Mock(_serializerProvider); + serializer.CallBase = true; + + serializer.Setup(s => s.CreateComputedProperty("Computed1", _entityContext)).Returns(properties[0]).Verifiable(); + serializer.Setup(s => s.CreateComputedProperty("Computed2", _entityContext)).Returns(properties[1]).Verifiable(); + + // Act + ODataResource entry = serializer.Object.CreateResource(selectExpandNode, _entityContext); + + // Assert + serializer.Verify(); + Assert.Equal(properties, entry.Properties); + } + + [Fact] + public void CreateResource_Calls_CreateStructuralProperty_ForEachSelectedStructuralProperty() + { + // Arrange + SelectExpandNode selectExpandNode = new SelectExpandNode + { + SelectedStructuralProperties = new HashSet + { + new Mock((IEdmStructuredType)_customerType.Definition,"FirstName", EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.String, false)).Object, + new Mock((IEdmStructuredType)_customerType.Definition,"LastName", EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.String, false)).Object + } + }; + ODataProperty[] properties = new ODataProperty[] { new ODataProperty{Name="FirstName"}, new ODataProperty{Name="LastName"} }; + Mock serializer = new Mock(_serializerProvider); + serializer.CallBase = true; + + serializer + .Setup(s => s.CreateStructuralProperty(selectExpandNode.SelectedStructuralProperties.ElementAt(0), _entityContext)) + .Returns(properties[0]) + .Verifiable(); + serializer + .Setup(s => s.CreateStructuralProperty(selectExpandNode.SelectedStructuralProperties.ElementAt(1), _entityContext)) + .Returns(properties[1]) + .Verifiable(); + + // Act + ODataDeletedResource entry = serializer.Object.CreateDeletedResource(new Uri("http://customers/1",UriKind.Absolute), DeltaDeletedEntryReason.Deleted, selectExpandNode, _entityContext); + + // Assert + serializer.Verify(); + Assert.Equal(properties, entry.Properties); + } + + [Fact] + public void CreateResource_SetsETagToNull_IfRequestIsNull() + { + // Arrange + SelectExpandNode selectExpandNode = new SelectExpandNode + { + SelectedStructuralProperties = new HashSet + { + new Mock().Object, new Mock().Object + } + }; + ODataProperty[] properties = new[] { new ODataProperty(), new ODataProperty() }; + Mock serializer = new Mock(_serializerProvider); + serializer.CallBase = true; + + serializer + .Setup(s => s.CreateStructuralProperty(selectExpandNode.SelectedStructuralProperties.ElementAt(0), _entityContext)) + .Returns(properties[0]); + serializer + .Setup(s => s.CreateStructuralProperty(selectExpandNode.SelectedStructuralProperties.ElementAt(1), _entityContext)) + .Returns(properties[1]); + + // Act + ODataResource entry = serializer.Object.CreateResource(selectExpandNode, _entityContext); + + // Assert + Assert.Null(entry.ETag); + } + + [Fact] + public void CreateResource_SetsETagToNull_IfModelNotHaveConcurrencyProperty() + { + // Arrange + IEdmEntitySet orderSet = _model.EntityContainer.FindEntitySet("Orders"); + Order order = new Order() + { + Name = "Foo", + Shipment = "Bar", + ID = 10, + }; + + _writeContext.NavigationSource = orderSet; + _entityContext = new ResourceContext(_writeContext, orderSet.EntityType.AsReference(), order); + + SelectExpandNode selectExpandNode = new SelectExpandNode + { + SelectedStructuralProperties = new HashSet + { + new Mock().Object, new Mock().Object + } + }; + ODataProperty[] properties = new[] { new ODataProperty(), new ODataProperty() }; + Mock serializer = new Mock(_serializerProvider); + serializer.CallBase = true; + + serializer + .Setup(s => s.CreateStructuralProperty(selectExpandNode.SelectedStructuralProperties.ElementAt(0), _entityContext)) + .Returns(properties[0]); + serializer + .Setup(s => s.CreateStructuralProperty(selectExpandNode.SelectedStructuralProperties.ElementAt(1), _entityContext)) + .Returns(properties[1]); + + //var config = RoutingConfigurationFactory.CreateWithRootContainer("Route"); + var request = RequestFactory.Create(opt => opt.AddRouteComponents("route", _model)); + request.ODataFeature().RoutePrefix = "route"; + _entityContext.Request = request; + + // Act + ODataResource entry = serializer.Object.CreateResource(selectExpandNode, _entityContext); + + // Assert + Assert.Null(entry.ETag); + } + + [Fact] + public void CreateResource_SetsEtagToNotNull_IfWithConcurrencyProperty() + { + // Arrange + Mock mockConcurrencyProperty = new Mock(); + mockConcurrencyProperty.SetupGet(s => s.Name).Returns("City"); + SelectExpandNode selectExpandNode = new SelectExpandNode + { + SelectedStructuralProperties = new HashSet { new Mock().Object, mockConcurrencyProperty.Object } + }; + ODataProperty[] properties = new[] { new ODataProperty(), new ODataProperty() }; + Mock serializer = new Mock(_serializerProvider); + serializer.CallBase = true; + serializer + .Setup(s => s.CreateStructuralProperty(selectExpandNode.SelectedStructuralProperties.ElementAt(0), _entityContext)) + .Returns(properties[0]); + serializer + .Setup(s => s.CreateStructuralProperty(selectExpandNode.SelectedStructuralProperties.ElementAt(1), _entityContext)) + .Returns(properties[1]); + + Mock mockETagHandler = new Mock(); + string tag = "\"'anycity'\""; + EntityTagHeaderValue etagHeaderValue = new EntityTagHeaderValue(tag, isWeak: true); + mockETagHandler.Setup(e => e.CreateETag(It.IsAny>(), It.IsAny())).Returns(etagHeaderValue); + + var request = RequestFactory.Create(opt => opt.AddRouteComponents("route", _model, services => + { + services.AddSingleton(sp => mockETagHandler.Object); + })); + request.ODataFeature().RoutePrefix = "route"; + _entityContext.Request = request; + + // Act + ODataDeletedResource resource = serializer.Object.CreateDeletedResource(new Uri("http://customer/1"),DeltaDeletedEntryReason.Deleted, selectExpandNode, _entityContext); + + // Assert + Assert.Equal(etagHeaderValue.ToString(), resource.ETag); + } + + [Fact] + public void CreateResource_IgnoresProperty_IfCreateStructuralPropertyReturnsNull() + { + // Arrange + SelectExpandNode selectExpandNode = new SelectExpandNode + { + SelectedStructuralProperties = new HashSet { new Mock().Object } + }; + Mock serializer = new Mock(_serializerProvider); + serializer.CallBase = true; + + serializer + .Setup(s => s.CreateStructuralProperty(selectExpandNode.SelectedStructuralProperties.ElementAt(0), _entityContext)) + .Returns(null); + + // Act + ODataResource entry = serializer.Object.CreateResource(selectExpandNode, _entityContext); + + // Assert + serializer.Verify(); + Assert.Empty(entry.Properties); + } + + [Fact] + public void CreateResource_Calls_CreateODataAction_ForEachSelectAction() + { + // Arrange + ODataAction[] actions = new ODataAction[] { new ODataAction(), new ODataAction() }; + SelectExpandNode selectExpandNode = new SelectExpandNode + { + SelectedActions = new HashSet { new Mock().Object, new Mock().Object } + }; + Mock serializer = new Mock(_serializerProvider); + serializer.CallBase = true; + + serializer.Setup(s => s.CreateODataAction(selectExpandNode.SelectedActions.ElementAt(0), _entityContext)).Returns(actions[0]).Verifiable(); + serializer.Setup(s => s.CreateODataAction(selectExpandNode.SelectedActions.ElementAt(1), _entityContext)).Returns(actions[1]).Verifiable(); + + // Act + ODataResource entry = serializer.Object.CreateResource(selectExpandNode, _entityContext); + + // Assert + Assert.Equal(actions, entry.Actions); + serializer.Verify(); + } + + [Fact] + public void CreateResource_Works_ToAppendDynamicProperties_ForOpenEntityType() + { + // Arrange + IEdmModel model = SerializationTestsHelpers.SimpleOpenTypeModel(); + + IEdmEntitySet customers = model.EntityContainer.FindEntitySet("Customers"); + + IEdmEntityType customerType = model.FindDeclaredType("Default.Customer") as IEdmEntityType; + Type simpleOpenCustomer = typeof(SimpleOpenCustomer); + model.SetAnnotationValue(customerType, new ClrTypeAnnotation(simpleOpenCustomer)); + + IEdmComplexType addressType = model.FindDeclaredType("Default.Address") as IEdmComplexType; + Type simpleOpenAddress = typeof(SimpleOpenAddress); + model.SetAnnotationValue(addressType, new ClrTypeAnnotation(simpleOpenAddress)); + + IEdmEnumType enumType = model.FindDeclaredType("Default.SimpleEnum") as IEdmEnumType; + Type simpleEnumType = typeof(SimpleEnum); + model.SetAnnotationValue(enumType, new ClrTypeAnnotation(simpleEnumType)); + + model.SetAnnotationValue(customerType, new DynamicPropertyDictionaryAnnotation( + simpleOpenCustomer.GetProperty("CustomerProperties"))); + + model.SetAnnotationValue(addressType, new DynamicPropertyDictionaryAnnotation( + simpleOpenAddress.GetProperty("Properties"))); + + ODataResourceSerializer serializer = new ODataResourceSerializer(_serializerProvider); + + SelectExpandNode selectExpandNode = new SelectExpandNode(null, customerType, model); + ODataSerializerContext writeContext = new ODataSerializerContext + { + Model = model, + Path = new ODataPath(new EntitySetSegment(customers)) + }; + + SimpleOpenCustomer customer = new SimpleOpenCustomer() + { + CustomerId = 991, + Name = "Name #991", + Address = new SimpleOpenAddress + { + City = "a city", + Street = "a street", + Properties = new Dictionary { { "ArrayProperty", new[] { "15", "14", "13" } } } + }, + CustomerProperties = new Dictionary() + }; + DateTime dateTime = new DateTime(2014, 10, 24, 0, 0, 0, DateTimeKind.Utc); + customer.CustomerProperties.Add("EnumProperty", SimpleEnum.Fourth); + customer.CustomerProperties.Add("GuidProperty", new Guid("181D3A20-B41A-489F-9F15-F91F0F6C9ECA")); + customer.CustomerProperties.Add("ListProperty", new List { 5, 4, 3, 2, 1 }); + customer.CustomerProperties.Add("DateTimeProperty", dateTime); + + ResourceContext resourceContext = new ResourceContext(writeContext, + customerType.ToEdmTypeReference(false) as IEdmEntityTypeReference, customer); + + // Act + ODataResource resource = serializer.CreateResource(selectExpandNode, resourceContext); + + // Assert + Assert.Equal("Default.Customer", resource.TypeName); + Assert.Equal(6, resource.Properties.Count()); + + // Verify the declared properties + ODataProperty street = Assert.IsType(Assert.Single(resource.Properties.Where(p => p.Name == "CustomerId"))); + Assert.Equal(991, street.Value); + + ODataProperty city = Assert.IsType(Assert.Single(resource.Properties.Where(p => p.Name == "Name"))); + Assert.Equal("Name #991", city.Value); + + // Verify the nested open complex property + Assert.Empty(resource.Properties.Where(p => p.Name == "Address")); + + // Verify the dynamic properties + ODataProperty enumProperty = Assert.IsType(Assert.Single(resource.Properties.Where(p => p.Name == "EnumProperty"))); + ODataEnumValue enumValue = Assert.IsType(enumProperty.Value); + Assert.Equal("Fourth", enumValue.Value); + Assert.Equal("Default.SimpleEnum", enumValue.TypeName); + + ODataProperty guidProperty = Assert.IsType(Assert.Single(resource.Properties.Where(p => p.Name == "GuidProperty"))); + Assert.Equal(new Guid("181D3A20-B41A-489F-9F15-F91F0F6C9ECA"), guidProperty.Value); + + ODataProperty listProperty = Assert.IsType(Assert.Single(resource.Properties.Where(p => p.Name == "ListProperty"))); + ODataCollectionValue collectionValue = Assert.IsType(listProperty.Value); + Assert.Equal(new List { 5, 4, 3, 2, 1 }, collectionValue.Items.OfType().ToList()); + Assert.Equal("Collection(Edm.Int32)", collectionValue.TypeName); + + ODataProperty dateTimeProperty = Assert.IsType(Assert.Single(resource.Properties.Where(p => p.Name == "DateTimeProperty"))); + Assert.Equal(new DateTimeOffset(dateTime), dateTimeProperty.Value); + } + + [Fact] + public void CreateResource_Works_ToAppendNullDynamicProperties_ForOpenEntityType() + { + // Arrange + IEdmModel model = SerializationTestsHelpers.SimpleOpenTypeModel(); + + IEdmEntitySet customers = model.EntityContainer.FindEntitySet("Customers"); + + IEdmEntityType customerType = model.FindDeclaredType("Default.Customer") as IEdmEntityType; + Type simpleOpenCustomer = typeof(SimpleOpenCustomer); + model.SetAnnotationValue(customerType, new ClrTypeAnnotation(simpleOpenCustomer)); + + IEdmComplexType addressType = model.FindDeclaredType("Default.Address") as IEdmComplexType; + Type simpleOpenAddress = typeof(SimpleOpenAddress); + model.SetAnnotationValue(addressType, new ClrTypeAnnotation(simpleOpenAddress)); + + IEdmEnumType enumType = model.FindDeclaredType("Default.SimpleEnum") as IEdmEnumType; + Type simpleEnumType = typeof(SimpleEnum); + model.SetAnnotationValue(enumType, new ClrTypeAnnotation(simpleEnumType)); + + model.SetAnnotationValue(customerType, new DynamicPropertyDictionaryAnnotation( + simpleOpenCustomer.GetProperty("CustomerProperties"))); + + model.SetAnnotationValue(addressType, new DynamicPropertyDictionaryAnnotation( + simpleOpenAddress.GetProperty("Properties"))); + + ODataResourceSerializer serializer = new ODataResourceSerializer(_serializerProvider); + + var request = RequestFactory.Create(opt => opt.AddRouteComponents("route", model)); + request.ODataFeature().RoutePrefix = "route"; + SelectExpandNode selectExpandNode = new SelectExpandNode(null, customerType, model); + ODataSerializerContext writeContext = new ODataSerializerContext + { + Model = model, + Path = new ODataPath(new EntitySetSegment(customers)), + Request = request + }; + + SimpleOpenCustomer customer = new SimpleOpenCustomer() + { + CustomerId = 991, + Name = "Name #991", + Address = new SimpleOpenAddress + { + City = "a city", + Street = "a street", + Properties = new Dictionary { { "ArrayProperty", new[] { "15", "14", "13" } } } + }, + CustomerProperties = new Dictionary() + }; + + customer.CustomerProperties.Add("GuidProperty", new Guid("181D3A20-B41A-489F-9F15-F91F0F6C9ECA")); + customer.CustomerProperties.Add("NullProperty", null); + + ResourceContext entityContext = new ResourceContext(writeContext, + customerType.ToEdmTypeReference(false) as IEdmEntityTypeReference, customer); + + // Act + ODataResource resource = serializer.CreateResource(selectExpandNode, entityContext); + + // Assert + Assert.Equal("Default.Customer", resource.TypeName); + Assert.Equal(4, resource.Properties.Count()); + + // Verify the declared properties + ODataProperty street = Assert.IsType(Assert.Single(resource.Properties.Where(p => p.Name == "CustomerId"))); + Assert.Equal(991, street.Value); + + ODataProperty city = Assert.IsType(Assert.Single(resource.Properties.Where(p => p.Name == "Name"))); + Assert.Equal("Name #991", city.Value); + + // Verify the nested open complex property + Assert.Empty(resource.Properties.Where(p => p.Name == "Address")); + + // Verify the dynamic properties + ODataProperty guidProperty = Assert.IsType(Assert.Single(resource.Properties.Where(p => p.Name == "GuidProperty"))); + Assert.Equal(new Guid("181D3A20-B41A-489F-9F15-F91F0F6C9ECA"), guidProperty.Value); + + ODataProperty nullProperty = resource.Properties.OfType().Single(p => p.Name == "NullProperty"); + Assert.Null(nullProperty.Value); + } + + [Fact] + public void CreateResource_Works_ToIgnoreDynamicPropertiesWithEmptyNames_ForOpenEntityType() + { + // Arrange + IEdmModel model = SerializationTestsHelpers.SimpleOpenTypeModel(); + + IEdmEntitySet customers = model.EntityContainer.FindEntitySet("Customers"); + + IEdmEntityType customerType = model.FindDeclaredType("Default.Customer") as IEdmEntityType; + Type simpleOpenCustomer = typeof(SimpleOpenCustomer); + model.SetAnnotationValue(customerType, new ClrTypeAnnotation(simpleOpenCustomer)); + + IEdmComplexType addressType = model.FindDeclaredType("Default.Address") as IEdmComplexType; + Type simpleOpenAddress = typeof(SimpleOpenAddress); + model.SetAnnotationValue(addressType, new ClrTypeAnnotation(simpleOpenAddress)); + + IEdmEnumType enumType = model.FindDeclaredType("Default.SimpleEnum") as IEdmEnumType; + Type simpleEnumType = typeof(SimpleEnum); + model.SetAnnotationValue(enumType, new ClrTypeAnnotation(simpleEnumType)); + + model.SetAnnotationValue(customerType, new DynamicPropertyDictionaryAnnotation( + simpleOpenCustomer.GetProperty("CustomerProperties"))); + + model.SetAnnotationValue(addressType, new DynamicPropertyDictionaryAnnotation( + simpleOpenAddress.GetProperty("Properties"))); + + ODataResourceSerializer serializer = new ODataResourceSerializer(_serializerProvider); + + var request = RequestFactory.Create(opt => opt.AddRouteComponents("route", model)); + request.ODataFeature().RoutePrefix = "route"; + SelectExpandNode selectExpandNode = new SelectExpandNode(null, customerType, model); + ODataSerializerContext writeContext = new ODataSerializerContext + { + Model = model, + Path = new ODataPath(new EntitySetSegment(customers)), + Request = request + }; + + SimpleOpenCustomer customer = new SimpleOpenCustomer() + { + CustomerId = 991, + Name = "Name #991", + Address = new SimpleOpenAddress + { + City = "a city", + Street = "a street", + Properties = new Dictionary { { "ArrayProperty", new[] { "15", "14", "13" } } } + }, + CustomerProperties = new Dictionary() + }; + + customer.CustomerProperties.Add("GuidProperty", new Guid("181D3A20-B41A-489F-9F15-F91F0F6C9ECA")); + customer.CustomerProperties.Add("", "EmptyProperty"); + + ResourceContext entityContext = new ResourceContext(writeContext, + customerType.ToEdmTypeReference(false) as IEdmEntityTypeReference, customer); + + // Act + ODataResource resource = serializer.CreateResource(selectExpandNode, entityContext); + + // Assert + Assert.Equal("Default.Customer", resource.TypeName); + Assert.Equal(3, resource.Properties.Count()); + + // Verify properties with empty names are ignored + ODataProperty emptyProperty = resource.Properties.OfType().SingleOrDefault(p => p.Name == ""); + Assert.Null(emptyProperty); + } + + [Fact] + public void CreateResource_Throws_IfDynamicPropertyUsesExistingName_ForOpenType() + { + // Arrange + IEdmModel model = SerializationTestsHelpers.SimpleOpenTypeModel(); + + IEdmEntitySet customers = model.EntityContainer.FindEntitySet("Customers"); + + IEdmEntityType customerType = model.FindDeclaredType("Default.Customer") as IEdmEntityType; + Type simpleOpenCustomer = typeof(SimpleOpenCustomer); + model.SetAnnotationValue(customerType, new ClrTypeAnnotation(simpleOpenCustomer)); + + IEdmComplexType addressType = model.FindDeclaredType("Default.Address") as IEdmComplexType; + Type simpleOpenAddress = typeof(SimpleOpenAddress); + model.SetAnnotationValue(addressType, new ClrTypeAnnotation(simpleOpenAddress)); + + IEdmEnumType enumType = model.FindDeclaredType("Default.SimpleEnum") as IEdmEnumType; + Type simpleEnumType = typeof(SimpleEnum); + model.SetAnnotationValue(enumType, new ClrTypeAnnotation(simpleEnumType)); + + model.SetAnnotationValue(customerType, new DynamicPropertyDictionaryAnnotation( + simpleOpenCustomer.GetProperty("CustomerProperties"))); + + model.SetAnnotationValue(addressType, new DynamicPropertyDictionaryAnnotation( + simpleOpenAddress.GetProperty("Properties"))); + + ODataResourceSerializer serializer = new ODataResourceSerializer(_serializerProvider); + + var request = RequestFactory.Create(opt => opt.AddRouteComponents("route", model)); + request.ODataFeature().RoutePrefix = "route"; + SelectExpandNode selectExpandNode = new SelectExpandNode(null, customerType, model); + ODataSerializerContext writeContext = new ODataSerializerContext + { + Model = model, + Path = new ODataPath(new EntitySetSegment(customers)), + Request = request + }; + + SimpleOpenCustomer customer = new SimpleOpenCustomer() + { + CustomerId = 991, + Name = "Name #991", + Address = new SimpleOpenAddress + { + City = "a city", + Street = "a street", + Properties = new Dictionary { { "ArrayProperty", new[] { "15", "14", "13" } } } + }, + CustomerProperties = new Dictionary() + }; + + customer.CustomerProperties.Add("GuidProperty", new Guid("181D3A20-B41A-489F-9F15-F91F0F6C9ECA")); + customer.CustomerProperties.Add("Name", "DynamicName"); + + ResourceContext entityContext = new ResourceContext(writeContext, + customerType.ToEdmTypeReference(false) as IEdmEntityTypeReference, customer); + + // Act & Assert + ExceptionAssert.Throws( + () => serializer.CreateResource(selectExpandNode, entityContext), + "Name", + partialMatch: true); + } + + [Fact] + public void CreateResource_Throws_IfNullDynamicPropertyUsesExistingName_ForOpenType() + { + // Arrange + IEdmModel model = SerializationTestsHelpers.SimpleOpenTypeModel(); + + IEdmEntitySet customers = model.EntityContainer.FindEntitySet("Customers"); + + IEdmEntityType customerType = model.FindDeclaredType("Default.Customer") as IEdmEntityType; + Type simpleOpenCustomer = typeof(SimpleOpenCustomer); + model.SetAnnotationValue(customerType, new ClrTypeAnnotation(simpleOpenCustomer)); + + IEdmComplexType addressType = model.FindDeclaredType("Default.Address") as IEdmComplexType; + Type simpleOpenAddress = typeof(SimpleOpenAddress); + model.SetAnnotationValue(addressType, new ClrTypeAnnotation(simpleOpenAddress)); + + IEdmEnumType enumType = model.FindDeclaredType("Default.SimpleEnum") as IEdmEnumType; + Type simpleEnumType = typeof(SimpleEnum); + model.SetAnnotationValue(enumType, new ClrTypeAnnotation(simpleEnumType)); + + model.SetAnnotationValue(customerType, new DynamicPropertyDictionaryAnnotation( + simpleOpenCustomer.GetProperty("CustomerProperties"))); + + model.SetAnnotationValue(addressType, new DynamicPropertyDictionaryAnnotation( + simpleOpenAddress.GetProperty("Properties"))); + + ODataResourceSerializer serializer = new ODataResourceSerializer(_serializerProvider); + + var request = RequestFactory.Create(opt => opt.AddRouteComponents("route", model)); + request.ODataFeature().RoutePrefix = "route"; + SelectExpandNode selectExpandNode = new SelectExpandNode(null, customerType, model); + ODataSerializerContext writeContext = new ODataSerializerContext + { + Model = model, + Path = new ODataPath(new EntitySetSegment(customers)), + Request = request + }; + + SimpleOpenCustomer customer = new SimpleOpenCustomer() + { + CustomerId = 991, + Name = "Name #991", + Address = new SimpleOpenAddress + { + City = "a city", + Street = "a street", + Properties = new Dictionary { { "ArrayProperty", new[] { "15", "14", "13" } } } + }, + CustomerProperties = new Dictionary() + }; + + customer.CustomerProperties.Add("GuidProperty", new Guid("181D3A20-B41A-489F-9F15-F91F0F6C9ECA")); + customer.CustomerProperties.Add("Name", null); + + ResourceContext entityContext = new ResourceContext(writeContext, + customerType.ToEdmTypeReference(false) as IEdmEntityTypeReference, customer); + + // Act & Assert + ExceptionAssert.Throws( + () => serializer.CreateResource(selectExpandNode, entityContext), + "Name", + partialMatch: true); + } + [Fact] + public void CreateUntypedPropertyValue_ThrowsArgumentNull_StructuralProperty() + { + // Arrange + Mock serializerProvider = new Mock(); + ODataResourceSerializer serializer = new ODataResourceSerializer(serializerProvider.Object); + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull( + () => serializer.CreateUntypedPropertyValue(structuralProperty: null, resourceContext: null, out _), + "structuralProperty"); + } + + [Fact] + public void CreateUntypedPropertyValue_ThrowsArgumentNull_ResourceContext() + { + // Arrange + Mock serializerProvider = new Mock(); + ODataResourceSerializer serializer = new ODataResourceSerializer(serializerProvider.Object); + Mock property = new Mock(); + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull( + () => serializer.CreateUntypedPropertyValue(property.Object, resourceContext: null, out _), + "resourceContext"); + } + + [Fact] + public void CreateUntypedPropertyValue_ReturnsNull_ForNonUntypedProperty() + { + // Arrange + Mock serializerProvider = new Mock(); + ODataResourceSerializer serializer = new ODataResourceSerializer(serializerProvider.Object); + Mock property = new Mock(); + property.Setup(p => p.Type).Returns(EdmCoreModel.Instance.GetInt32(true)); + + // Act + object result = serializer.CreateUntypedPropertyValue(property.Object, new ResourceContext(), out _); + + // Assert + Assert.Null(result); + } + + [Fact] + public void CreateUntypedPropertyValue_ReturnsRealValue_ForUntypedPropertyWithRealType() + { + // Arrange + Mock property = new Mock(); + property.Setup(p => p.Name).Returns("PropertyName"); + Mock serializerProvider = new Mock(MockBehavior.Strict); + object propertyValue = new List(); + var entity = new { PropertyName = propertyValue }; + Mock innerSerializer = new Mock(ODataPayloadKind.Property); + IEdmTypeReference propertyType = EdmUntypedStructuredTypeReference.NullableTypeReference; + + property.Setup(p => p.Type).Returns(propertyType); + + var serializer = new ODataResourceSerializer(serializerProvider.Object); + ResourceContext entityContext = new ResourceContext(_writeContext, _customerType, entity); + + // Act + object result = serializer.CreateUntypedPropertyValue(property.Object, entityContext, out IEdmTypeReference actualType); + + // Assert + innerSerializer.Verify(); + Assert.Same(propertyValue, result); + Assert.Same(EdmUntypedHelpers.NullableUntypedCollectionReference, actualType); + } + + [Fact] + public void CreateUntypedPropertyValue_Calls_CreateODataValueOnInnerSerializer() + { + // Arrange + Mock property = new Mock(); + property.Setup(p => p.Name).Returns("PropertyName"); + Mock serializerProvider = new Mock(MockBehavior.Strict); + var entity = new { PropertyName = 42 }; + Mock innerSerializer = new Mock(ODataPayloadKind.Property); + ODataPrimitiveValue propertyValue = new ODataPrimitiveValue(42); + IEdmTypeReference propertyType = EdmUntypedStructuredTypeReference.NullableTypeReference; + IEdmTypeReference actualType = _writeContext.GetEdmType(propertyValue, typeof(int)); + + property.Setup(p => p.Type).Returns(propertyType); + serializerProvider.Setup(s => s.GetEdmTypeSerializer(actualType)).Returns(innerSerializer.Object); + innerSerializer.Setup(s => s.CreateODataValue(42, actualType, _writeContext)).Returns(propertyValue).Verifiable(); + + var serializer = new ODataResourceSerializer(serializerProvider.Object); + ResourceContext entityContext = new ResourceContext(_writeContext, _customerType, entity); + + // Act + object createdProperty = serializer.CreateUntypedPropertyValue(property.Object, entityContext, out _); + + // Assert + innerSerializer.Verify(); + ODataProperty odataProperty = Assert.IsType(createdProperty); + Assert.Equal("PropertyName", odataProperty.Name); + Assert.Equal(42, odataProperty.Value); + } + + [Fact] + public void CreateStructuralProperty_ThrowsArgumentNull_StructuralProperty() + { + // Arrange + Mock serializerProvider = new Mock(); + ODataResourceSerializer serializer = new ODataResourceSerializer(serializerProvider.Object); + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull( + () => serializer.CreateStructuralProperty(structuralProperty: null, resourceContext: null), + "structuralProperty"); + } + + [Fact] + public void CreateStructuralProperty_ThrowsArgumentNull_ResourceContext() + { + // Arrange + Mock serializerProvider = new Mock(); + ODataResourceSerializer serializer = new ODataResourceSerializer(serializerProvider.Object); + Mock property = new Mock(); + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull( + () => serializer.CreateStructuralProperty(property.Object, resourceContext: null), + "resourceContext"); + } + + [Fact] + public void CreateStructuralProperty_ThrowsSerializationException_TypeCannotBeSerialized() + { + // Arrange + Mock propertyType = new Mock(); + propertyType.Setup(t => t.Definition).Returns(new EdmEntityType("Namespace", "Name")); + Mock property = new Mock(); + Mock serializerProvider = new Mock(MockBehavior.Strict); + IEdmEntityObject entity = new Mock().Object; + property.Setup(p => p.Type).Returns(propertyType.Object); + serializerProvider.Setup(s => s.GetEdmTypeSerializer(propertyType.Object)).Returns(null); + + var serializer = new ODataResourceSerializer(serializerProvider.Object); + + // Act & Assert + ExceptionAssert.Throws( + () => serializer.CreateStructuralProperty(property.Object, new ResourceContext { EdmObject = entity }), + "'Namespace.Name' cannot be serialized using the OData output formatter."); + } + + [Fact] + public void CreateStructuralProperty_Calls_CreateODataValueOnInnerSerializer() + { + // Arrange + Mock property = new Mock(); + property.Setup(p => p.Name).Returns("PropertyName"); + Mock serializerProvider = new Mock(MockBehavior.Strict); + var entity = new { PropertyName = 42 }; + Mock innerSerializer = new Mock(ODataPayloadKind.Property); + ODataValue propertyValue = new Mock().Object; + IEdmTypeReference propertyType = _writeContext.GetEdmType(propertyValue, typeof(int)); + + property.Setup(p => p.Type).Returns(propertyType); + serializerProvider.Setup(s => s.GetEdmTypeSerializer(propertyType)).Returns(innerSerializer.Object); + innerSerializer.Setup(s => s.CreateODataValue(42, propertyType, _writeContext)).Returns(propertyValue).Verifiable(); + + var serializer = new ODataResourceSerializer(serializerProvider.Object); + ResourceContext entityContext = new ResourceContext(_writeContext, _customerType, entity); + + // Act + ODataProperty createdProperty = serializer.CreateStructuralProperty(property.Object, entityContext); + + // Assert + innerSerializer.Verify(); + Assert.Equal("PropertyName", createdProperty.Name); + Assert.Equal(propertyValue, createdProperty.Value); + } + + private bool Verify(ResourceContext instanceContext, object instance, ODataSerializerContext writeContext) + { + Assert.Same(instance, (instanceContext.EdmObject as TypedEdmStructuredObject).Instance); + Assert.Equal(writeContext.Model, instanceContext.EdmModel); + Assert.Equal(writeContext.NavigationSource, instanceContext.NavigationSource); + Assert.Equal(writeContext.Request, instanceContext.Request); + Assert.Equal(writeContext.SkipExpensiveAvailabilityChecks, instanceContext.SkipExpensiveAvailabilityChecks); + return true; + } + + [Fact] + public void CreateUntypedNestedResourceInfo_ThrowsArgumentNull_StructuralProperty() + { + // Arrange + Mock serializerProvider = new Mock(); + ODataResourceSerializer serializer = new ODataResourceSerializer(serializerProvider.Object); + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull( + () => serializer.CreateUntypedNestedResourceInfo(null, null, null, null, resourceContext: _entityContext), + "structuralProperty"); + } + + [Fact] + public void CreateUntypedNestedResourceInfo_CreatesCorrectNestedResourceInfo() + { + // Arrange + Mock property = new Mock(); + property.Setup(p => p.Name).Returns("AnyUntypedName"); + + Mock serializer = new Mock(_serializerProvider); + serializer.CallBase = true; + + // Act + ODataNestedResourceInfo untypedNestedResourceInfo + = serializer.Object.CreateUntypedNestedResourceInfo(property.Object, + It.IsAny(), EdmUntypedHelpers.NullableUntypedCollectionReference, null, _entityContext); + + // Assert + Assert.NotNull(untypedNestedResourceInfo); + Assert.Equal("AnyUntypedName", untypedNestedResourceInfo.Name); + Assert.True(untypedNestedResourceInfo.IsCollection); + } + + [Fact] + public void CreateNavigationLink_ThrowsArgumentNull_NavigationProperty() + { + // Arrange + Mock serializerProvider = new Mock(); + ODataResourceSerializer serializer = new ODataResourceSerializer(serializerProvider.Object); + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull( + () => serializer.CreateNavigationLink(navigationProperty: null, resourceContext: _entityContext), + "navigationProperty"); + } + + [Fact] + public void CreateNavigationLink_ThrowsArgumentNull_EntityContext() + { + // Arrange + Mock serializerProvider = new Mock(); + ODataResourceSerializer serializer = new ODataResourceSerializer(serializerProvider.Object); + IEdmNavigationProperty navigationProperty = new Mock().Object; + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull( + () => serializer.CreateNavigationLink(navigationProperty, resourceContext: null), + "resourceContext"); + } + + [Fact] + public void CreateNavigationLink_CreatesCorrectNavigationLink() + { + // Arrange + Uri navigationLinkUri = new Uri("http://navigation_link"); + IEdmNavigationProperty property1 = CreateFakeNavigationProperty("Property1", _customerType); + OData.Edm.NavigationSourceLinkBuilderAnnotation linkAnnotation = new MockNavigationSourceLinkBuilderAnnotation + { + NavigationLinkBuilder = (ctxt, property, metadataLevel) => navigationLinkUri + }; + _model.SetNavigationSourceLinkBuilder(_customerSet, linkAnnotation); + + Mock serializer = new Mock(_serializerProvider); + serializer.CallBase = true; + + // Act + ODataNestedResourceInfo navigationLink = serializer.Object.CreateNavigationLink(property1, _entityContext); + + // Assert + Assert.Equal("Property1", navigationLink.Name); + Assert.Equal(navigationLinkUri, navigationLink.Url); + } + + [Fact] + public void CreateResource_UsesCorrectTypeName() + { + ResourceContext instanceContext = + new ResourceContext { StructuredType = _customerType.EntityDefinition(), SerializerContext = _writeContext }; + Mock serializer = new Mock(_serializerProvider); + serializer.CallBase = true; + SelectExpandNode selectExpandNode = new SelectExpandNode(); + + // Act + ODataResource entry = serializer.Object.CreateResource(selectExpandNode, instanceContext); + + // Assert + Assert.Equal("Default.Customer", entry.TypeName); + } + + [Fact] + public void CreateODataAction_ThrowsArgumentNull_Action() + { + // Arrange + Mock serializerProvider = new Mock(); + ODataResourceSerializer serializer = new ODataResourceSerializer(serializerProvider.Object); + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull( + () => serializer.CreateODataAction(action: null, resourceContext: null), + "action"); + } + + [Fact] + public void CreateODataAction_ThrowsArgumentNull_EntityContext() + { + // Arrange + Mock serializerProvider = new Mock(); + ODataResourceSerializer serializer = new ODataResourceSerializer(serializerProvider.Object); + IEdmAction action = new Mock().Object; + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull( + () => serializer.CreateODataAction(action, resourceContext: null), + "resourceContext"); + } + + [Fact] + public void CreateResource_WritesCorrectIdLink() + { + // Arrange + ResourceContext instanceContext = new ResourceContext + { + SerializerContext = _writeContext, + StructuredType = _customerType.EntityDefinition() + }; + + bool customIdLinkbuilderCalled = false; + OData.Edm.NavigationSourceLinkBuilderAnnotation linkAnnotation = new MockNavigationSourceLinkBuilderAnnotation + { + IdLinkBuilder = new SelfLinkBuilder((ResourceContext context) => + { + Assert.Same(instanceContext, context); + customIdLinkbuilderCalled = true; + return new Uri("http://sample_id_link"); + }, + followsConventions: false) + }; + _model.SetNavigationSourceLinkBuilder(_customerSet, linkAnnotation); + + Mock serializer = new Mock(_serializerProvider); + serializer.CallBase = true; + SelectExpandNode selectExpandNode = new SelectExpandNode(); + + // Act + ODataResource entry = serializer.Object.CreateResource(selectExpandNode, instanceContext); + + // Assert + Assert.True(customIdLinkbuilderCalled); + } + + [Fact] + public void WriteObjectInline_WritesCorrectEditLink() + { + // Arrange + ResourceContext instanceContext = new ResourceContext + { + SerializerContext = _writeContext, + StructuredType = _customerType.EntityDefinition() + }; + bool customEditLinkbuilderCalled = false; + OData.Edm.NavigationSourceLinkBuilderAnnotation linkAnnotation = new MockNavigationSourceLinkBuilderAnnotation + { + EditLinkBuilder = new SelfLinkBuilder((ResourceContext context) => + { + Assert.Same(instanceContext, context); + customEditLinkbuilderCalled = true; + return new Uri("http://sample_edit_link"); + }, + followsConventions: false) + }; + _model.SetNavigationSourceLinkBuilder(_customerSet, linkAnnotation); + + Mock serializer = new Mock(_serializerProvider); + serializer.CallBase = true; + SelectExpandNode selectExpandNode = new SelectExpandNode(); + + // Act + ODataResource entry = serializer.Object.CreateResource(selectExpandNode, instanceContext); + + // Assert + Assert.True(customEditLinkbuilderCalled); + } + + [Fact] + public void WriteObjectInline_WritesCorrectReadLink() + { + // Arrange + ResourceContext instanceContext = new ResourceContext(_writeContext, _customerType, 42); + bool customReadLinkbuilderCalled = false; + OData.Edm.NavigationSourceLinkBuilderAnnotation linkAnnotation = new MockNavigationSourceLinkBuilderAnnotation + { + ReadLinkBuilder = new SelfLinkBuilder((ResourceContext context) => + { + Assert.Same(instanceContext, context); + customReadLinkbuilderCalled = true; + return new Uri("http://sample_read_link"); + }, + followsConventions: false) + }; + + _model.SetNavigationSourceLinkBuilder(_customerSet, linkAnnotation); + + Mock serializer = new Mock(_serializerProvider); + serializer.CallBase = true; + SelectExpandNode selectExpandNode = new SelectExpandNode(); + + // Act + ODataResource entry = serializer.Object.CreateResource(selectExpandNode, instanceContext); + + // Assert + Assert.True(customReadLinkbuilderCalled); + } + + [Fact] + public void CreateSelectExpandNode_ThrowsArgumentNull_EntityContext() + { + // Arrange + Mock serializerProvider = new Mock(); + ODataResourceSerializer serializer = new ODataResourceSerializer(serializerProvider.Object); + + ExceptionAssert.ThrowsArgumentNull( + () => serializer.CreateSelectExpandNode(resourceContext: null), + "resourceContext"); + } + + [Fact] + public void AddTypeNameAnnotationAsNeeded_AddsAnnotation_IfTypeOfPathDoesNotMatchEntryType() + { + // Arrange + string expectedTypeName = "TypeName"; + ODataResource entry = new ODataResource + { + TypeName = expectedTypeName + }; + + // Act + ODataResourceSerializer.AddTypeNameAnnotationAsNeeded(entry, _customerType.EntityDefinition(), ODataMetadataLevel.Minimal); + + // Assert + ODataTypeAnnotation annotation = entry.TypeAnnotation; + Assert.NotNull(annotation); // Guard + Assert.Equal(expectedTypeName, annotation.TypeName); + } + + [Fact] // Issue 984: Redundant type name serialization in OData JSON light minimal metadata mode + public void AddTypeNameAnnotationAsNeeded_AddsAnnotationWithNullValue_IfTypeOfPathMatchesEntryType() + { + // Arrange + CustomersModelWithInheritance model = new CustomersModelWithInheritance(); + ODataResource entry = new ODataResource + { + TypeName = model.SpecialCustomer.FullName() + }; + + // Act + ODataResourceSerializer.AddTypeNameAnnotationAsNeeded(entry, model.SpecialCustomer, ODataMetadataLevel.Minimal); + + // Assert + ODataTypeAnnotation annotation = entry.TypeAnnotation; + Assert.NotNull(annotation); // Guard + Assert.Null(annotation.TypeName); + } + + [Theory] + [InlineData("MatchingType", "MatchingType", ODataMetadataLevel.Full, false)] + [InlineData("DoesNotMatch1", "DoesNotMatch2", ODataMetadataLevel.Full, false)] + [InlineData("MatchingType", "MatchingType", ODataMetadataLevel.Minimal, true)] + [InlineData("DoesNotMatch1", "DoesNotMatch2", ODataMetadataLevel.Minimal, false)] + [InlineData("MatchingType", "MatchingType", ODataMetadataLevel.None, true)] + [InlineData("DoesNotMatch1", "DoesNotMatch2", ODataMetadataLevel.None, true)] + public void ShouldSuppressTypeNameSerialization(string resourceType, string entitySetType, + ODataMetadataLevel metadataLevel, bool expectedResult) + { + // Arrange + ODataResource resource = new ODataResource + { + // The caller uses a namespace-qualified name, which this test leaves empty. + TypeName = "NS." + resourceType + }; + IEdmEntityType edmType = CreateEntityTypeWithName(entitySetType); + + // Act + bool actualResult = ODataResourceSerializer.ShouldSuppressTypeNameSerialization(resource, edmType, metadataLevel); + + // Assert + Assert.Equal(expectedResult, actualResult); + } + + [Fact] + public void CreateODataAction_IncludesEverything_ForFullMetadata() + { + // Arrange + string expectedContainerName = "Container"; + string expectedNamespace = "NS"; + string expectedActionName = "Action"; + string expectedTarget = "aa://Target"; + string expectedMetadataPrefix = "http://Metadata"; + + IEdmEntityContainer container = CreateFakeContainer(expectedContainerName); + IEdmAction action = CreateFakeAction(expectedNamespace, expectedActionName, isBindable: true); + + OperationLinkBuilder linkBuilder = new OperationLinkBuilder((ResourceContext a) => new Uri(expectedTarget), + followsConventions: false); + IEdmDirectValueAnnotationsManager annotationsManager = CreateFakeAnnotationsManager(); + annotationsManager.SetOperationLinkBuilder(action, linkBuilder); + annotationsManager.SetIsAlwaysBindable(action); + IEdmModel model = CreateFakeModel(annotationsManager); + + ResourceContext context = CreateContext(model, expectedMetadataPrefix); + context.SerializerContext.MetadataLevel = ODataMetadataLevel.Full; + + // Act + ODataAction actualAction = _serializer.CreateODataAction(action, context); + + // Assert + // In ASP.NET Core, it's using the global UriHelper to pick up the first Router to generate the Uri. + // Owing that there's no router created in this case, a default OData router will be used to generate the Uri. + // The default OData router will add '$metadata' after the route prefix. + string expectedMetadata = expectedMetadataPrefix + "/OData/$metadata#" + expectedNamespace + "." + expectedActionName; + ODataAction expectedAction = new ODataAction + { + Metadata = new Uri(expectedMetadata), + Target = new Uri(expectedTarget), + Title = expectedActionName + }; + + AssertEqual(expectedAction, actualAction); + } + + [Fact] + public void CreateODataAction_OmitsAction_WheOperationLinkBuilderReturnsNull() + { + // Arrange + IEdmAction action = CreateFakeAction("IgnoreAction"); + + OperationLinkBuilder linkBuilder = new OperationLinkBuilder((ResourceContext a) => null, followsConventions: false); + IEdmDirectValueAnnotationsManager annotationsManager = CreateFakeAnnotationsManager(); + annotationsManager.SetOperationLinkBuilder(action, linkBuilder); + + IEdmModel model = CreateFakeModel(annotationsManager); + + ResourceContext context = CreateContext(model); + context.SerializerContext.MetadataLevel = ODataMetadataLevel.Minimal; + + // Act + ODataAction actualAction = _serializer.CreateODataAction(action, context); + + // Assert + Assert.Null(actualAction); + } + + [Fact] + public void CreateODataAction_ForJsonLight_OmitsContainerName_PerCreateMetadataFragment() + { + // Arrange + string expectedMetadataPrefix = "http://Metadata"; + string expectedNamespace = "NS"; + string expectedActionName = "Action"; + + IEdmEntityContainer container = CreateFakeContainer("ContainerShouldNotAppearInResult"); + IEdmAction action = CreateFakeAction(expectedNamespace, expectedActionName); + + OperationLinkBuilder linkBuilder = new OperationLinkBuilder((ResourceContext a) => new Uri("aa://IgnoreTarget"), + followsConventions: false); + IEdmDirectValueAnnotationsManager annotationsManager = CreateFakeAnnotationsManager(); + annotationsManager.SetOperationLinkBuilder(action, linkBuilder); + + IEdmModel model = CreateFakeModel(annotationsManager); + + ResourceContext context = CreateContext(model, expectedMetadataPrefix); + context.SerializerContext.MetadataLevel = ODataMetadataLevel.Minimal; + + // Act + ODataAction actualAction = _serializer.CreateODataAction(action, context); + + // Assert + Assert.NotNull(actualAction); + // Assert + // In ASP.NET Core, it's using the global UriHelper to pick up the first Router to generate the Uri. + // Owing that there's no router created in this case, a default OData router will be used to generate the Uri. + // The default OData router will add '$metadata' after the route prefix. + string expectedMetadata = expectedMetadataPrefix + "/OData/$metadata#" + expectedNamespace + "." + expectedActionName; + AssertEqual(new Uri(expectedMetadata), actualAction.Metadata); + } + + [Fact] + public void CreateODataAction_SkipsAlwaysAvailableAction_PerShouldOmitAction() + { + // Arrange + IEdmAction action = CreateFakeAction("action"); + + OperationLinkBuilder linkBuilder = new OperationLinkBuilder((ResourceContext a) => new Uri("aa://IgnoreTarget"), + followsConventions: true); + IEdmDirectValueAnnotationsManager annotationsManager = CreateFakeAnnotationsManager(); + annotationsManager.SetOperationLinkBuilder(action, linkBuilder); + annotationsManager.SetIsAlwaysBindable(action); + + IEdmModel model = CreateFakeModel(annotationsManager); + + ResourceContext context = CreateContext(model); + context.SerializerContext.MetadataLevel = ODataMetadataLevel.Minimal; + + // Act + ODataAction actualAction = _serializer.CreateODataAction(action, context); + + // Assert + Assert.Null(actualAction); + } + + [Fact] + public void CreateODataAction_IncludesTitle() + { + // Arrange + string expectedActionName = "Action"; + + IEdmAction action = CreateFakeAction(expectedActionName); + + OperationLinkBuilder linkBuilder = new OperationLinkBuilder((ResourceContext a) => new Uri("aa://IgnoreTarget"), + followsConventions: false); + IEdmDirectValueAnnotationsManager annotationsManager = CreateFakeAnnotationsManager(); + annotationsManager.SetOperationLinkBuilder(action, linkBuilder); + + IEdmModel model = CreateFakeModel(annotationsManager); + + ResourceContext context = CreateContext(model, "http://IgnoreMetadataPath"); + context.SerializerContext.MetadataLevel = ODataMetadataLevel.Full; + + // Act + ODataAction actualAction = _serializer.CreateODataAction(action, context); + + // Assert + Assert.NotNull(actualAction); + Assert.Equal(expectedActionName, actualAction.Title); + } + + [Theory] + [InlineData(ODataMetadataLevel.Minimal)] + [InlineData(ODataMetadataLevel.None)] + public void CreateODataAction_OmitsTitle(ODataMetadataLevel metadataLevel) + { + // Arrange + IEdmAction action = CreateFakeAction("IgnoreAction"); + + OperationLinkBuilder linkBuilder = new OperationLinkBuilder((ResourceContext a) => new Uri("aa://Ignore"), + followsConventions: false); + IEdmDirectValueAnnotationsManager annotationsManager = CreateFakeAnnotationsManager(); + annotationsManager.SetOperationLinkBuilder(action, linkBuilder); + + IEdmModel model = CreateFakeModel(annotationsManager); + + ResourceContext context = CreateContext(model, "http://IgnoreMetadataPath"); + context.SerializerContext.MetadataLevel = (ODataMetadataLevel)metadataLevel; + + // Act + ODataAction actualAction = _serializer.CreateODataAction(action, context); + + // Assert + Assert.NotNull(actualAction); + Assert.Null(actualAction.Title); + } + + [Fact] + public void CreateODataAction_IncludesTarget_IfDoesnotFollowODataConvention() + { + // Arrange + Uri expectedTarget = new Uri("aa://Target"); + + IEdmAction action = CreateFakeAction("IgnoreAction"); + + OperationLinkBuilder linkBuilder = new OperationLinkBuilder((ResourceContext a) => expectedTarget, followsConventions: false); + IEdmDirectValueAnnotationsManager annotationsManager = CreateFakeAnnotationsManager(); + annotationsManager.SetOperationLinkBuilder(action, linkBuilder); + + IEdmModel model = CreateFakeModel(annotationsManager); + + ResourceContext context = CreateContext(model, "http://IgnoreMetadataPath"); + context.SerializerContext.MetadataLevel = ODataMetadataLevel.Full; + + // Act + ODataAction actualAction = _serializer.CreateODataAction(action, context); + + // Assert + Assert.NotNull(actualAction); + Assert.Equal(expectedTarget, actualAction.Target); + } + + [Theory] + [InlineData(ODataMetadataLevel.Minimal)] + [InlineData(ODataMetadataLevel.None)] + public void CreateODataAction_OmitsAction_WhenFollowingConventions(ODataMetadataLevel metadataLevel) + { + // Arrange + IEdmAction action = CreateFakeAction("IgnoreAction", isBindable: true); + + OperationLinkBuilder linkBuilder = new OperationLinkBuilder((ResourceContext a) => new Uri("aa://Ignore"), + followsConventions: true); + IEdmDirectValueAnnotationsManager annotationsManager = CreateFakeAnnotationsManager(); + annotationsManager.SetOperationLinkBuilder(action, linkBuilder); + + IEdmModel model = CreateFakeModel(annotationsManager); + + ResourceContext context = CreateContext(model, "http://IgnoreMetadataPath"); + context.SerializerContext.MetadataLevel = metadataLevel; + + // Act + ODataAction actualAction = _serializer.CreateODataAction(action, context); + + // Assert + Assert.Null(actualAction); + } + + [Fact] + public void CreateODataFunction_IncludesEverything_ForFullMetadata() + { + // Arrange + string expectedTarget = "aa://Target"; + string expectedMetadataPrefix = "http://Metadata"; + + IEdmTypeReference returnType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Boolean, isNullable: false); + IEdmFunction function = new EdmFunction("NS", "Function", returnType, isBound: true, entitySetPathExpression: null, isComposable: false); + + OperationLinkBuilder linkBuilder = new OperationLinkBuilder((ResourceContext a) => new Uri(expectedTarget), followsConventions: false); + IEdmDirectValueAnnotationsManager annotationsManager = CreateFakeAnnotationsManager(); + annotationsManager.SetOperationLinkBuilder(function, linkBuilder); + annotationsManager.SetIsAlwaysBindable(function); + IEdmModel model = CreateFakeModel(annotationsManager); + + ResourceContext context = CreateContext(model, expectedMetadataPrefix); + context.SerializerContext.MetadataLevel = ODataMetadataLevel.Full; + + // Act + ODataFunction actualFunction = _serializer.CreateODataFunction(function, context); + + // Assert + string expectedMetadata = expectedMetadataPrefix + "#NS.Function"; + ODataFunction expectedFunction = new ODataFunction + { + Metadata = new Uri(expectedMetadata), + Target = new Uri(expectedTarget), + Title = "Function" + }; + + AssertEqual(actualFunction, actualFunction); + } + + [Fact] + public void CreateODataFunction_IncludesTitle() + { + // Arrange + string expectedActionName = "Function"; + + IEdmTypeReference returnType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Boolean, isNullable: false); + IEdmFunction function = new EdmFunction("NS", "Function", returnType, isBound: true, entitySetPathExpression: null, isComposable: false); + + OperationLinkBuilder linkBuilder = new OperationLinkBuilder((ResourceContext a) => new Uri("aa://IgnoreTarget"), + followsConventions: false); + IEdmDirectValueAnnotationsManager annotationsManager = CreateFakeAnnotationsManager(); + annotationsManager.SetOperationLinkBuilder(function, linkBuilder); + + IEdmModel model = CreateFakeModel(annotationsManager); + + ResourceContext context = CreateContext(model, "http://IgnoreMetadataPath"); + context.SerializerContext.MetadataLevel = ODataMetadataLevel.Full; + + // Act + ODataFunction actualFunction = _serializer.CreateODataFunction(function, context); + + // Assert + Assert.NotNull(actualFunction); + Assert.Equal(expectedActionName, actualFunction.Title); + } + + [Theory] + [InlineData(ODataMetadataLevel.Minimal)] + [InlineData(ODataMetadataLevel.None)] + public void CreateODataFunction_OmitsTitle(ODataMetadataLevel metadataLevel) + { + // Arrange + IEdmTypeReference returnType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Boolean, isNullable: false); + IEdmFunction function = new EdmFunction("NS", "Function", returnType, isBound: true, entitySetPathExpression: null, isComposable: false); + + OperationLinkBuilder linkBuilder = new OperationLinkBuilder((ResourceContext a) => new Uri("aa://Ignore"), + followsConventions: false); + IEdmDirectValueAnnotationsManager annotationsManager = CreateFakeAnnotationsManager(); + annotationsManager.SetOperationLinkBuilder(function, linkBuilder); + + IEdmModel model = CreateFakeModel(annotationsManager); + + ResourceContext context = CreateContext(model, "http://IgnoreMetadataPath"); + context.SerializerContext.MetadataLevel = metadataLevel; + + // Act + ODataFunction actualFunction = _serializer.CreateODataFunction(function, context); + + // Assert + Assert.NotNull(actualFunction); + Assert.Null(actualFunction.Title); + } + + [Fact] + public void CreateODataFunction_IncludesTarget_IfDoesnotFollowODataConvention() + { + // Arrange + Uri expectedTarget = new Uri("aa://Target"); + IEdmTypeReference returnType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Boolean, isNullable: false); + IEdmFunction function = new EdmFunction("NS", "Function", returnType, isBound: true, entitySetPathExpression: null, isComposable: false); + + OperationLinkBuilder linkBuilder = new OperationLinkBuilder((ResourceContext a) => expectedTarget, followsConventions: false); + IEdmDirectValueAnnotationsManager annotationsManager = CreateFakeAnnotationsManager(); + annotationsManager.SetOperationLinkBuilder(function, linkBuilder); + + IEdmModel model = CreateFakeModel(annotationsManager); + + ResourceContext context = CreateContext(model, "http://IgnoreMetadataPath"); + context.SerializerContext.MetadataLevel = ODataMetadataLevel.Full; + + // Act + ODataFunction actualFunction = _serializer.CreateODataFunction(function, context); + + // Assert + Assert.NotNull(actualFunction); + Assert.Equal(expectedTarget, actualFunction.Target); + } + + [Theory] + [InlineData(ODataMetadataLevel.Minimal)] + [InlineData(ODataMetadataLevel.None)] + public void CreateODataFunction_OmitsAction_WhenFollowingConventions(ODataMetadataLevel metadataLevel) + { + // Arrange + IEdmTypeReference returnType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Boolean, isNullable: false); + IEdmFunction function = new EdmFunction("NS", "Function", returnType, isBound: true, entitySetPathExpression: null, isComposable: false); + + OperationLinkBuilder linkBuilder = new OperationLinkBuilder((ResourceContext a) => new Uri("aa://Ignore"), + followsConventions: true); + IEdmDirectValueAnnotationsManager annotationsManager = CreateFakeAnnotationsManager(); + annotationsManager.SetOperationLinkBuilder(function, linkBuilder); + + IEdmModel model = CreateFakeModel(annotationsManager); + + ResourceContext context = CreateContext(model, "http://IgnoreMetadataPath"); + context.SerializerContext.MetadataLevel = metadataLevel; + + // Act + ODataFunction actualFunction = _serializer.CreateODataFunction(function, context); + + // Assert + Assert.Null(actualFunction); + } + + [Fact] + public void CreateMetadataFragment_IncludesNamespaceAndName() + { + // Arrange + string expectedActionName = "Action"; + string expectedNamespace = "NS"; + + IEdmEntityContainer container = CreateFakeContainer("ContainerShouldNotAppearInResult"); + IEdmAction action = CreateFakeAction(expectedNamespace, expectedActionName); + + IEdmDirectValueAnnotationsManager annotationsManager = CreateFakeAnnotationsManager(); + IEdmModel model = CreateFakeModel(annotationsManager); + + // Act + string actualFragment = ODataResourceSerializer.CreateMetadataFragment(action); + + // Assert + Assert.Equal(expectedNamespace + "." + expectedActionName, actualFragment); + } + + [Theory] + [InlineData(ODataMetadataLevel.Full, false, false)] + [InlineData(ODataMetadataLevel.Full, true, false)] + [InlineData(ODataMetadataLevel.Minimal, false, false)] + [InlineData(ODataMetadataLevel.Minimal, true, true)] + [InlineData(ODataMetadataLevel.None, false, false)] + [InlineData(ODataMetadataLevel.None, true, true)] + public void TestShouldOmitAction(ODataMetadataLevel metadataLevel, + bool followsConventions, bool expectedResult) + { + // Arrange + IEdmActionImport action = CreateFakeActionImport(true); + IEdmDirectValueAnnotationsManager annonationsManager = CreateFakeAnnotationsManager(); + + IEdmModel model = CreateFakeModel(annonationsManager); + + OperationLinkBuilder builder = new OperationLinkBuilder((ResourceContext a) => { throw new NotImplementedException(); }, + followsConventions); + + // Act + bool actualResult = ODataResourceSerializer.ShouldOmitOperation(action.Action, builder, metadataLevel); + + // Assert + Assert.Equal(expectedResult, actualResult); + } + + [Fact] + public void TestSetTitleAnnotation() + { + // Arrange + IEdmActionImport action = CreateFakeActionImport(true); + IEdmDirectValueAnnotationsManager annonationsManager = CreateFakeAnnotationsManager(); + IEdmModel model = CreateFakeModel(annonationsManager); + string expectedTitle = "The title"; + model.SetOperationTitleAnnotation(action.Operation, new OperationTitleAnnotation(expectedTitle)); + ODataAction odataAction = new ODataAction(); + + // Act + ODataResourceSerializer.EmitTitle(model, action.Operation, odataAction); + + // Assert + Assert.Equal(expectedTitle, odataAction.Title); + } + + [Fact] + public void TestSetTitleAnnotation_UsesNameIfNoTitleAnnotationIsPresent() + { + // Arrange + IEdmActionImport action = CreateFakeActionImport(CreateFakeContainer("Container"), "Action"); + IEdmDirectValueAnnotationsManager annonationsManager = CreateFakeAnnotationsManager(); + IEdmModel model = CreateFakeModel(annonationsManager); + ODataAction odataAction = new ODataAction(); + + // Act + ODataResourceSerializer.EmitTitle(model, action.Operation, odataAction); + + // Assert + Assert.Equal(action.Operation.Name, odataAction.Title); + } + + [Fact] + public async Task WriteObjectInlineAsync_SetsParentContext_ForExpandedNavigationProperties() + { + // Arrange + ODataWriter mockWriter = new Mock().Object; + Mock expandedItemSerializer = new Mock(ODataPayloadKind.ResourceSet); + Mock serializerProvider = new Mock(); + serializerProvider.Setup(p => p.GetEdmTypeSerializer(It.IsAny())) + .Returns(expandedItemSerializer.Object); + + SelectExpandNode selectExpandNode = new SelectExpandNode + { + ExpandedProperties = new Dictionary + { + { _ordersNavigation, null } + } + }; + Mock serializer = new Mock(serializerProvider.Object); + serializer.Setup(s => s.CreateSelectExpandNode(It.IsAny())).Returns(selectExpandNode); + serializer.Setup(s => s.CreateResource(selectExpandNode, _entityContext)).Returns(new ODataResource()); + serializer.CallBase = true; + + // Act + await serializer.Object.WriteObjectInlineAsync(_customer, _customerType, mockWriter, _writeContext); + + // Assert + expandedItemSerializer.Verify( + s => s.WriteObjectInlineAsync(It.IsAny(), It.IsAny(), mockWriter, + It.Is(c => c.ExpandedResource.SerializerContext == _writeContext))); + } + + [Fact] + public async Task WriteObjectInlineAsync_Writes_Nested_Entities_Without_NavigationSource() + { + // Arrange + ODataModelBuilder builder = new ODataConventionModelBuilder(); + builder.Namespace = "Default"; + builder.EntityType(); + builder.ComplexType(); + var model = builder.GetEdmModel(); + + var result = new Result + { + Title = "myResult", + Products = new Product[] + { + new Product + { + ProductID = 1 + }, + new Product + { + ProductID = 2 + } + } + }; + + var resultType = model.FindType("Default.Result") as IEdmComplexType; + var resultTypeReference = new EdmComplexTypeReference(resultType as IEdmComplexType, false); + var titleProperty = resultTypeReference.FindProperty("Title") as IEdmStructuralProperty; + var productsProperty = resultTypeReference.FindNavigationProperty("Products"); + var selectExpand = new SelectExpandClause(new SelectItem[] + { + new PathSelectItem(new ODataSelectPath(new PropertySegment(titleProperty))), + new ExpandedNavigationSelectItem(new ODataExpandPath(new NavigationPropertySegment(productsProperty, null)),null,null) + }, + false); + + var writeContext = new ODataSerializerContext() + { + Model = model, + SelectExpandClause = selectExpand + }; + + using (var stream = new MemoryStream()) + { + IODataResponseMessage responseMessage = new ODataMessageWrapper(stream); + ODataUri uri = new ODataUri { ServiceRoot = new Uri("http://myService", UriKind.Absolute) }; + ODataMessageWriterSettings settings = new ODataMessageWriterSettings + { + ODataUri = uri + }; + + using (ODataMessageWriter messageWriter = new ODataMessageWriter(responseMessage, settings)) + { + ODataWriter writer = await messageWriter.CreateODataResourceWriterAsync(null, resultType as IEdmComplexType); + ODataResourceSerializer serializer = new ODataResourceSerializer(_serializerProvider); + + // Act + await serializer.WriteObjectInlineAsync(result, resultTypeReference, writer, writeContext); + + // Assert + stream.Position = 0; + using (StreamReader reader = new StreamReader(stream, leaveOpen: true)) + { + string response = reader.ReadToEnd(); + Assert.Contains(@"""ProductID"":1", response); + Assert.Contains(@"""ProductID"":2", response); + } + } + } + } + + [Fact] + public void CreateSelectExpandNode_Caches_SelectExpandNode() + { + // Arrange + IEdmEntityTypeReference customerType = _customerSet.EntityType.AsReference(); + ResourceContext entity1 = new ResourceContext(_writeContext, customerType, new Customer()); + ResourceContext entity2 = new ResourceContext(_writeContext, customerType, new Customer()); + + // Act + var selectExpandNode1 = _serializer.CreateSelectExpandNode(entity1); + var selectExpandNode2 = _serializer.CreateSelectExpandNode(entity2); + + // Assert + Assert.Same(selectExpandNode1, selectExpandNode2); + } + + [Fact] + public void CreateSelectExpandNode_ReturnsDifferentSelectExpandNode_IfEntityTypeIsDifferent() + { + // Arrange + IEdmEntityType customerType = _customerSet.EntityType; + IEdmEntityType derivedCustomerType = new EdmEntityType("NS", "DerivedCustomer", customerType); + + ResourceContext entity1 = new ResourceContext(_writeContext, customerType.AsReference(), new Customer()); + ResourceContext entity2 = new ResourceContext(_writeContext, derivedCustomerType.AsReference(), new Customer()); + + // Act + var selectExpandNode1 = _serializer.CreateSelectExpandNode(entity1); + var selectExpandNode2 = _serializer.CreateSelectExpandNode(entity2); + + // Assert + Assert.NotSame(selectExpandNode1, selectExpandNode2); + } + + [Fact] + public void CreateSelectExpandNode_ReturnsDifferentSelectExpandNode_IfSelectExpandClauseIsDifferent() + { + // Arrange + IEdmEntityType customerType = _customerSet.EntityType; + + ResourceContext entity1 = new ResourceContext(_writeContext, customerType.AsReference(), new Customer()); + ResourceContext entity2 = new ResourceContext(_writeContext, customerType.AsReference(), new Customer()); + + // Act + _writeContext.SelectExpandClause = new SelectExpandClause(new SelectItem[0], allSelected: true); + var selectExpandNode1 = _serializer.CreateSelectExpandNode(entity1); + _writeContext.SelectExpandClause = new SelectExpandClause(new SelectItem[0], allSelected: false); + var selectExpandNode2 = _serializer.CreateSelectExpandNode(entity2); + + // Assert + Assert.NotSame(selectExpandNode1, selectExpandNode2); + } + + private static IEdmNavigationProperty CreateFakeNavigationProperty(string name, IEdmTypeReference type) + { + Mock property = new Mock(); + property.Setup(p => p.Name).Returns(name); + property.Setup(p => p.Type).Returns(type); + return property.Object; + } + + private static void AssertEqual(ODataOperation expected, ODataOperation actual) + { + if (expected == null) + { + Assert.Null(actual); + return; + } + + Assert.NotNull(actual); + AssertEqual(expected.Metadata, actual.Metadata); + AssertEqual(expected.Target, actual.Target); + Assert.Equal(expected.Title, actual.Title); + } + + private static void AssertEqual(Uri expected, Uri actual) + { + if (expected == null) + { + Assert.Null(actual); + return; + } + + Assert.NotNull(actual); + Assert.Equal(expected.AbsoluteUri, actual.AbsoluteUri); + } + + private static ResourceContext CreateContext(IEdmModel model) + { + return new ResourceContext + { + EdmModel = model + }; + } + + private static ResourceContext CreateContext(IEdmModel model, string expectedMetadataPrefix) + { + var request = RequestFactory.Create("get", expectedMetadataPrefix, opt => opt.AddRouteComponents("OData", model)); + request.ODataFeature().RoutePrefix = "OData"; + request.ODataFeature().Model = model; + return new ResourceContext + { + EdmModel = model, + Request = request + }; + } + + private static IEdmEntityType CreateEntityTypeWithName(string typeName) + { + IEdmEntityType entityType = new EdmEntityType("NS", typeName); + return entityType; + } + + private static IEdmDirectValueAnnotationsManager CreateFakeAnnotationsManager() + { + return new FakeAnnotationsManager(); + } + + private static IEdmEntityContainer CreateFakeContainer(string name) + { + Mock mock = new Mock(); + mock.Setup(o => o.Name).Returns(name); + return mock.Object; + } + + private static IEdmActionImport CreateFakeActionImport(IEdmEntityContainer container, string name) + { + Mock mockAction = new Mock(); + mockAction.Setup(o => o.IsBound).Returns(true); + mockAction.Setup(o => o.Name).Returns(name); + Mock mock = new Mock(); + mock.Setup(o => o.Container).Returns(container); + mock.Setup(o => o.Name).Returns(name); + mock.Setup(o => o.Action).Returns(mockAction.Object); + mock.Setup(o => o.Operation).Returns(mockAction.Object); + return mock.Object; + } + + private static IEdmActionImport CreateFakeActionImport(IEdmEntityContainer container, string name, bool isBindable) + { + Mock mock = new Mock(); + mock.Setup(o => o.Container).Returns(container); + mock.Setup(o => o.Name).Returns(name); + Mock mockAction = new Mock(); + mockAction.Setup(o => o.IsBound).Returns(isBindable); + mock.Setup(o => o.Action).Returns(mockAction.Object); + return mock.Object; + } + + private static IEdmActionImport CreateFakeActionImport(bool isBindable) + { + Mock mock = new Mock(); + Mock mockAction = new Mock(); + mockAction.Setup(o => o.IsBound).Returns(isBindable); + mock.Setup(o => o.Action).Returns(mockAction.Object); + mock.Setup(o => o.Operation).Returns(mockAction.Object); + return mock.Object; + } + + private static IEdmAction CreateFakeAction(string name) + { + return CreateFakeAction(nameSpace: null, name: name, isBindable: true); + } + + private static IEdmAction CreateFakeAction(string name, bool isBindable) + { + return CreateFakeAction(nameSpace: null, name: name, isBindable: isBindable); + } + + private static IEdmAction CreateFakeAction(string nameSpace, string name) + { + return CreateFakeAction(nameSpace, name, isBindable: true); + } + + private static IEdmAction CreateFakeAction(string nameSpace, string name, bool isBindable) + { + Mock mockAction = new Mock(); + mockAction.SetupGet(o => o.Namespace).Returns(nameSpace); + mockAction.SetupGet(o => o.Name).Returns(name); + mockAction.Setup(o => o.IsBound).Returns(isBindable); + Mock mockParameter = new Mock(); + mockParameter.SetupGet(o => o.DeclaringOperation).Returns(mockAction.Object); + Mock mockEntityTyeRef = new Mock(); + mockEntityTyeRef.Setup(o => o.Definition).Returns(new Mock().Object); + mockParameter.SetupGet(o => o.Type).Returns(mockEntityTyeRef.Object); + mockAction.SetupGet(o => o.Parameters).Returns(new[] { mockParameter.Object }); + return mockAction.Object; + } + + private static IEdmModel CreateFakeModel(IEdmDirectValueAnnotationsManager annotationsManager) + { + Mock model = new Mock(); + model.Setup(m => m.DirectValueAnnotationsManager).Returns(annotationsManager); + return model.Object; + } + + private static IServiceProvider GetServiceProvider() + { + IServiceCollection services = new ServiceCollection(); + + services.AddSingleton(); + + // Serializers. + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + return services.BuildServiceProvider(); + } + + private class Customer + { + public Customer() + { + this.Orders = new List(); + } + public int ID { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } + public string City { get; set; } + public Size Size { get; set; } + public IList Orders { get; private set; } + } + + private class Size() + { + public decimal Height { get; set; } + public decimal Weight { get; set; } + } + + + private class SpecialCustomer + { + public IList SpecialOrders { get; private set; } + } + + private class Order + { + public int ID { get; set; } + public string Name { get; set; } + public string Shipment { get; set; } + public Customer Customer { get; set; } + } + + private class Result + { + public string Title { get; set; } + public IList Products { get; set; } + } + + private class SpecialOrder + { + public SpecialCustomer SpecialCustomer { get; set; } + } + + private class FakeBindableOperationFinder : BindableOperationFinder + { + private IEdmOperation[] _operations; + + public FakeBindableOperationFinder(params IEdmOperation[] operations) + : base(EdmCoreModel.Instance) + { + _operations = operations; + } + + public override IEnumerable FindOperations(IEdmEntityType entityType) + { + return _operations; + } + } + + private class FakeAnnotationsManager : IEdmDirectValueAnnotationsManager + { + IDictionary, object> annotations = + new Dictionary, object>(); + + public object GetAnnotationValue(IEdmElement element, string namespaceName, string localName) + { + object value; + + if (!annotations.TryGetValue(CreateKey(element, namespaceName, localName), out value)) + { + return null; + } + + return value; + } + + public object[] GetAnnotationValues(IEnumerable annotations) + { + throw new NotImplementedException(); + } + + public IEnumerable GetDirectValueAnnotations(IEdmElement element) + { + throw new NotImplementedException(); + } + + public void SetAnnotationValue(IEdmElement element, string namespaceName, string localName, object value) + { + annotations[CreateKey(element, namespaceName, localName)] = value; + } + + public void SetAnnotationValues(IEnumerable annotations) + { + throw new NotImplementedException(); + } + + private static Tuple CreateKey(IEdmElement element, string namespaceName, + string localName) + { + return new Tuple(element, namespaceName, localName); + } + } + +} diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataResourceSerializerTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataResourceSerializerTests.cs index 5d9b46cdc..aabacb3cf 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataResourceSerializerTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataResourceSerializerTests.cs @@ -9,8 +9,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Reflection.Metadata; -using System.Runtime.CompilerServices; using System.Runtime.Serialization; using System.Threading.Tasks; using Microsoft.AspNetCore.OData.Abstracts; @@ -23,7 +21,6 @@ using Microsoft.AspNetCore.OData.Tests.Edm; using Microsoft.AspNetCore.OData.Tests.Extensions; using Microsoft.AspNetCore.OData.Tests.Models; -using Microsoft.AspNetCore.Rewrite; using Microsoft.Extensions.DependencyInjection; using Microsoft.Net.Http.Headers; using Microsoft.OData; @@ -69,6 +66,7 @@ public ODataResourceSerializerTests() FirstName = "Foo", LastName = "Bar", ID = 10, + Size = new Size { Weight = 180, Height = 72 } }; _orderSet = _model.EntityContainer.FindEntitySet("Orders"); @@ -183,7 +181,10 @@ public async Task WriteObjectInlineAsync_Calls_CreateSelectExpandNode() { // Arrange Mock serializerProvider = new Mock(); - serializerProvider.Setup(s => s.GetEdmTypeSerializer(It.IsAny())).Returns(new ODataPrimitiveSerializer()); + serializerProvider.Setup( + s => s.GetEdmTypeSerializer(It.IsAny())).Returns(new ODataResourceSerializer(serializerProvider.Object)); + serializerProvider.Setup( + s => s.GetEdmTypeSerializer(It.IsAny())).Returns(new ODataPrimitiveSerializer()); Mock serializer = new Mock(serializerProvider.Object); ODataWriter writer = new Mock().Object; @@ -223,6 +224,11 @@ public async Task WriteObjectInlineAsync_WritesODataResourceFrom_CreateResource( // Arrange ODataResource entry = new ODataResource(); Mock serializerProvider = new Mock(); + serializerProvider.Setup(s => + s.GetEdmTypeSerializer(It.IsAny())).Returns(new ODataResourceSerializer(serializerProvider.Object)); + serializerProvider.Setup(s => + s.GetEdmTypeSerializer(It.IsAny())).Returns(new ODataPrimitiveSerializer()); + Mock serializer = new Mock(serializerProvider.Object); Mock writer = new Mock(); @@ -2565,6 +2571,7 @@ public Customer() public string FirstName { get; set; } public string LastName { get; set; } public string City { get; set; } + public Size Size { get; set; } public IList Orders { get; private set; } } @@ -2581,6 +2588,12 @@ private class Order public Customer Customer { get; set; } } + private class Size + { + public Decimal Height { get; set; } + public Decimal Weight { get; set; } + } + private class Result { public string Title { get; set; } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/SerializationTestsHelpers.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/SerializationTestsHelpers.cs index 6ad3de765..27d4417e4 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/SerializationTestsHelpers.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/SerializationTestsHelpers.cs @@ -17,10 +17,17 @@ internal class SerializationTestsHelpers public static IEdmModel SimpleCustomerOrderModel() { var model = new EdmModel(); + + var sizeType = new EdmComplexType("Default", "Size"); + sizeType.AddStructuralProperty("Height", EdmPrimitiveTypeKind.Decimal); + sizeType.AddStructuralProperty("Weight", EdmPrimitiveTypeKind.Decimal); + model.AddElement(sizeType); + var customerType = new EdmEntityType("Default", "Customer"); customerType.AddStructuralProperty("ID", EdmPrimitiveTypeKind.Int32); customerType.AddStructuralProperty("FirstName", EdmPrimitiveTypeKind.String); customerType.AddStructuralProperty("LastName", EdmPrimitiveTypeKind.String); + customerType.AddStructuralProperty("Size", new EdmComplexTypeReference(sizeType,true)); IEdmTypeReference primitiveTypeReference = EdmCoreModel.Instance.GetPrimitive( EdmPrimitiveTypeKind.String, isNullable: true); diff --git a/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.Net8.bsl b/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.Net8.bsl index c518305aa..97d705abe 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.Net8.bsl +++ b/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.Net8.bsl @@ -2166,21 +2166,6 @@ public class Microsoft.AspNetCore.OData.Formatter.Serialization.ODataCollectionS public virtual System.Threading.Tasks.Task WriteObjectAsync (object graph, System.Type type, Microsoft.OData.ODataMessageWriter messageWriter, Microsoft.AspNetCore.OData.Formatter.Serialization.ODataSerializerContext writeContext) } -public class Microsoft.AspNetCore.OData.Formatter.Serialization.ODataDeletedResourceSerializer : Microsoft.AspNetCore.OData.Formatter.Serialization.ODataResourceSerializer, IODataEdmTypeSerializer, IODataSerializer { - public ODataDeletedResourceSerializer (Microsoft.AspNetCore.OData.Formatter.Serialization.IODataSerializerProvider serializerProvider) - - public virtual Microsoft.OData.ODataDeletedResource CreateDeletedResource (System.Uri id, Microsoft.OData.DeltaDeletedEntryReason reason, Microsoft.AspNetCore.OData.Formatter.Serialization.SelectExpandNode selectExpandNode, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) - [ - AsyncStateMachineAttribute(), - ] - public virtual System.Threading.Tasks.Task WriteObjectAsync (object graph, System.Type type, Microsoft.OData.ODataMessageWriter messageWriter, Microsoft.AspNetCore.OData.Formatter.Serialization.ODataSerializerContext writeContext) - - [ - AsyncStateMachineAttribute(), - ] - public virtual System.Threading.Tasks.Task WriteObjectInlineAsync (object graph, Microsoft.OData.Edm.IEdmTypeReference expectedType, Microsoft.OData.ODataWriter writer, Microsoft.AspNetCore.OData.Formatter.Serialization.ODataSerializerContext writeContext) -} - public class Microsoft.AspNetCore.OData.Formatter.Serialization.ODataDeltaResourceSetSerializer : Microsoft.AspNetCore.OData.Formatter.Serialization.ODataEdmTypeSerializer, IODataEdmTypeSerializer, IODataSerializer { public ODataDeltaResourceSetSerializer (Microsoft.AspNetCore.OData.Formatter.Serialization.IODataSerializerProvider serializerProvider) @@ -2288,9 +2273,9 @@ 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) - protected void AppendDynamicPropertiesInternal (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.ODataDeletedResource CreateDeletedResource (System.Uri id, Microsoft.OData.DeltaDeletedEntryReason reason, Microsoft.AspNetCore.OData.Formatter.Serialization.SelectExpandNode selectExpandNode, 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) public virtual string CreateETag (Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) public virtual Microsoft.OData.ODataNestedResourceInfo CreateNavigationLink (Microsoft.OData.Edm.IEdmNavigationProperty navigationProperty, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) @@ -2300,11 +2285,8 @@ public class Microsoft.AspNetCore.OData.Formatter.Serialization.ODataResourceSer 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) public virtual Microsoft.OData.ODataProperty CreateStructuralProperty (Microsoft.OData.Edm.IEdmStructuralProperty structuralProperty, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) - protected System.Collections.Generic.IEnumerable`1[[Microsoft.OData.ODataProperty]] CreateStructuralPropertyBag (Microsoft.AspNetCore.OData.Formatter.Serialization.SelectExpandNode selectExpandNode, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) public virtual Microsoft.OData.ODataNestedResourceInfo CreateUntypedNestedResourceInfo (Microsoft.OData.Edm.IEdmStructuralProperty structuralProperty, object propertyValue, Microsoft.OData.Edm.IEdmTypeReference valueType, Microsoft.OData.UriParser.PathSelectItem pathSelectItem, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) public virtual object CreateUntypedPropertyValue (Microsoft.OData.Edm.IEdmStructuralProperty structuralProperty, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext, out Microsoft.OData.Edm.IEdmTypeReference& actualType) - protected void InitializeODataResource (Microsoft.AspNetCore.OData.Formatter.Serialization.SelectExpandNode selectExpandNode, Microsoft.OData.ODataResourceBase resource, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) - protected virtual bool ShouldWriteNavigation (Microsoft.OData.ODataNestedResourceInfo navigationLink, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext) [ AsyncStateMachineAttribute(), ] @@ -2319,11 +2301,6 @@ public class Microsoft.AspNetCore.OData.Formatter.Serialization.ODataResourceSer AsyncStateMachineAttribute(), ] public virtual System.Threading.Tasks.Task WriteObjectInlineAsync (object graph, Microsoft.OData.Edm.IEdmTypeReference expectedType, Microsoft.OData.ODataWriter writer, Microsoft.AspNetCore.OData.Formatter.Serialization.ODataSerializerContext writeContext) - - [ - AsyncStateMachineAttribute(), - ] - protected System.Threading.Tasks.Task WriteResourceContent (Microsoft.OData.ODataWriter writer, Microsoft.AspNetCore.OData.Formatter.Serialization.SelectExpandNode selectExpandNode, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext, bool isDelta) } public class Microsoft.AspNetCore.OData.Formatter.Serialization.ODataResourceSetSerializer : Microsoft.AspNetCore.OData.Formatter.Serialization.ODataEdmTypeSerializer, IODataEdmTypeSerializer, IODataSerializer { From 85a92fd580a4aa013143a200fe93d080af646f3b Mon Sep 17 00:00:00 2001 From: mikepizzo Date: Thu, 19 Dec 2024 22:56:04 -0800 Subject: [PATCH 8/9] Add ConfigureAwait(false) and test to verify that WriteDeltaDeletedResourceAsync is called if overridden in a derived type. --- .../Serialization/ODataResourceSerializer.cs | 4 +-- .../ODataDeletedResourceSerializerTests.cs | 33 +++++++++++++++++-- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataResourceSerializer.cs b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataResourceSerializer.cs index b98e44b34..16dd486f8 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataResourceSerializer.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataResourceSerializer.cs @@ -477,7 +477,7 @@ private async Task WriteResourceAsync(object graph, ODataWriter writer, ODataSer } await writer.WriteStartAsync(odataDeletedResource).ConfigureAwait(false); - await WriteResourceContent(writer, selectExpandNode, resourceContext, /*isDelta*/ true); + await WriteResourceContent(writer, selectExpandNode, resourceContext, /*isDelta*/ true).ConfigureAwait(false); await writer.WriteEndAsync().ConfigureAwait(false); } else @@ -496,7 +496,7 @@ await writer.WriteEntityReferenceLinkAsync(new ODataEntityReferenceLink { bool isDelta = graph is IDelta || graph is IEdmChangedObject; await writer.WriteStartAsync(resource).ConfigureAwait(false); - await WriteResourceContent(writer, selectExpandNode, resourceContext, isDelta); + await WriteResourceContent(writer, selectExpandNode, resourceContext, isDelta).ConfigureAwait(false); await writer.WriteEndAsync().ConfigureAwait(false); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataDeletedResourceSerializerTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataDeletedResourceSerializerTests.cs index 1fb5a99cb..cad21f9d1 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataDeletedResourceSerializerTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataDeletedResourceSerializerTests.cs @@ -76,10 +76,10 @@ public ODataDeletedResourceSerializerTests() _customer.TrySetPropertyValue("Size", _size); _orderSet = _model.EntityContainer.FindEntitySet("Orders"); - _order = new DeltaDeletedResource(); + _order = new DeltaDeletedResource{Id = new Uri("http://customers/1")}; _order.TrySetPropertyValue("ID", 20); - _orders = new DeltaSet{ _order }; + _orders = new DeltaSet { _order }; _customer.TrySetPropertyValue("Orders", _orders); _serializerProvider = GetServiceProvider().GetService(); @@ -91,7 +91,7 @@ public ODataDeletedResourceSerializerTests() _ordersNavigation = _customerType.FindNavigationProperty("Orders"); _serializer = new ODataResourceSerializer(_serializerProvider); _path = new ODataPath(new EntitySetSegment(_customerSet)); - _writeContext = new ODataSerializerContext() { NavigationSource = _customerSet, Model = _model, Path = _path, Type = typeof(DeltaDeletedResource)}; + _writeContext = new ODataSerializerContext() { NavigationSource = _customerSet, Model = _model, Path = _path, Type = typeof(DeltaDeletedResource) }; _entityContext = new ResourceContext(_writeContext, _customerSet.EntityType.AsReference(), _customer); } @@ -129,6 +129,33 @@ await ExceptionAssert.ThrowsArgumentNullAsync( "writeContext"); } + [Fact] + public async Task WriteDeletedResourceCallsWriteDeltaDeletedResourceAsyncIfOverridden() + { + // Arrange + Mock serializerProvider = new Mock(); + var serializer = new Mock(serializerProvider.Object); + serializer.Setup(s=>s.WriteDeltaDeletedResourceAsync(_orders.First(),It.IsAny(), It.IsAny())) + .Verifiable(); + serializer.CallBase = true; + + // Act + await serializer.Object.WriteObjectAsync(_orders, typeof(DeltaSet), ODataTestUtil.GetMockODataMessageWriter(ODataVersion.V401), _writeContext); + + // Assert + serializer.Verify(); + } + + public class MyDeltaResourceSetSerializer : ODataDeltaResourceSetSerializer + { + public MyDeltaResourceSetSerializer(IODataSerializerProvider serializerProvider) : base(serializerProvider) { } + + public override async Task WriteDeltaDeletedResourceAsync(object value, ODataWriter writer, ODataSerializerContext writeContext) + { + await base.WriteDeltaDeletedResourceAsync(value, writer, writeContext).ConfigureAwait(false); + } + } + [Fact] public async Task WriteObjectAsync_Calls_WriteObjectInline_WithRightEntityType() { From d855ccfeb9433790ce00a8a4481d909dfb210d01 Mon Sep 17 00:00:00 2001 From: Michael Pizzo Date: Thu, 19 Dec 2024 23:03:07 -0800 Subject: [PATCH 9/9] Update package references in csproj file --- .../Microsoft.AspNetCore.OData.csproj | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.csproj b/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.csproj index 3c9d197c0..6aa7c2ffc 100644 --- a/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.csproj +++ b/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -32,9 +32,6 @@ - - - all runtime; build; native; contentfiles; analyzers; buildtransitive