Skip to content

Commit

Permalink
Resolve ContentID in odata.bind annotation (#643)
Browse files Browse the repository at this point in the history
  • Loading branch information
gathogojr authored Jul 27, 2022
1 parent f675f6c commit 0feec0a
Show file tree
Hide file tree
Showing 2 changed files with 229 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,17 @@
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;
using Microsoft.AspNetCore.OData.Formatter.Value;
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;
Expand All @@ -33,6 +36,8 @@ namespace Microsoft.AspNetCore.OData.Formatter.Deserialization
/// </summary>
public class ODataResourceDeserializer : ODataEdmTypeDeserializer
{
private static readonly Regex ContentIdReferencePattern = new Regex(@"\$\d", RegexOptions.Compiled);

/// <summary>
/// Initializes a new instance of the <see cref="ODataResourceDeserializer"/> class.
/// </summary>
Expand Down Expand Up @@ -376,7 +381,7 @@ public virtual void ApplyNestedProperty(object resource, ODataNestedResourceInfo
}

IList<ODataItemWrapper> nestedItems;
var referenceLinks = resourceInfoWrapper.NestedItems.OfType<ODataEntityReferenceLinkWrapper>().ToArray();
ODataEntityReferenceLinkWrapper[] referenceLinks = resourceInfoWrapper.NestedItems.OfType<ODataEntityReferenceLinkWrapper>().ToArray();
if (referenceLinks.Length > 0)
{
// Be noted:
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -797,7 +802,7 @@ private static ODataResourceWrapper CreateResourceWrapper(IEdmTypeReference edmP
resource.Properties = CreateKeyProperties(refLink.EntityReferenceLink.Url, readContext) ?? Array.Empty<ODataProperty>();
ODataResourceWrapper resourceWrapper = new ODataResourceWrapper(resource);

foreach (var instanceAnnotation in refLink.EntityReferenceLink.InstanceAnnotations)
foreach (ODataInstanceAnnotation instanceAnnotation in refLink.EntityReferenceLink.InstanceAnnotations)
{
resource.InstanceAnnotations.Add(instanceAnnotation);
}
Expand Down Expand Up @@ -835,7 +840,7 @@ private ODataResourceWrapper UpdateResourceWrapper(ODataResourceWrapper resource
else
{
IDictionary<string, ODataProperty> 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?
Expand Down Expand Up @@ -870,26 +875,35 @@ private static IList<ODataProperty> 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<IODataPathParser>();
if (pathParser == null) // Seems like IODataPathParser is NOT injected into DI container by default
{
pathParser = new DefaultODataPathParser();
}

IList<ODataProperty> properties = null;
var path = pathParser.Parse(model, serviceRootUri, id, request.GetRouteServices());
ODataPath path = pathParser.Parse(model, serviceRootUri, resolvedId, requestContainer);
KeySegment keySegment = path.OfType<KeySegment>().LastOrDefault();
if (keySegment != null)
{
properties = new List<ODataProperty>();
foreach (var key in keySegment.Keys)
foreach (KeyValuePair<string, object> key in keySegment.Keys)
{
properties.Add(new ODataProperty
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
//-----------------------------------------------------------------------------
// <copyright file="ContentIdToLocationMappingTests.cs" company=".NET Foundation">
// Copyright (c) .NET Foundation and Contributors. All rights reserved.
// See License.txt in the project root for license information.
// </copyright>
//------------------------------------------------------------------------------

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<ContentIdToLocationMappingTests>
{
private static IEdmModel edmModel;

public ContentIdToLocationMappingTests(WebApiTestFixture<ContentIdToLocationMappingTests> 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<ContentIdToLocationMappingParent>("ContentIdToLocationMappingParents");
builder.EntitySet<ContentIdToLocationMappingChild>("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<ContentIdToLocationMappingChild>();
}

[Key]
public int ParentId
{
get; set;
}

public virtual ICollection<ContentIdToLocationMappingChild> Children
{
get; set;
}
}

public class ContentIdToLocationMappingChild
{
[Key]
public int ChildId
{
get; set;
}

public int? ParentId
{
get; set;
}

public virtual ContentIdToLocationMappingParent Parent
{
get; set;
}
}
}

0 comments on commit 0feec0a

Please sign in to comment.