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
8 changes: 8 additions & 0 deletions sample/ODataMiniApi/AppModels.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ public static IEdmModel GetEdmModel()
builder.EntitySet<Customer>("Customers");
builder.EntitySet<Order>("Orders");
builder.ComplexType<Info>();

var action = builder.EntityType<Customer>().Action("RateByName");
action.Parameter<string>("name");
action.Parameter<int>("age");
action.Returns<string>();

builder.EntityType<Customer>().Collection.Action("Rating").Parameter<int>("p");

return builder.GetEdmModel();
}
}
Expand Down
30 changes: 29 additions & 1 deletion sample/ODataMiniApi/CustomersAndOrders.http
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,32 @@ Content-Type: application/json

{
"name": "kerry"
}
}

###

POST {{ODataMiniApi_HostAddress}}/v1/customers/1/rateByName
Content-Type: application/json

{
"name": "kerry",
"age":16
}

###

# You will get the following response:
#
#{
# "@odata.context": "http://localhost:5177/$metadata#Edm.String",
# "value": "EdmActionName: 'Rating': rate based on '8'"
#}

POST {{ODataMiniApi_HostAddress}}/v1/customers/rating
Content-Type: application/json

{
"p": 8
}

###
48 changes: 48 additions & 0 deletions sample/ODataMiniApi/Endpoints/CustomersEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

using Microsoft.AspNetCore.OData;
using Microsoft.AspNetCore.OData.Deltas;
using Microsoft.AspNetCore.OData.Formatter;
using Microsoft.AspNetCore.OData.Query;
using Microsoft.EntityFrameworkCore;
using Microsoft.OData;
Expand Down Expand Up @@ -111,6 +112,53 @@ public static IEndpointRouteBuilder MapCustomersEndpoints(this IEndpointRouteBui
keysValues["Id"] = id;
return new ODataPath(new EntitySetSegment(customers), new KeySegment(keysValues, customers.EntityType, customers));
});

app.MapPost("v1/customers/{id}/rateByName", (AppDb db, int id, ODataActionParameters parameters) =>
{
Customer customer = db.Customers.FirstOrDefault(s => s.Id == id);
if (customer == null)
{
return null; // should return Results.NotFound();
};

return $"{customer.Name}: {System.Text.Json.JsonSerializer.Serialize(parameters)}";
})
.WithODataResult()
.WithODataModel(model)
.WithODataPathFactory(
(h, t) =>
{
string idStr = h.GetRouteValue("id") as string;
int id = int.Parse(idStr);
IEdmEntitySet customers = model.FindDeclaredEntitySet("Customers");

IDictionary<string, object> keysValues = new Dictionary<string, object>();
keysValues["Id"] = id;

IEdmAction action = model.SchemaElements.OfType<IEdmAction>().First(a => a.Name == "RateByName");

return new ODataPath(new EntitySetSegment(customers),
new KeySegment(keysValues, customers.EntityType, customers),
new OperationSegment(action, null)
);
});

app.MapPost("v1/customers/rating", (AppDb db, ODataUntypedActionParameters parameters) =>
{
return $"EdmActionName: '{parameters.Action.Name}': rate based on '{parameters["p"]}'";
})
.WithODataResult()
.WithODataModel(model)
.WithODataPathFactory(
(h, t) =>
{
IEdmEntitySet customers = model.FindDeclaredEntitySet("Customers");
IEdmAction action = model.SchemaElements.OfType<IEdmAction>().First(a => a.Name == "Rating");

return new ODataPath(new EntitySetSegment(customers),
new OperationSegment(action, null)
);
});
return app;
}

Expand Down
57 changes: 1 addition & 56 deletions src/Microsoft.AspNetCore.OData/Deltas/DeltaOfT.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,6 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.OData.Abstracts;
using Microsoft.AspNetCore.OData.Common;
using Microsoft.AspNetCore.OData.Query;
using Microsoft.OData.Edm;
using Microsoft.OData.UriParser;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.OData.Formatter;
using Microsoft.AspNetCore.OData.Results;
using Microsoft.OData;
using System.Net.Http;
using Microsoft.AspNetCore.OData.Extensions;

namespace Microsoft.AspNetCore.OData.Deltas;
Expand Down Expand Up @@ -445,54 +437,7 @@ public void CopyChangedValues(T original)
/// <returns>The built <see cref="Delta{T}"/></returns>
public static async ValueTask<Delta<T>> BindAsync(HttpContext context, ParameterInfo parameter)
{
ArgumentNullException.ThrowIfNull(context, nameof(context));
ArgumentNullException.ThrowIfNull(parameter, nameof(parameter));

var endpoint = context.GetEndpoint();
ODataMiniMetadata metadata = endpoint.Metadata.GetMetadata<ODataMiniMetadata>();
if (metadata is null || metadata.Model is null || metadata.PathFactory is null)
{
throw new ODataException($"Please call WithODataModel and WithODataPathFactory for the endpoint");
}

IEdmModel model = metadata.Model;

context.ODataFeature().Model = model;
context.ODataFeature().Services = context.GetOrCreateServiceProvider();
context.ODataFeature().Path = metadata.PathFactory(context, typeof(T));
HttpRequest request = context.Request;
Type type = typeof(Delta<T>);
IList<IDisposable> toDispose = new List<IDisposable>();
Uri baseAddress = GetBaseAddress(context, metadata);

ODataVersion version = ODataResult.GetODataVersion(request, metadata);

object result = await ODataInputFormatter.ReadFromStreamAsync(
type,
defaultValue: null,
baseAddress,
version,
request,
toDispose).ConfigureAwait(false);

foreach (IDisposable obj in toDispose)
{
obj.Dispose();
}

return await ValueTask.FromResult(result as Delta<T>);
}

private static Uri GetBaseAddress(HttpContext httpContext, ODataMiniMetadata options)
{
if (options.BaseAddressFactory is not null)
{
return options.BaseAddressFactory(httpContext);
}
else
{
return ODataInputFormatter.GetDefaultBaseAddress(httpContext.Request);
}
return await context.BindODataParameterAsync<Delta<T>>(parameter);
}

/// <summary>
Expand Down
85 changes: 85 additions & 0 deletions src/Microsoft.AspNetCore.OData/Extensions/HttpContextExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,18 @@
//------------------------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.OData.Abstracts;
using Microsoft.AspNetCore.OData.Edm;
using Microsoft.AspNetCore.OData.Formatter;
using Microsoft.AspNetCore.OData.Results;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Microsoft.OData;
using Microsoft.OData.Edm;
using Microsoft.OData.ModelBuilder;
using Microsoft.OData.UriParser;
Expand Down Expand Up @@ -83,6 +88,18 @@ public static ODataOptions ODataOptions(this HttpContext httpContext)
return httpContext.RequestServices?.GetService<IOptions<ODataOptions>>()?.Value;
}

internal static bool IsMinimalEndpoint(this HttpContext httpContext)
{
if (httpContext == null)
{
throw Error.ArgumentNull(nameof(httpContext));
}

// Check if the endpoint is a minimal endpoint.
var endpoint = httpContext.GetEndpoint();
return endpoint?.Metadata.GetMetadata<ODataMiniMetadata>() != null;
}

internal static IEdmModel GetOrCreateEdmModel(this HttpContext httpContext, Type clrType, ParameterInfo parameter = null)
{
if (httpContext == null)
Expand Down Expand Up @@ -196,6 +213,74 @@ internal static IServiceProvider GetOrCreateServiceProvider(this HttpContext htt
return null;
}

internal static async ValueTask<T> BindODataParameterAsync<T>(this HttpContext httpContext, ParameterInfo parameter)
where T : class
{
ArgumentNullException.ThrowIfNull(httpContext, nameof(httpContext));
ArgumentNullException.ThrowIfNull(parameter, nameof(parameter));

var endpoint = httpContext.GetEndpoint();
ODataMiniMetadata metadata = endpoint.Metadata.GetMetadata<ODataMiniMetadata>();
if (metadata is null || metadata.Model is null || metadata.PathFactory is null)
{
throw new ODataException(SRResources.ODataMustBeSetOnMinimalAPIEndpoint);
}

Type parameterType = parameter.ParameterType;

IEdmModel model = metadata.Model;

IODataFeature oDataFeature = httpContext.ODataFeature();
oDataFeature.Model = model;
oDataFeature.Services = httpContext.GetOrCreateServiceProvider();
oDataFeature.Path = metadata.PathFactory(httpContext, parameterType);
HttpRequest request = httpContext.Request;
IList<IDisposable> toDispose = new List<IDisposable>();
Uri baseAddress = httpContext.GetInputBaseAddress(metadata);

ODataVersion version = ODataResult.GetODataVersion(request, metadata);

object result = null;
try
{
result = await ODataInputFormatter.ReadFromStreamAsync(
parameterType,
defaultValue: null,
baseAddress,
version,
request,
toDispose).ConfigureAwait(false);

foreach (IDisposable obj in toDispose)
{
obj.Dispose();
}
}
catch (Exception ex)
{
throw new ODataException(Error.Format(SRResources.BindParameterFailedOnMinimalAPIEndpoint, parameter.Name, ex.Message));
}

return result as T;
}

internal static Uri GetInputBaseAddress(this HttpContext httpContext, ODataMiniMetadata options)
{
if (httpContext == null)
{
throw Error.ArgumentNull(nameof(httpContext));
}

if (options.BaseAddressFactory is not null)
{
return options.BaseAddressFactory(httpContext);
}
else
{
return ODataInputFormatter.GetDefaultBaseAddress(httpContext.Request);
}
}

internal static IServiceProvider BuildDefaultServiceProvider(this HttpContext httpContext, IEdmModel model)
{
ArgumentNullException.ThrowIfNull(httpContext);
Expand Down
27 changes: 27 additions & 0 deletions src/Microsoft.AspNetCore.OData/Extensions/HttpRequestExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
using Microsoft.AspNetCore.OData.Formatter.Deserialization;
using Microsoft.AspNetCore.OData.Query;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
using Microsoft.OData;
using Microsoft.OData.Edm;
Expand Down Expand Up @@ -98,6 +99,19 @@ public static TimeZoneInfo GetTimeZoneInfo(this HttpRequest request)
throw Error.ArgumentNull(nameof(request));
}

bool isMinimalApi = request.HttpContext.IsMinimalEndpoint();
if (isMinimalApi)
{
// In minimal API, we don't have ODataOptions, so we check the ODataMiniOptions directly.
ODataMiniOptions miniOptions = request.HttpContext.RequestServices?.GetService<IOptions<ODataMiniOptions>>()?.Value;
if (miniOptions is not null)
{
return miniOptions.TimeZone;
}

return null;
}

return request.ODataOptions()?.TimeZone;
}

Expand All @@ -113,6 +127,19 @@ public static bool IsNoDollarQueryEnable(this HttpRequest request)
throw Error.ArgumentNull(nameof(request));
}

bool isMinimalApi = request.HttpContext.IsMinimalEndpoint();
if (isMinimalApi)
{
// In minimal API, we don't have ODataOptions, so we check the ODataMiniOptions directly.
ODataMiniOptions miniOptions = request.HttpContext.RequestServices?.GetService<IOptions<ODataMiniOptions>>()?.Value;
if (miniOptions is not null)
{
return miniOptions.EnableNoDollarQueryOptions;
}

return false;
}

return request.ODataOptions()?.EnableNoDollarQueryOptions ?? false;
}

Expand Down
15 changes: 15 additions & 0 deletions src/Microsoft.AspNetCore.OData/Formatter/ODataActionParameters.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@

using System.Diagnostics.CodeAnalysis;
using System.Collections.Generic;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNetCore.OData.Abstracts;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.OData.Deltas;
using Microsoft.AspNetCore.OData.Extensions;

namespace Microsoft.AspNetCore.OData.Formatter;

Expand All @@ -19,4 +24,14 @@ namespace Microsoft.AspNetCore.OData.Formatter;
[SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix", Justification = "ODataActionParameters is more appropriate here.")]
public class ODataActionParameters : Dictionary<string, object>
{
/// <summary>
/// Binds the <see cref="HttpContext"/> and <see cref="ParameterInfo"/> to generate the <see cref="Delta{T}"/>.
/// </summary>
/// <param name="context">The HttpContext.</param>
/// <param name="parameter">The parameter info.</param>
/// <returns>The built <see cref="ODataActionParameters"/></returns>
public static async ValueTask<ODataActionParameters> BindAsync(HttpContext context, ParameterInfo parameter)
{
return await context.BindODataParameterAsync<ODataActionParameters>(parameter);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@
using System.Collections.Generic;
using Microsoft.OData.Edm;
using Microsoft.AspNetCore.OData.Abstracts;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.OData.Deltas;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNetCore.OData.Extensions;

namespace Microsoft.AspNetCore.OData.Formatter;

Expand All @@ -34,4 +39,15 @@ public ODataUntypedActionParameters(IEdmAction action)
/// Gets the OData action of this parameters.
/// </summary>
public IEdmAction Action { get; }

/// <summary>
/// Binds the <see cref="HttpContext"/> and <see cref="ParameterInfo"/> to generate the <see cref="Delta{T}"/>.
/// </summary>
/// <param name="context">The HttpContext.</param>
/// <param name="parameter">The parameter info.</param>
/// <returns>The built <see cref="ODataUntypedActionParameters"/></returns>
public static async ValueTask<ODataUntypedActionParameters> BindAsync(HttpContext context, ParameterInfo parameter)
{
return await context.BindODataParameterAsync<ODataUntypedActionParameters>(parameter);
}
}
Loading