diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataResourceDeserializer.cs b/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataResourceDeserializer.cs
index e99c87358..ce0cc6b6f 100644
--- a/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataResourceDeserializer.cs
+++ b/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataResourceDeserializer.cs
@@ -14,7 +14,9 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Serialization;
+using System.Text.RegularExpressions;
using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.OData.Deltas;
using Microsoft.AspNetCore.OData.Edm;
using Microsoft.AspNetCore.OData.Extensions;
@@ -22,6 +24,7 @@
using Microsoft.AspNetCore.OData.Formatter.Wrapper;
using Microsoft.AspNetCore.OData.Routing;
using Microsoft.AspNetCore.OData.Routing.Parser;
+using Microsoft.Extensions.DependencyInjection;
using Microsoft.OData;
using Microsoft.OData.Edm;
using Microsoft.OData.UriParser;
@@ -33,6 +36,8 @@ namespace Microsoft.AspNetCore.OData.Formatter.Deserialization
///
public class ODataResourceDeserializer : ODataEdmTypeDeserializer
{
+ private static readonly Regex ContentIdReferencePattern = new Regex(@"\$\d", RegexOptions.Compiled);
+
///
/// Initializes a new instance of the class.
///
@@ -376,7 +381,7 @@ public virtual void ApplyNestedProperty(object resource, ODataNestedResourceInfo
}
IList nestedItems;
- var referenceLinks = resourceInfoWrapper.NestedItems.OfType().ToArray();
+ ODataEntityReferenceLinkWrapper[] referenceLinks = resourceInfoWrapper.NestedItems.OfType().ToArray();
if (referenceLinks.Length > 0)
{
// Be noted:
@@ -578,7 +583,7 @@ private object ReadNestedResourceInline(ODataResourceWrapper resourceWrapper, IE
IEdmStructuredTypeReference structuredType = edmType.AsStructured();
- var nestedReadContext = new ODataDeserializerContext
+ ODataDeserializerContext nestedReadContext = new ODataDeserializerContext
{
Path = readContext.Path,
Model = readContext.Model,
@@ -797,7 +802,7 @@ private static ODataResourceWrapper CreateResourceWrapper(IEdmTypeReference edmP
resource.Properties = CreateKeyProperties(refLink.EntityReferenceLink.Url, readContext) ?? Array.Empty();
ODataResourceWrapper resourceWrapper = new ODataResourceWrapper(resource);
- foreach (var instanceAnnotation in refLink.EntityReferenceLink.InstanceAnnotations)
+ foreach (ODataInstanceAnnotation instanceAnnotation in refLink.EntityReferenceLink.InstanceAnnotations)
{
resource.InstanceAnnotations.Add(instanceAnnotation);
}
@@ -835,7 +840,7 @@ private ODataResourceWrapper UpdateResourceWrapper(ODataResourceWrapper resource
else
{
IDictionary newPropertiesDic = resourceWrapper.Resource.Properties.ToDictionary(p => p.Name, p => p);
- foreach (var key in keys)
+ foreach (ODataProperty key in keys)
{
// Logic: if we have the key property, try to keep the key property and get rid of the key value from ID.
// Need to double confirm whether it is the right logic?
@@ -870,26 +875,35 @@ private static IList CreateKeyProperties(Uri id, ODataDeserialize
try
{
- Uri serviceRootUri = null;
- if (id.IsAbsoluteUri)
+ IEdmModel model = readContext.Model;
+ HttpRequest request = readContext.Request;
+ IServiceProvider requestContainer = request.GetRouteServices();
+ Uri resolvedId = id;
+
+ string idOriginalString = id.OriginalString;
+ if (ContentIdReferencePattern.IsMatch(idOriginalString))
{
- string serviceRoot = readContext.Request.CreateODataLink();
- serviceRootUri = new Uri(serviceRoot, UriKind.Absolute);
+ // We can expect request.ODataBatchFeature() to not be null
+ string resolvedUri = ContentIdHelpers.ResolveContentId(
+ idOriginalString,
+ request.ODataBatchFeature().ContentIdMapping);
+ resolvedId = new Uri(resolvedUri, UriKind.RelativeOrAbsolute);
}
- var request = readContext.Request;
- IEdmModel model = readContext.Model;
-
- // TODO: shall we use the DI to inject the path parser?
- DefaultODataPathParser pathParser = new DefaultODataPathParser();
+ Uri serviceRootUri = new Uri(request.CreateODataLink());
+ IODataPathParser pathParser = requestContainer?.GetService();
+ if (pathParser == null) // Seems like IODataPathParser is NOT injected into DI container by default
+ {
+ pathParser = new DefaultODataPathParser();
+ }
IList properties = null;
- var path = pathParser.Parse(model, serviceRootUri, id, request.GetRouteServices());
+ ODataPath path = pathParser.Parse(model, serviceRootUri, resolvedId, requestContainer);
KeySegment keySegment = path.OfType().LastOrDefault();
if (keySegment != null)
{
properties = new List();
- foreach (var key in keySegment.Keys)
+ foreach (KeyValuePair key in keySegment.Keys)
{
properties.Add(new ODataProperty
{
diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Batch/ContentIdToLocationMappingTests.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Batch/ContentIdToLocationMappingTests.cs
new file mode 100644
index 000000000..7cce8a56a
--- /dev/null
+++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Batch/ContentIdToLocationMappingTests.cs
@@ -0,0 +1,200 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (c) .NET Foundation and Contributors. All rights reserved.
+// See License.txt in the project root for license information.
+//
+//------------------------------------------------------------------------------
+
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Net;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.OData.Batch;
+using Microsoft.AspNetCore.OData.E2E.Tests.Commons;
+using Microsoft.AspNetCore.OData.Routing.Controllers;
+using Microsoft.AspNetCore.OData.TestCommon;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.OData;
+using Microsoft.OData.Edm;
+using Microsoft.OData.ModelBuilder;
+using Xunit;
+
+namespace Microsoft.AspNetCore.OData.E2E.Tests.Batch
+{
+ public class ContentIdToLocationMappingTests : WebApiTestBase
+ {
+ private static IEdmModel edmModel;
+
+ public ContentIdToLocationMappingTests(WebApiTestFixture fixture)
+ : base(fixture)
+ {
+ }
+
+ protected static void UpdateConfigureServices(IServiceCollection services)
+ {
+ services.ConfigureControllers(
+ typeof(ContentIdToLocationMappingParentsController),
+ typeof(ContentIdToLocationMappingChildrenController));
+
+ edmModel = GetEdmModel();
+ services.AddControllers().AddOData(opt =>
+ {
+ opt.EnableQueryFeatures();
+ opt.EnableContinueOnErrorHeader = true;
+ opt.AddRouteComponents("ContentIdToLocationMapping", edmModel, new DefaultODataBatchHandler());
+ });
+ }
+
+ protected static void UpdateConfigure(IApplicationBuilder app)
+ {
+ app.UseODataBatching();
+ app.UseRouting();
+ app.UseEndpoints(endpoints =>
+ {
+ endpoints.MapControllers();
+ });
+ }
+
+ protected static IEdmModel GetEdmModel()
+ {
+ ODataModelBuilder builder = new ODataConventionModelBuilder();
+ builder.EntitySet("ContentIdToLocationMappingParents");
+ builder.EntitySet("ContentIdToLocationMappingChildren");
+ builder.Namespace = typeof(ContentIdToLocationMappingParent).Namespace;
+
+ return builder.GetEdmModel();
+ }
+
+ [Fact]
+ public async Task CanResolveContentIdInODataBindAnnotationAsync()
+ {
+ // Arrange
+ HttpClient client = CreateClient();
+ string serviceBase = $"{client.BaseAddress}ContentIdToLocationMapping";
+ string requestUri = $"{serviceBase}/$batch";
+ string parentsUri = $"{serviceBase}/ContentIdToLocationMappingParents";
+ string childrenUri = $"{serviceBase}/ContentIdToLocationMappingChildren";
+ string payload = "{" +
+ " \"requests\": [" +
+ " {" +
+ " \"id\": \"1\"," +
+ " \"method\": \"POST\"," +
+ $" \"url\": \"{parentsUri}\"," +
+ " \"headers\": {" +
+ " \"OData-Version\": \"4.0\"," +
+ " \"Content-Type\": \"application/json;odata.metadata=minimal\"," +
+ " \"Accept\": \"application/json;odata.metadata=minimal\"" +
+ " }," +
+ " \"body\": {\"ParentId\":123}" +
+ " }," +
+ " {" +
+ " \"id\": \"2\"," +
+ " \"method\": \"POST\"," +
+ $" \"url\": \"{childrenUri}\"," +
+ " \"headers\": {" +
+ " \"OData-Version\": \"4.0\"," +
+ " \"Content-Type\": \"application/json;odata.metadata=minimal\"," +
+ " \"Accept\": \"application/json;odata.metadata=minimal\"" +
+ " }," +
+ " \"body\": {" +
+ " \"Parent@odata.bind\": \"$1\"" +
+ " }" +
+ " }" +
+ " ]" +
+ "}";
+
+ // Act
+ HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, requestUri);
+ request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json"));
+ request.Content = new StringContent(payload);
+ request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json");
+ HttpResponseMessage response = await client.SendAsync(request);
+
+ // Assert
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+
+ var stream = await response.Content.ReadAsStreamAsync();
+ IODataResponseMessage odataResponseMessage = new ODataMessageWrapper(stream, response.Content.Headers);
+ int subResponseCount = 0;
+ using (var messageReader = new ODataMessageReader(odataResponseMessage, new ODataMessageReaderSettings(), edmModel))
+ {
+ var batchReader = messageReader.CreateODataBatchReader();
+ while (batchReader.Read())
+ {
+ switch (batchReader.State)
+ {
+ case ODataBatchReaderState.Operation:
+ var operationMessage = batchReader.CreateOperationResponseMessage();
+ subResponseCount++;
+ Assert.Equal(201, operationMessage.StatusCode);
+ break;
+ }
+ }
+ }
+
+ // NOTE: We assert that $1 is successfully resolved from the controller action
+ Assert.Equal(2, subResponseCount);
+ }
+ }
+
+ public class ContentIdToLocationMappingParentsController : ODataController
+ {
+ public ActionResult Post([FromBody] ContentIdToLocationMappingParent parent)
+ {
+ return Created(new Uri($"{Request.Scheme}://{Request.Host}{Request.Path}/{parent.ParentId}"), parent);
+ }
+ }
+
+ public class ContentIdToLocationMappingChildrenController : ODataController
+ {
+ public ActionResult Post([FromBody] ContentIdToLocationMappingChild child)
+ {
+ Assert.Equal(123, child.Parent.ParentId);
+
+ return Created(new Uri($"{Request.Scheme}://{Request.Host}{Request.Path}/{child.ChildId}"), child);
+ }
+ }
+
+ public class ContentIdToLocationMappingParent
+ {
+ public ContentIdToLocationMappingParent()
+ {
+ Children = new HashSet();
+ }
+
+ [Key]
+ public int ParentId
+ {
+ get; set;
+ }
+
+ public virtual ICollection Children
+ {
+ get; set;
+ }
+ }
+
+ public class ContentIdToLocationMappingChild
+ {
+ [Key]
+ public int ChildId
+ {
+ get; set;
+ }
+
+ public int? ParentId
+ {
+ get; set;
+ }
+
+ public virtual ContentIdToLocationMappingParent Parent
+ {
+ get; set;
+ }
+ }
+}