Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 39 additions & 1 deletion sample/ODataMiniApi/ODataMetadataApi.http
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,42 @@ GET {{ODataMiniApi_HostAddress}}/v1/$metadata?$format=application/json
GET {{ODataMiniApi_HostAddress}}/v1/$metadata
Accept: application/json

# The above two requests get the CSDL-JSON file
# The above two requests get the CSDL-JSON file

###

POST {{ODataMiniApi_HostAddress}}/v1/$batch
Content-Type: application/json
Accept: application/json

{
"requests": [
{
"id": "1",
"atomicityGroup": "f7de7314-2f3d-4422-b840-ada6d6de0f18",
"method": "POST",
"url": "http://localhost:5177/v1/customers/rating",
"headers": {
"OData-Version": "4.0",
"Content-Type": "application/json",
"Accept": "application/json"
},
"body": {
"p": 8
}
},
{
"id": "2",
"atomicityGroup": "f7de7314-2f3d-4422-b840-ada6d6de0f18",
"method": "GET",
"url": "http://localhost:5177/v1/customers",
"headers": {
"OData-Version": "4.0",
"Content-Type": "application/json;odata.metadata=minimal",
"Accept": "application/json;odata.metadata=minimal"
}
}
]
}

###
3 changes: 3 additions & 0 deletions sample/ODataMiniApi/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@
services.AddSingleton<IFilterBinder, FilterBinder>();
};

// Be noted: should call UseODataMiniBatching() as early as possible to support OData batching.
app.UseODataMiniBatching("v1/$batch", model);

app.MapGet("test", () => "hello world").WithODataResult();

// Group
Expand Down
34 changes: 30 additions & 4 deletions src/Microsoft.AspNetCore.OData/Batch/DefaultODataBatchHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,20 @@ public override async Task ProcessBatchAsync(HttpContext context, RequestDelegat
return;
}

SetMinimalApi(context);

IList<ODataBatchRequestItem> subRequests = await ParseBatchRequestsAsync(context).ConfigureAwait(false);

ODataOptions options = context.RequestServices.GetRequiredService<IOptions<ODataOptions>>().Value;
bool enableContinueOnErrorHeader = (options != null) ? options.EnableContinueOnErrorHeader : false;
bool enableContinueOnErrorHeader = false;
if (MiniMetadata != null)
{
enableContinueOnErrorHeader = MiniMetadata.Options.EnableContinueOnErrorHeader;
}
else
{
ODataOptions options = context.RequestServices.GetRequiredService<IOptions<ODataOptions>>().Value;
enableContinueOnErrorHeader = (options != null) ? options.EnableContinueOnErrorHeader : false;
}

SetContinueOnError(context.Request.Headers, enableContinueOnErrorHeader);

Expand Down Expand Up @@ -100,7 +110,23 @@ public virtual async Task<IList<ODataBatchRequestItem>> ParseBatchRequestsAsync(
}

HttpRequest request = context.Request;
IServiceProvider requestContainer = request.CreateRouteServices(PrefixName);

IServiceProvider requestContainer = null;
if (MiniMetadata != null)
{
requestContainer = MiniMetadata.ServiceProvider;

// We should dispose the scope after the request is processed?
// In the case of batch request, we can leave the GC to clean up the scope?
IServiceScope scope = requestContainer.GetRequiredService<IServiceScopeFactory>().CreateScope();
request.ODataFeature().Services = scope.ServiceProvider;
request.ODataFeature().RequestScope = scope;
}
else
{
requestContainer = request.CreateRouteServices(PrefixName);
}

requestContainer.GetRequiredService<ODataMessageReaderSettings>().BaseUri = GetBaseUri(request);

using (ODataMessageReader reader = request.GetODataMessageReader(requestContainer))
Expand Down Expand Up @@ -135,7 +161,7 @@ public virtual async Task<IList<ODataBatchRequestItem>> ParseBatchRequestsAsync(
}
}

return requests;
return requests;
}
}
}
16 changes: 16 additions & 0 deletions src/Microsoft.AspNetCore.OData/Batch/ODataBatchHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,11 @@ public virtual Uri GetBaseUri(HttpRequest request)
/// </summary>
internal bool ContinueOnError { get; private set; }

/// <summary>
/// Gets or sets the mini metadata for the OData batch handler.
/// </summary>
internal ODataMiniMetadata MiniMetadata { get; private set; }

/// <summary>
/// Set ContinueOnError based on the request and headers.
/// </summary>
Expand All @@ -118,4 +123,15 @@ internal void SetContinueOnError(IHeaderDictionary header, bool enableContinueOn
ContinueOnError = false;
}
}

internal void SetMinimalApi(HttpContext context)
{
ODataMiniMetadata metadata = null;
if (context.Items.TryGetValue(ODataMiniBatchMiddleware.MinimalApiMetadataKey, out object value))
{
metadata = value as ODataMiniMetadata;
}

MiniMetadata = metadata;
}
}
103 changes: 103 additions & 0 deletions src/Microsoft.AspNetCore.OData/Batch/ODataMiniBatchMiddleware.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
//-----------------------------------------------------------------------------
// <copyright file="ODataMiniBatchMiddleware.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.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.OData.Extensions;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Template;

namespace Microsoft.AspNetCore.OData.Batch;

/// <summary>
/// Defines the middleware for handling OData $batch requests in the minimal API scenarios.
/// This middleware essentially acts like branching middleware and redirects OData $batch
/// requests to the appropriate ODataBatchHandler.
/// </summary>
public class ODataMiniBatchMiddleware
{
internal const string MinimalApiMetadataKey = "MS_MinimalApiMetadataKey_386F76A4-5E7C-4B4D-8A20-261A23C3DD9A";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this key used for? Is it specific for minimal API?
Can I use another value other than the one you have provided?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's only for minimal API scenario only. It's a key to save the metadata into HttpContext.Items and retrieve it from the HttpContext is it's OData $batch request.


private readonly RequestDelegate _next;
private readonly TemplateMatcher _routeMatcher;
private readonly ODataBatchHandler _handler;
private readonly ODataMiniMetadata _metadata;

/// <summary>
/// Instantiates a new instance of <see cref="ODataBatchMiddleware"/>.
/// </summary>
/// <param name="routePattern">The route pattern for the OData $batch endpoint.</param>
/// <param name="handler">The handler for processing batch requests.</param>
/// <param name="metadata">the metadata for the OData $batch endpoint.</param>
/// <param name="next">The next middleware.</param>
public ODataMiniBatchMiddleware(string routePattern, ODataBatchHandler handler, ODataMiniMetadata metadata, RequestDelegate next)
{
if (routePattern == null)
{
throw Error.ArgumentNull(nameof(routePattern));
}

// ensure _routePattern starts with /
string newRouteTemplate = routePattern.StartsWith("/", StringComparison.Ordinal) ? routePattern.Substring(1) : routePattern;
RouteTemplate parsedTemplate = TemplateParser.Parse(newRouteTemplate);
_routeMatcher = new TemplateMatcher(parsedTemplate, new RouteValueDictionary());
_handler = handler ?? throw Error.ArgumentNull(nameof(handler));
_metadata = metadata ?? throw Error.ArgumentNull(nameof(metadata));

_next = next;
}

/// <summary>
/// Invoke the OData $Batch middleware.
/// </summary>
/// <param name="context">The http context.</param>
/// <returns>A task that can be awaited.</returns>
public async Task Invoke(HttpContext context)
{
if (context == null)
{
throw Error.ArgumentNull(nameof(context));
}

// The batch middleware should not handle the options requests for cors to properly function.
bool isPostRequest = HttpMethods.IsPost(context.Request.Method);
if (!isPostRequest)
{
await _next(context);
}
else
{
string path = context.Request.Path;
RouteValueDictionary routeData = new RouteValueDictionary();
if (_routeMatcher.TryMatch(path, routeData))
{
if (routeData.Count > 0)
{
Merge(context.ODataFeature().BatchRouteData, routeData);
}

// Add the ODataMiniMetadata to the context items for later retrieval.
context.Items.Add(MinimalApiMetadataKey, _metadata);

await _handler.ProcessBatchAsync(context, _next);
}
else
{
await _next(context);
}
}
}

private static void Merge(RouteValueDictionary batchRouteData, RouteValueDictionary routeData)
{
foreach (var item in routeData)
{
batchRouteData.Add(item.Key, item.Value);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
Expand Down Expand Up @@ -41,21 +40,45 @@ public override async Task ProcessBatchAsync(HttpContext context, RequestDelegat
return;
}

SetMinimalApi(context);

// This container is for the overall batch request.
HttpRequest request = context.Request;
IServiceProvider requestContainer = request.CreateRouteServices(PrefixName);

IServiceProvider requestContainer = null;
if (MiniMetadata != null)
{
requestContainer = MiniMetadata.ServiceProvider;

// We should dispose the scope after the request is processed?
// In the case of batch request, we can leave the GC to clean up the scope?
IServiceScope scope = requestContainer.GetRequiredService<IServiceScopeFactory>().CreateScope();
request.ODataFeature().Services = scope.ServiceProvider;
request.ODataFeature().RequestScope = scope;
}
else
{
requestContainer = request.CreateRouteServices(PrefixName);
}

requestContainer.GetRequiredService<ODataMessageReaderSettings>().BaseUri = GetBaseUri(request);
List<ODataBatchResponseItem> responses = new List<ODataBatchResponseItem>();

using (ODataMessageReader reader = request.GetODataMessageReader(requestContainer))
{
ODataBatchReader batchReader = await reader.CreateODataBatchReaderAsync().ConfigureAwait(false);
Guid batchId = Guid.NewGuid();

ODataOptions options = context.RequestServices.GetRequiredService<IOptions<ODataOptions>>().Value;
bool enableContinueOnErrorHeader = (options != null)
? options.EnableContinueOnErrorHeader
: false;
bool enableContinueOnErrorHeader = false;
if (MiniMetadata != null)
{
enableContinueOnErrorHeader = MiniMetadata.Options.EnableContinueOnErrorHeader;
}
else
{
ODataOptions options = context.RequestServices.GetRequiredService<IOptions<ODataOptions>>().Value;
enableContinueOnErrorHeader = (options != null) ? options.EnableContinueOnErrorHeader : false;
}

SetContinueOnError(request.Headers, enableContinueOnErrorHeader);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,10 +203,12 @@ internal static IServiceProvider GetOrCreateServiceProvider(this HttpContext htt

// 2. Retrieve it from metadata?
var endpoint = httpContext.GetEndpoint();
var odataMiniMetadata = endpoint.Metadata.GetMetadata<ODataMiniMetadata>();
var odataMiniMetadata = endpoint?.Metadata.GetMetadata<ODataMiniMetadata>();
if (odataMiniMetadata is not null)
{
odataFeature.Services = odataMiniMetadata.ServiceProvider;
IServiceScope scope = odataMiniMetadata.ServiceProvider.GetRequiredService<IServiceScopeFactory>().CreateScope();
odataFeature.Services = scope.ServiceProvider;
odataFeature.RequestScope = scope;
return odataFeature.Services;
}

Expand Down
Loading