Skip to content

Commit bf4931a

Browse files
authored
Deep insert (#2761)
1 parent 9300f41 commit bf4931a

File tree

26 files changed

+1528
-310
lines changed

26 files changed

+1528
-310
lines changed

src/Microsoft.AspNet.OData.Shared/Extensions/ContainerBuilderExtensions.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,9 @@ public static IContainerBuilder AddDefaultWebApiServices(this IContainerBuilder
9292
// HttpRequestScope.
9393
builder.AddService<HttpRequestScope>(ServiceLifetime.Scoped);
9494
builder.AddService(ServiceLifetime.Scoped, sp => sp.GetRequiredService<HttpRequestScope>().HttpRequest);
95+
96+
// QueryBuilders.
97+
builder.AddService<IExpandQueryBuilder, ExpandQueryBuilder>(ServiceLifetime.Singleton);
9598
return builder;
9699
}
97100
}

src/Microsoft.AspNet.OData.Shared/Microsoft.AspNet.OData.Shared.projitems

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@
9595
<Compile Include="$(MSBuildThisFileDirectory)ODataAPIResponseStatus.cs" />
9696
<Compile Include="$(MSBuildThisFileDirectory)IODataAPIHandler.cs" />
9797
<Compile Include="$(MSBuildThisFileDirectory)ODataIdContainer.cs" />
98+
<Compile Include="$(MSBuildThisFileDirectory)Query\ExpandQueryBuilder.cs" />
99+
<Compile Include="$(MSBuildThisFileDirectory)Query\IExpandQueryBuilder.cs" />
98100
<Compile Include="$(MSBuildThisFileDirectory)TransientAnnotations.cs" />
99101
<Compile Include="$(MSBuildThisFileDirectory)IPerRouteContainer.cs" />
100102
<Compile Include="$(MSBuildThisFileDirectory)CompatibilityOptions.cs" />

src/Microsoft.AspNet.OData.Shared/ODataAPIHandler.cs

Lines changed: 294 additions & 78 deletions
Large diffs are not rendered by default.
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
//-----------------------------------------------------------------------------
2+
// <copyright file="ExpandQueryBuilder.cs" company=".NET Foundation">
3+
// Copyright (c) .NET Foundation and Contributors. All rights reserved.
4+
// See License.txt in the project root for license information.
5+
// </copyright>
6+
//------------------------------------------------------------------------------
7+
8+
using System;
9+
using System.Collections;
10+
using System.Collections.Generic;
11+
using System.Linq;
12+
using System.Reflection;
13+
using System.Text;
14+
using Microsoft.AspNet.OData.Common;
15+
using Microsoft.AspNet.OData.Formatter;
16+
using Microsoft.OData.Edm;
17+
18+
namespace Microsoft.AspNet.OData.Query
19+
{
20+
/// <summary>
21+
/// Exposes the ability to generate a $expand query parameter from a payload object.
22+
/// </summary>
23+
public class ExpandQueryBuilder : IExpandQueryBuilder
24+
{
25+
/// <inheritdoc />
26+
public virtual string GenerateExpandQueryParameter(object value, IEdmModel model)
27+
{
28+
if (value == null)
29+
{
30+
throw Error.ArgumentNull(nameof(value));
31+
}
32+
33+
if (model == null)
34+
{
35+
throw Error.ArgumentNull(nameof(model));
36+
}
37+
38+
return GenerateExpandQueryStringInternal(value, model, false);
39+
}
40+
41+
private string GenerateExpandQueryStringInternal(object value, IEdmModel model, bool isNestedExpand)
42+
{
43+
Type type = value.GetType();
44+
bool isCollection = TypeHelper.IsCollection(type, out Type elementType);
45+
IEnumerable collection = null;
46+
if (isCollection)
47+
{
48+
type = elementType;
49+
collection = value as IEnumerable;
50+
}
51+
52+
string edmFullName = type.EdmFullName();
53+
IEdmSchemaType schemaType = model.FindType(edmFullName);
54+
IEdmStructuredType edmStructuredType = schemaType as IEdmStructuredType;
55+
56+
IEnumerable<IEdmNavigationProperty> navigationProperties = edmStructuredType.NavigationProperties();
57+
string expandString = "";
58+
59+
int count = 0;
60+
61+
HashSet<string> navPropNames = new HashSet<string>();
62+
63+
foreach (IEdmNavigationProperty navProp in navigationProperties)
64+
{
65+
count++;
66+
PropertyInfo prop = type.GetProperty(navProp.Name);
67+
68+
if (isCollection)
69+
{
70+
foreach (object obj in collection)
71+
{
72+
object navPropValue = prop.GetValue(obj);
73+
74+
if (navPropValue == null)
75+
{
76+
continue;
77+
}
78+
79+
if (!navPropNames.Contains(navProp.Name))
80+
{
81+
expandString += !isNestedExpand ? "" : "(";
82+
expandString += count > 1 ? "," + prop.Name : string.Concat("$expand=", prop.Name);
83+
navPropNames.Add(navProp.Name);
84+
expandString += GenerateExpandQueryStringInternal(navPropValue, model, true);
85+
}
86+
else
87+
{
88+
expandString = expandString.TrimEnd(')');
89+
expandString += GenerateExpandQueryStringInternal(navPropValue, model, true);
90+
}
91+
92+
expandString += !isNestedExpand ? "" : ")";
93+
}
94+
}
95+
else
96+
{
97+
object navPropValue = prop.GetValue(value);
98+
99+
if (navPropValue != null)
100+
{
101+
if (!navPropNames.Contains(navProp.Name))
102+
{
103+
expandString += !isNestedExpand ? "" : "(";
104+
expandString += count > 1 ? "," + prop.Name : string.Concat("$expand=", prop.Name);
105+
navPropNames.Add(navProp.Name);
106+
expandString += GenerateExpandQueryStringInternal(navPropValue, model, true);
107+
}
108+
else
109+
{
110+
expandString = expandString.TrimEnd(')');
111+
expandString += GenerateExpandQueryStringInternal(navPropValue, model, true);
112+
}
113+
114+
expandString += !isNestedExpand ? "" : ")";
115+
}
116+
}
117+
}
118+
119+
return expandString;
120+
}
121+
}
122+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
//-----------------------------------------------------------------------------
2+
// <copyright file="IExpandQueryBuilder.cs" company=".NET Foundation">
3+
// Copyright (c) .NET Foundation and Contributors. All rights reserved.
4+
// See License.txt in the project root for license information.
5+
// </copyright>
6+
//------------------------------------------------------------------------------
7+
8+
using System;
9+
using System.Collections.Generic;
10+
using System.Text;
11+
using Microsoft.OData.Edm;
12+
13+
namespace Microsoft.AspNet.OData.Query
14+
{
15+
/// <summary>
16+
/// Exposes the ability to generate a $expand query string from a payload object.
17+
/// </summary>
18+
public interface IExpandQueryBuilder
19+
{
20+
/// <summary>
21+
/// Generates a $expand query string from a payload object.
22+
/// </summary>
23+
/// <param name="value">The payload object.</param>
24+
/// <param name="model">The service model.</param>
25+
/// <returns>A $expand query string.</returns>
26+
string GenerateExpandQueryParameter(object value, IEdmModel model);
27+
}
28+
}

src/Microsoft.AspNet.OData/EnableQueryAttribute.cs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ public partial class EnableQueryAttribute : ActionFilterAttribute
4747
/// request message and HttpConfiguration etc.</param>
4848
[SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling",
4949
Justification = "The majority of types referenced by this method result from HttpActionExecutedContext")]
50+
[SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity",
51+
Justification = "The majority of types referenced by this method result from HttpActionExecutedContext")]
5052
public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
5153
{
5254
if (actionExecutedContext == null)
@@ -71,6 +73,34 @@ public override void OnActionExecuted(HttpActionExecutedContext actionExecutedCo
7173
throw Error.Argument("actionExecutedContext", SRResources.ActionExecutedContextMustHaveActionContext);
7274
}
7375

76+
bool hasExpandQueryParameter = false;
77+
string queryString = request.RequestUri.Query;
78+
var absoluteUri = request.RequestUri.AbsoluteUri;
79+
bool queryStringIsEmpty = string.IsNullOrEmpty(queryString);
80+
81+
ReadOnlySpan<char> absoluteUriSpan = absoluteUri.AsSpan();
82+
string baseUriWithPath = queryStringIsEmpty ? absoluteUri : absoluteUriSpan.Slice(0, absoluteUri.IndexOf('?', 0)).ToString();
83+
84+
if (!queryStringIsEmpty)
85+
{
86+
string[] queryParameters = queryString.Split('&');
87+
88+
hasExpandQueryParameter = queryParameters.Any(x => x.StartsWith("?$expand", StringComparison.OrdinalIgnoreCase) || x.StartsWith("$expand", StringComparison.OrdinalIgnoreCase) || x.StartsWith("expand", StringComparison.OrdinalIgnoreCase));
89+
}
90+
91+
// Create a $expand query string if 1) It's a POST request 2) if there is no expand query string.
92+
if (string.Equals(actionExecutedContext.Request.Method, HttpMethod.Post) && !hasExpandQueryParameter)
93+
{
94+
string expand = GenerateExpandQueryFromPayload(actionExecutedContext.ActionContext);
95+
96+
if (!string.IsNullOrEmpty(expand))
97+
{
98+
// If query string was empty, we add a new query string e.g ?$expand=Order. If not, we prepend a $expand query
99+
expand = queryStringIsEmpty ? "?" + expand : "?" + expand + "&" + queryString.TrimStart('?');
100+
request.RequestUri = new Uri(baseUriWithPath + expand);
101+
}
102+
}
103+
74104
HttpActionDescriptor actionDescriptor = actionExecutedContext.ActionContext.ActionDescriptor;
75105
if (actionDescriptor == null)
76106
{
@@ -122,6 +152,30 @@ public override void OnActionExecuted(HttpActionExecutedContext actionExecutedCo
122152
}
123153
}
124154

155+
private static string GenerateExpandQueryFromPayload(HttpActionContext context)
156+
{
157+
object obj = null;
158+
159+
if (context.ActionArguments == null || context.ActionArguments.Count == 0 || (obj = context.ActionArguments.First().Value) == null)
160+
{
161+
return string.Empty;
162+
}
163+
164+
Type type = obj.GetType();
165+
166+
// Ignore Action payloads
167+
if (type == typeof(ODataActionParameters) || type == typeof(ODataUntypedActionParameters))
168+
{
169+
return string.Empty;
170+
}
171+
172+
IExpandQueryBuilder expandQueryBuilder = context.Request.GetExpandQueryBuilder();
173+
174+
string expandString = expandQueryBuilder.GenerateExpandQueryParameter(obj, context.Request.GetModel());
175+
176+
return expandString;
177+
}
178+
125179
/// <summary>
126180
/// Create and validate a new instance of <see cref="ODataQueryOptions"/> from a query and context.
127181
/// </summary>

src/Microsoft.AspNet.OData/Extensions/HttpRequestMessageExtensions.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,22 @@ public static IEnumerable<IODataRoutingConvention> GetRoutingConventions(this Ht
416416

417417
return request.GetRequestContainer().GetServices<IODataRoutingConvention>();
418418
}
419+
420+
/// <summary>
421+
/// Gets the <see cref="IExpandQueryBuilder"/> from the request container.
422+
/// </summary>
423+
/// <param name="request">The request.</param>
424+
/// <returns>The <see cref="IExpandQueryBuilder"/> from the request container.</returns>
425+
public static IExpandQueryBuilder GetExpandQueryBuilder(this HttpRequestMessage request)
426+
{
427+
if (request == null)
428+
{
429+
throw Error.ArgumentNull("request");
430+
}
431+
432+
return request.GetRequestContainer().GetRequiredService<IExpandQueryBuilder>();
433+
}
434+
419435
/// <summary>
420436
/// Checks whether the request is a POST targeted at a resource path ending in /$query.
421437
/// </summary>

src/Microsoft.AspNet.OData/Microsoft.AspNet.OData.csproj

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,15 +47,24 @@
4747
<Private>True</Private>
4848
</Reference>
4949
<Reference Include="System" />
50+
<Reference Include="System.Buffers, Version=4.0.3.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
51+
<HintPath>..\..\sln\packages\System.Buffers.4.5.1\lib\netstandard1.1\System.Buffers.dll</HintPath>
52+
</Reference>
5053
<Reference Include="System.ComponentModel.DataAnnotations" />
5154
<Reference Include="System.Core" />
5255
<Reference Include="System.Data.Linq" />
56+
<Reference Include="System.Memory, Version=4.0.1.1, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
57+
<HintPath>..\..\sln\packages\System.Memory.4.5.4\lib\netstandard1.1\System.Memory.dll</HintPath>
58+
</Reference>
5359
<Reference Include="System.Net.Http" />
5460
<Reference Include="System.Net.Http.Formatting, Version=5.2.2.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
5561
<SpecificVersion>False</SpecificVersion>
5662
<HintPath>..\..\sln\packages\Microsoft.AspNet.WebApi.Client.5.2.2\lib\net45\System.Net.Http.Formatting.dll</HintPath>
5763
</Reference>
5864
<Reference Include="System.Net.Http.WebRequest" />
65+
<Reference Include="System.Runtime.CompilerServices.Unsafe, Version=4.0.4.1, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
66+
<HintPath>..\..\sln\packages\System.Runtime.CompilerServices.Unsafe.4.5.3\lib\netstandard1.0\System.Runtime.CompilerServices.Unsafe.dll</HintPath>
67+
</Reference>
5968
<Reference Include="System.Runtime.Serialization" />
6069
<Reference Include="System.Web.Http, Version=5.2.2.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
6170
<SpecificVersion>False</SpecificVersion>

src/Microsoft.AspNet.OData/app.config

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66
<assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral" />
77
<bindingRedirect oldVersion="0.0.0.0-13.0.0.0" newVersion="13.0.0.0" />
88
</dependentAssembly>
9+
<dependentAssembly>
10+
<assemblyIdentity name="System.Buffers" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
11+
<bindingRedirect oldVersion="0.0.0.0-4.0.3.0" newVersion="4.0.3.0" />
12+
</dependentAssembly>
913
</assemblyBinding>
1014
</runtime>
1115
</configuration>

src/Microsoft.AspNet.OData/packages.config

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,7 @@
1111
<package id="Microsoft.SourceLink.GitHub" version="1.0.0" targetFramework="net45" developmentDependency="true" />
1212
<package id="Microsoft.Spatial" version="7.15.0" targetFramework="net45" />
1313
<package id="Newtonsoft.Json" version="13.0.1" targetFramework="net45" />
14+
<package id="System.Buffers" version="4.5.1" targetFramework="net45" />
15+
<package id="System.Memory" version="4.5.4" targetFramework="net45" />
16+
<package id="System.Runtime.CompilerServices.Unsafe" version="4.5.3" targetFramework="net45" />
1417
</packages>

src/Microsoft.AspNetCore.OData/EnableQueryAttribute.cs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,31 @@ public override void OnActionExecuting(ActionExecutingContext context)
6666
context.HttpContext.Items.Add(nameof(RequestQueryData), requestQueryData);
6767

6868
HttpRequest request = context.HttpContext.Request;
69+
70+
bool hasExpandQueryParameter = false;
71+
72+
string queryString = request.QueryString.ToString();
73+
bool queryStringIsEmpty = string.IsNullOrEmpty(queryString);
74+
75+
if (!queryStringIsEmpty)
76+
{
77+
string[] queryParameters = queryString.Split('&');
78+
hasExpandQueryParameter = queryParameters.Any(x => x.StartsWith("?$expand", StringComparison.OrdinalIgnoreCase) || x.StartsWith("$expand", StringComparison.OrdinalIgnoreCase) || x.StartsWith("expand", StringComparison.OrdinalIgnoreCase));
79+
}
80+
81+
// Create a $expand query string if 1) It's a POST request 2) if there is no expand query string.
82+
if (string.Equals(request.Method, "post", StringComparison.OrdinalIgnoreCase) && !hasExpandQueryParameter)
83+
{
84+
string expand = GenerateExpandQueryFromPayload(context);
85+
86+
if (!string.IsNullOrEmpty(expand))
87+
{
88+
// If query string was empty, we add a new query string e.g ?$expand=Order. If not, we prepend a $expand query
89+
expand = queryStringIsEmpty ? "?" + expand : "?" + expand + "&" + queryString.TrimStart('?');
90+
request.QueryString = new QueryString(expand);
91+
}
92+
}
93+
6994
ODataPath path = request.ODataFeature().Path;
7095

7196
ODataQueryContext queryContext = null;
@@ -275,6 +300,30 @@ public override void OnActionExecuted(ActionExecutedContext actionExecutedContex
275300
}
276301
}
277302

303+
private string GenerateExpandQueryFromPayload(ActionExecutingContext context)
304+
{
305+
object obj = null;
306+
307+
if (context.ActionArguments == null || context.ActionArguments.Count == 0 || (obj = context.ActionArguments.First().Value) == null)
308+
{
309+
return string.Empty;
310+
}
311+
312+
Type type = obj.GetType();
313+
314+
// Ignore Action payloads
315+
if (type == typeof(ODataActionParameters) || type == typeof(ODataUntypedActionParameters))
316+
{
317+
return string.Empty;
318+
}
319+
320+
IExpandQueryBuilder expandQueryBuilder = context.HttpContext.Request.GetExpandQueryBuilder();
321+
322+
string expandString = expandQueryBuilder.GenerateExpandQueryParameter(obj, context.HttpContext.Request.GetModel());
323+
324+
return expandString;
325+
}
326+
278327
/// <summary>
279328
/// Create and validate a new instance of <see cref="ODataQueryOptions"/> from a query and context.
280329
/// </summary>

src/Microsoft.AspNetCore.OData/Extensions/HttpRequestExtensions.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,21 @@ public static void DeleteRequestContainer(this HttpRequest request, bool dispose
456456
}
457457
}
458458

459+
/// <summary>
460+
/// Gets the <see cref="IExpandQueryBuilder"/> from the request container.
461+
/// </summary>
462+
/// <param name="request">The request.</param>
463+
/// <returns>The <see cref="IExpandQueryBuilder"/> from the request container.</returns>
464+
public static IExpandQueryBuilder GetExpandQueryBuilder(this HttpRequest request)
465+
{
466+
if (request == null)
467+
{
468+
throw Error.ArgumentNull("request");
469+
}
470+
471+
return request.GetRequestContainer().GetRequiredService<IExpandQueryBuilder>();
472+
}
473+
459474
/// <summary>
460475
/// Checks whether the request is a POST targeted at a resource path ending in /$query.
461476
/// </summary>

0 commit comments

Comments
 (0)