diff --git a/sample/ODataMiniApi/AppModels.cs b/sample/ODataMiniApi/AppModels.cs index e037ea27..59e26fd0 100644 --- a/sample/ODataMiniApi/AppModels.cs +++ b/sample/ODataMiniApi/AppModels.cs @@ -23,6 +23,14 @@ public static IEdmModel GetEdmModel() builder.EntitySet("Customers"); builder.EntitySet("Orders"); builder.ComplexType(); + + var action = builder.EntityType().Action("RateByName"); + action.Parameter("name"); + action.Parameter("age"); + action.Returns(); + + builder.EntityType().Collection.Action("Rating").Parameter("p"); + return builder.GetEdmModel(); } } diff --git a/sample/ODataMiniApi/CustomersAndOrders.http b/sample/ODataMiniApi/CustomersAndOrders.http index 8aee8693..73907dfc 100644 --- a/sample/ODataMiniApi/CustomersAndOrders.http +++ b/sample/ODataMiniApi/CustomersAndOrders.http @@ -61,4 +61,32 @@ Content-Type: application/json { "name": "kerry" -} \ No newline at end of file +} + +### + +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 +} + +### diff --git a/sample/ODataMiniApi/Endpoints/CustomersEndpoints.cs b/sample/ODataMiniApi/Endpoints/CustomersEndpoints.cs index addf654e..083372d8 100644 --- a/sample/ODataMiniApi/Endpoints/CustomersEndpoints.cs +++ b/sample/ODataMiniApi/Endpoints/CustomersEndpoints.cs @@ -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; @@ -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 keysValues = new Dictionary(); + keysValues["Id"] = id; + + IEdmAction action = model.SchemaElements.OfType().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().First(a => a.Name == "Rating"); + + return new ODataPath(new EntitySetSegment(customers), + new OperationSegment(action, null) + ); + }); return app; } diff --git a/src/Microsoft.AspNetCore.OData/Deltas/DeltaOfT.cs b/src/Microsoft.AspNetCore.OData/Deltas/DeltaOfT.cs index bbd887a1..61e6b230 100644 --- a/src/Microsoft.AspNetCore.OData/Deltas/DeltaOfT.cs +++ b/src/Microsoft.AspNetCore.OData/Deltas/DeltaOfT.cs @@ -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; @@ -445,54 +437,7 @@ public void CopyChangedValues(T original) /// The built public static async ValueTask> BindAsync(HttpContext context, ParameterInfo parameter) { - ArgumentNullException.ThrowIfNull(context, nameof(context)); - ArgumentNullException.ThrowIfNull(parameter, nameof(parameter)); - - var endpoint = context.GetEndpoint(); - ODataMiniMetadata metadata = endpoint.Metadata.GetMetadata(); - 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); - IList toDispose = new List(); - 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); - } - - 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>(parameter); } /// diff --git a/src/Microsoft.AspNetCore.OData/Extensions/HttpContextExtensions.cs b/src/Microsoft.AspNetCore.OData/Extensions/HttpContextExtensions.cs index 60ad59b7..4e151c32 100644 --- a/src/Microsoft.AspNetCore.OData/Extensions/HttpContextExtensions.cs +++ b/src/Microsoft.AspNetCore.OData/Extensions/HttpContextExtensions.cs @@ -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; @@ -83,6 +88,18 @@ public static ODataOptions ODataOptions(this HttpContext httpContext) return httpContext.RequestServices?.GetService>()?.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() != null; + } + internal static IEdmModel GetOrCreateEdmModel(this HttpContext httpContext, Type clrType, ParameterInfo parameter = null) { if (httpContext == null) @@ -196,6 +213,74 @@ internal static IServiceProvider GetOrCreateServiceProvider(this HttpContext htt return null; } + internal static async ValueTask BindODataParameterAsync(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(); + 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 toDispose = new List(); + 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); diff --git a/src/Microsoft.AspNetCore.OData/Extensions/HttpRequestExtensions.cs b/src/Microsoft.AspNetCore.OData/Extensions/HttpRequestExtensions.cs index d786ef7e..bd79485c 100644 --- a/src/Microsoft.AspNetCore.OData/Extensions/HttpRequestExtensions.cs +++ b/src/Microsoft.AspNetCore.OData/Extensions/HttpRequestExtensions.cs @@ -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; @@ -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>()?.Value; + if (miniOptions is not null) + { + return miniOptions.TimeZone; + } + + return null; + } + return request.ODataOptions()?.TimeZone; } @@ -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>()?.Value; + if (miniOptions is not null) + { + return miniOptions.EnableNoDollarQueryOptions; + } + + return false; + } + return request.ODataOptions()?.EnableNoDollarQueryOptions ?? false; } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/ODataActionParameters.cs b/src/Microsoft.AspNetCore.OData/Formatter/ODataActionParameters.cs index d97c1641..ca4f6c54 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/ODataActionParameters.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/ODataActionParameters.cs @@ -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; @@ -19,4 +24,14 @@ namespace Microsoft.AspNetCore.OData.Formatter; [SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix", Justification = "ODataActionParameters is more appropriate here.")] public class ODataActionParameters : Dictionary { + /// + /// Binds the and to generate the . + /// + /// The HttpContext. + /// The parameter info. + /// The built + public static async ValueTask BindAsync(HttpContext context, ParameterInfo parameter) + { + return await context.BindODataParameterAsync(parameter); + } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/ODataUntypedActionParameters.cs b/src/Microsoft.AspNetCore.OData/Formatter/ODataUntypedActionParameters.cs index 286e28a2..d13de361 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/ODataUntypedActionParameters.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/ODataUntypedActionParameters.cs @@ -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; @@ -34,4 +39,15 @@ public ODataUntypedActionParameters(IEdmAction action) /// Gets the OData action of this parameters. /// public IEdmAction Action { get; } + + /// + /// Binds the and to generate the . + /// + /// The HttpContext. + /// The parameter info. + /// The built + public static async ValueTask BindAsync(HttpContext context, ParameterInfo parameter) + { + return await context.BindODataParameterAsync(parameter); + } } diff --git a/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml b/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml index e7730476..381baec9 100644 --- a/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml +++ b/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml @@ -4092,6 +4092,14 @@ to invoke a particular Action. The Parameter values are stored in the dictionary keyed using the Parameter name. + + + Binds the and to generate the . + + The HttpContext. + The parameter info. + The built + A model binder for ODataParameterValue values. @@ -4334,6 +4342,14 @@ Gets the OData action of this parameters. + + + Binds the and to generate the . + + The HttpContext. + The parameter info. + The built + Contains context information about the resource currently being serialized. @@ -6751,6 +6767,7 @@ Gets the OData version. + Please call 'SetVersion()' to config. @@ -6765,6 +6782,12 @@ Please call 'SetCaseInsensitive()' to config. + + + Gets TimeZoneInfo for the serialization and deserialization. + Please call 'SetTimeZoneInfo()' to config. + + Config whether or not no '$' sign query option. @@ -6779,6 +6802,13 @@ Case insensitive or not. The current instance to enable further configuration. + + + Config the time zone information. + + Case insensitive or not. + The current instance to enable further configuration. + Config the OData version. @@ -7316,6 +7346,11 @@ Looks up a localized string similar to A binary operator with incompatible types was detected. Found operand types '{0}' and '{1}' for operator kind '{2}'.. + + + Looks up a localized string similar to Cannot bind parameter '{0}'. the error is: {1}.. + + Looks up a localized string similar to The property '{0}' on type '{1}' returned a null value. The input stream contains collection items which cannot be added if the instance is null.. @@ -7891,6 +7926,11 @@ Looks up a localized string similar to Unknown function '{0}'.. + + + Looks up a localized string similar to The OData configuration is missing for the minimal API. Please call WithODataModel() and WithODataPathFactory() for the endpoint.. + + Looks up a localized string similar to The operation cannot be completed because no ODataPath is available for the request.. diff --git a/src/Microsoft.AspNetCore.OData/ODataMiniOptions.cs b/src/Microsoft.AspNetCore.OData/ODataMiniOptions.cs index f4d35dd4..466d0678 100644 --- a/src/Microsoft.AspNetCore.OData/ODataMiniOptions.cs +++ b/src/Microsoft.AspNetCore.OData/ODataMiniOptions.cs @@ -5,6 +5,7 @@ // //------------------------------------------------------------------------------ +using System; using Microsoft.AspNetCore.OData.Common; using Microsoft.AspNetCore.OData.Query; using Microsoft.OData; @@ -19,6 +20,7 @@ public class ODataMiniOptions private DefaultQueryConfigurations _queryConfigurations = new DefaultQueryConfigurations(); private bool _enableNoDollarQueryOptions = true; private bool _enableCaseInsensitive = true; + private TimeZoneInfo _timeZone = TimeZoneInfo.Local; private ODataVersion _version = ODataVersionConstraint.DefaultODataVersion; /// @@ -28,6 +30,7 @@ public class ODataMiniOptions /// /// Gets the OData version. + /// Please call 'SetVersion()' to config. /// public ODataVersion Version { get => _version; } @@ -43,6 +46,12 @@ public class ODataMiniOptions /// public bool EnableCaseInsensitive { get => _enableCaseInsensitive; } + /// + /// Gets TimeZoneInfo for the serialization and deserialization. + /// Please call 'SetTimeZoneInfo()' to config. + /// + public TimeZoneInfo TimeZone { get => _timeZone; } + /// /// Config whether or not no '$' sign query option. /// @@ -65,6 +74,17 @@ public ODataMiniOptions SetCaseInsensitive(bool enableCaseInsensitive) return this; } + /// + /// Config the time zone information. + /// + /// Case insensitive or not. + /// The current instance to enable further configuration. + public ODataMiniOptions SetTimeZoneInfo(TimeZoneInfo tzi) + { + _timeZone = tzi; + return this; + } + /// /// Config the OData version. /// @@ -175,5 +195,6 @@ internal void UpdateFrom(ODataMiniOptions otherOptions) this._version = otherOptions.Version; this._enableNoDollarQueryOptions = otherOptions.EnableNoDollarQueryOptions; this._enableCaseInsensitive = otherOptions.EnableCaseInsensitive; + this._timeZone = otherOptions.TimeZone; } } diff --git a/src/Microsoft.AspNetCore.OData/Properties/SRResources.Designer.cs b/src/Microsoft.AspNetCore.OData/Properties/SRResources.Designer.cs index 82246912..ac987d72 100644 --- a/src/Microsoft.AspNetCore.OData/Properties/SRResources.Designer.cs +++ b/src/Microsoft.AspNetCore.OData/Properties/SRResources.Designer.cs @@ -258,6 +258,15 @@ internal static string BinaryOperatorNotSupported { } } + /// + /// Looks up a localized string similar to Cannot bind parameter '{0}'. the error is: {1}.. + /// + internal static string BindParameterFailedOnMinimalAPIEndpoint { + get { + return ResourceManager.GetString("BindParameterFailedOnMinimalAPIEndpoint", resourceCulture); + } + } + /// /// Looks up a localized string similar to The property '{0}' on type '{1}' returned a null value. The input stream contains collection items which cannot be added if the instance is null.. /// @@ -1293,6 +1302,15 @@ internal static string ODataFunctionNotSupported { } } + /// + /// Looks up a localized string similar to The OData configuration is missing for the minimal API. Please call WithODataModel() and WithODataPathFactory() for the endpoint.. + /// + internal static string ODataMustBeSetOnMinimalAPIEndpoint { + get { + return ResourceManager.GetString("ODataMustBeSetOnMinimalAPIEndpoint", resourceCulture); + } + } + /// /// Looks up a localized string similar to The operation cannot be completed because no ODataPath is available for the request.. /// @@ -1841,7 +1859,7 @@ internal static string TypeMustBeRelated { return ResourceManager.GetString("TypeMustBeRelated", resourceCulture); } } - + /// /// Looks up a localized string similar to '{0}' is not a resource set type. Only resource set are supported.. /// @@ -1850,7 +1868,7 @@ internal static string TypeMustBeResourceSet { return ResourceManager.GetString("TypeMustBeResourceSet", resourceCulture); } } - + /// /// Looks up a localized string similar to The type '{0}' does not implement '{1}' interface.. /// @@ -1859,7 +1877,7 @@ internal static string TypeMustImplementInterface { return ResourceManager.GetString("TypeMustImplementInterface", resourceCulture); } } - + /// /// Looks up a localized string similar to The type '{0}' does not inherit from '{1}'.. /// @@ -1868,7 +1886,7 @@ internal static string TypeMustInheritFromType { return ResourceManager.GetString("TypeMustInheritFromType", resourceCulture); } } - + /// /// Looks up a localized string similar to The type '{0}' of dynamic property '{1}' is not supported.. /// diff --git a/src/Microsoft.AspNetCore.OData/Properties/SRResources.resx b/src/Microsoft.AspNetCore.OData/Properties/SRResources.resx index e41cf332..3b648e5e 100644 --- a/src/Microsoft.AspNetCore.OData/Properties/SRResources.resx +++ b/src/Microsoft.AspNetCore.OData/Properties/SRResources.resx @@ -763,4 +763,10 @@ The '{0}' property must be set. + + The OData configuration is missing for the minimal API. Please call WithODataModel() and WithODataPathFactory() for the endpoint. + + + Cannot bind parameter '{0}'. the error is: {1}. + \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.OData/PublicAPI.Unshipped.txt b/src/Microsoft.AspNetCore.OData/PublicAPI.Unshipped.txt index 6b14c10a..18bac592 100644 --- a/src/Microsoft.AspNetCore.OData/PublicAPI.Unshipped.txt +++ b/src/Microsoft.AspNetCore.OData/PublicAPI.Unshipped.txt @@ -39,8 +39,10 @@ Microsoft.AspNetCore.OData.ODataMiniOptions.Select() -> Microsoft.AspNetCore.ODa Microsoft.AspNetCore.OData.ODataMiniOptions.SetCaseInsensitive(bool enableCaseInsensitive) -> Microsoft.AspNetCore.OData.ODataMiniOptions Microsoft.AspNetCore.OData.ODataMiniOptions.SetMaxTop(int? maxTopValue) -> Microsoft.AspNetCore.OData.ODataMiniOptions Microsoft.AspNetCore.OData.ODataMiniOptions.SetNoDollarQueryOptions(bool enableNoDollarQueryOptions) -> Microsoft.AspNetCore.OData.ODataMiniOptions +Microsoft.AspNetCore.OData.ODataMiniOptions.SetTimeZoneInfo(System.TimeZoneInfo tzi) -> Microsoft.AspNetCore.OData.ODataMiniOptions Microsoft.AspNetCore.OData.ODataMiniOptions.SetVersion(Microsoft.OData.ODataVersion version) -> Microsoft.AspNetCore.OData.ODataMiniOptions Microsoft.AspNetCore.OData.ODataMiniOptions.SkipToken() -> Microsoft.AspNetCore.OData.ODataMiniOptions +Microsoft.AspNetCore.OData.ODataMiniOptions.TimeZone.get -> System.TimeZoneInfo Microsoft.AspNetCore.OData.ODataMiniOptions.Version.get -> Microsoft.OData.ODataVersion Microsoft.AspNetCore.OData.Query.IODataQueryEndpointFilter Microsoft.AspNetCore.OData.Query.IODataQueryEndpointFilter.OnFilterExecutedAsync(object responseValue, Microsoft.AspNetCore.OData.Extensions.ODataQueryFilterInvocationContext context) -> System.Threading.Tasks.ValueTask @@ -56,6 +58,9 @@ Microsoft.AspNetCore.OData.Results.IODataResult.ExpectedType.get -> System.Type Microsoft.AspNetCore.OData.Results.IODataResult.Value.get -> object override Microsoft.AspNetCore.OData.Query.Wrapper.SelectExpandWrapperConverter.CanConvert(System.Type typeToConvert) -> bool override Microsoft.AspNetCore.OData.Query.Wrapper.SelectExpandWrapperConverter.CreateConverter(System.Type type, System.Text.Json.JsonSerializerOptions options) -> System.Text.Json.Serialization.JsonConverter +static Microsoft.AspNetCore.OData.Deltas.Delta.BindAsync(Microsoft.AspNetCore.Http.HttpContext context, System.Reflection.ParameterInfo parameter) -> System.Threading.Tasks.ValueTask> +static Microsoft.AspNetCore.OData.Formatter.ODataActionParameters.BindAsync(Microsoft.AspNetCore.Http.HttpContext context, System.Reflection.ParameterInfo parameter) -> System.Threading.Tasks.ValueTask +static Microsoft.AspNetCore.OData.Formatter.ODataUntypedActionParameters.BindAsync(Microsoft.AspNetCore.Http.HttpContext context, System.Reflection.ParameterInfo parameter) -> System.Threading.Tasks.ValueTask static Microsoft.AspNetCore.OData.ODataEndpointConventionBuilderExtensions.AddODataQueryEndpointFilter(this Microsoft.AspNetCore.Builder.RouteHandlerBuilder builder, Microsoft.AspNetCore.OData.Query.IODataQueryEndpointFilter queryFilter) -> Microsoft.AspNetCore.Builder.RouteHandlerBuilder static Microsoft.AspNetCore.OData.ODataEndpointConventionBuilderExtensions.AddODataQueryEndpointFilter(this Microsoft.AspNetCore.Builder.RouteHandlerBuilder builder, System.Action validationSetup = null, System.Action querySetup = null) -> Microsoft.AspNetCore.Builder.RouteHandlerBuilder static Microsoft.AspNetCore.OData.ODataEndpointConventionBuilderExtensions.AddODataQueryEndpointFilter(this Microsoft.AspNetCore.Routing.RouteGroupBuilder builder, Microsoft.AspNetCore.OData.Query.IODataQueryEndpointFilter queryFilter) -> Microsoft.AspNetCore.Routing.RouteGroupBuilder diff --git a/src/Microsoft.AspNetCore.OData/Query/ODataQueryContext.cs b/src/Microsoft.AspNetCore.OData/Query/ODataQueryContext.cs index 904e0539..8e1f364b 100644 --- a/src/Microsoft.AspNetCore.OData/Query/ODataQueryContext.cs +++ b/src/Microsoft.AspNetCore.OData/Query/ODataQueryContext.cs @@ -15,7 +15,6 @@ using Microsoft.AspNetCore.OData.Routing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; -using Microsoft.OData; using Microsoft.OData.Edm; using Microsoft.OData.UriParser; @@ -226,20 +225,4 @@ private DefaultQueryConfigurations GetDefaultQueryConfigurations() return odataOptions.Value.QueryConfigurations; } - - private DefaultQueryConfigurations GetDefaultQuerySettings() - { - if (Request is null) - { - return new DefaultQueryConfigurations(); - } - - IOptions odataOptions = Request.HttpContext?.RequestServices?.GetService>(); - if (odataOptions is null || odataOptions.Value is null) - { - return new DefaultQueryConfigurations(); - } - - return odataOptions.Value.QueryConfigurations; - } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Deltas/DeltaTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Deltas/DeltaTests.cs index 61e24f41..44f89f5d 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Deltas/DeltaTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Deltas/DeltaTests.cs @@ -5,6 +5,13 @@ // //------------------------------------------------------------------------------ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.OData.Deltas; +using Microsoft.AspNetCore.OData.Formatter; +using Microsoft.AspNetCore.OData.TestCommon; +using Microsoft.AspNetCore.OData.Tests.Commons; +using Microsoft.AspNetCore.OData.Tests.Models; +using Microsoft.OData.Edm; using System; using System.Collections.Generic; using System.Collections.ObjectModel; @@ -12,11 +19,7 @@ using System.Linq; using System.Reflection; using System.Runtime.Serialization; -using Microsoft.AspNetCore.OData.Deltas; -using Microsoft.AspNetCore.OData.TestCommon; -using Microsoft.AspNetCore.OData.Tests.Commons; -using Microsoft.AspNetCore.OData.Tests.Models; -using Microsoft.OData.Edm; +using System.Threading.Tasks; using Xunit; namespace Microsoft.AspNetCore.OData.Tests.Deltas; @@ -60,6 +63,19 @@ public static IEnumerable DeltaModelPropertyNamesData } } + [Fact] + public async ValueTask BindAsync_ThrowsArgumentNull_ForInputs() + { + // Arrange & Act & Assert + ArgumentNullException exception = await ExceptionAssert.ThrowsAsync(async () => await Delta.BindAsync(null, null), "httpContext", false, true); + Assert.Equal("The parameter cannot be null. (Parameter 'httpContext')", exception.Message); + + // Arrange & Act & Assert + HttpContext httpContext = new DefaultHttpContext(); + await ExceptionAssert.ThrowsAsync(async () => await Delta.BindAsync(httpContext, null)); + Assert.Equal("The parameter cannot be null. (Parameter 'parameter')", exception.Message); + } + [Fact] public void TryGetPropertyValue_ThrowsArgumentNull_original() { diff --git a/test/Microsoft.AspNetCore.OData.Tests/Extensions/HttpContextExtensionsTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Extensions/HttpContextExtensionsTests.cs index 2b47e40e..3b1ff21f 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Extensions/HttpContextExtensionsTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Extensions/HttpContextExtensionsTests.cs @@ -5,7 +5,9 @@ // //------------------------------------------------------------------------------ +using System.Threading.Tasks; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.OData.Abstracts; using Microsoft.AspNetCore.OData.Extensions; using Microsoft.AspNetCore.OData.Tests.Commons; using Xunit; @@ -29,4 +31,84 @@ public void ODataBatchFeature_ThrowsArgumentNull_HttpContext() HttpContext httpContext = null; ExceptionAssert.ThrowsArgumentNull(() => httpContext.ODataBatchFeature(), "httpContext"); } + + [Fact] + public void ODataOptions_ThrowsArgumentNull_HttpContext() + { + // Arrange & Act & Assert + HttpContext httpContext = null; + ExceptionAssert.ThrowsArgumentNull(() => httpContext.ODataOptions(), "httpContext"); + } + + [Fact] + public void ODataFeature_ReturnsODataFeature() + { + // Arrange + HttpContext httpContext = new DefaultHttpContext(); + IODataFeature odataFeature = new ODataFeature(); + httpContext.Features.Set(odataFeature); + + // Act + IODataFeature result = httpContext.ODataFeature(); + + // Assert + Assert.Same(odataFeature, result); + } + + [Fact] + public void IsMinimalEndpoint_ThrowsArgumentNull_HttpContext() + { + // Arrange & Act & Assert + HttpContext httpContext = null; + ExceptionAssert.ThrowsArgumentNull(() => httpContext.IsMinimalEndpoint(), "httpContext"); + } + + [Fact] + public void IsMinimalEndpoint_ReturnsFalse_WhenEndpointIsNull() + { + // Arrange + HttpContext httpContext = new DefaultHttpContext(); + httpContext.SetEndpoint(null); + + // Act + bool result = httpContext.IsMinimalEndpoint(); + + // Assert + Assert.False(result); + } + + [Fact] + public void IsMinimalEndpoint_ReturnsFalse_WhenEndpointIsNotNull_WithoutMetadata() + { + // Arrange + HttpContext httpContext = new DefaultHttpContext(); + Endpoint endpoint = new Endpoint((context) => Task.CompletedTask, EndpointMetadataCollection.Empty, "TestEndpoint"); + httpContext.SetEndpoint(endpoint); + + // Act + bool result = httpContext.IsMinimalEndpoint(); + + // Assert + Assert.False(result); + } + + [Fact] + public void IsMinimalEndpoint_ReturnsTrue_WhenEndpointHasMetadata() + { + // Arrange + HttpContext httpContext = new DefaultHttpContext(); + Endpoint endpoint = new Endpoint( + (context) => Task.CompletedTask, + new EndpointMetadataCollection([new ODataMiniMetadata()]), + "TestEndpoint"); + + httpContext.SetEndpoint(endpoint); + + // Act + bool result = httpContext.IsMinimalEndpoint(); + + // Assert + Assert.True(result); + } + } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/ODataActionParametersTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/ODataActionParametersTests.cs new file mode 100644 index 00000000..5f324b3c --- /dev/null +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/ODataActionParametersTests.cs @@ -0,0 +1,76 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +using System; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.OData.Formatter; +using Microsoft.AspNetCore.OData.Formatter.Deserialization; +using Microsoft.AspNetCore.OData.Tests.Commons; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.OData; +using Microsoft.OData.Edm; +using Microsoft.OData.UriParser; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.OData.Tests.Formatter; + +public class ODataActionParametersTests +{ + [Fact] + public async ValueTask BindAsync_ThrowsArgumentNull_ForInputs() + { + // Arrange & Act & Assert + ArgumentNullException exception = await ExceptionAssert.ThrowsAsync(async () => await ODataActionParameters.BindAsync(null, null), "httpContext", false, true); + Assert.Equal("The parameter cannot be null. (Parameter 'httpContext')", exception.Message); + + // Arrange & Act & Assert + HttpContext httpContext = new DefaultHttpContext(); + await ExceptionAssert.ThrowsAsync(async () => await ODataActionParameters.BindAsync(httpContext, null)); + Assert.Equal("The parameter cannot be null. (Parameter 'parameter')", exception.Message); + } + + [Fact] + public async ValueTask BindAsync_Returns_ValidODataActionParameter() + { + // Arrange + Mock deserializerProviderMock = new Mock(); + Mock mock = new Mock(deserializerProviderMock.Object); + + ODataActionParameters expectedParameters = new ODataActionParameters(); + + mock.Setup(m => m.ReadAsync(It.IsAny(), typeof(ODataActionParameters), It.IsAny())) + .ReturnsAsync(expectedParameters); + + HttpContext httpContext = new DefaultHttpContext(); + + ODataMiniMetadata metadata = new ODataMiniMetadata(); + metadata.Model = EdmCoreModel.Instance; + metadata.PathFactory = (c, t) => new ODataPath(); + metadata.BaseAddressFactory = c => new Uri("http://localhost/odata/"); + metadata.Services = services => services.AddSingleton(s => mock.Object); + + Endpoint endpoint = new Endpoint( + (context) => Task.CompletedTask, + new EndpointMetadataCollection([metadata]), + "TestEndpoint"); + + httpContext.SetEndpoint(endpoint); + ParameterInfo parameter = typeof(ODataActionParametersTests).GetMethod("TestMethod", BindingFlags.NonPublic | BindingFlags.Static).GetParameters().First(); + + ODataActionParameters actualParameter = await ODataActionParameters.BindAsync(httpContext, parameter); + + // Act & Assert + Assert.Same(expectedParameters, actualParameter); + } + + // This empty method is used to provide a parameter for the BindAsync test. + private static void TestMethod(ODataActionParameters parameters) { } +} diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/ODataUntypedActionParametersTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/ODataUntypedActionParametersTests.cs index baa102c7..fa622846 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/ODataUntypedActionParametersTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/ODataUntypedActionParametersTests.cs @@ -5,8 +5,19 @@ // //------------------------------------------------------------------------------ +using System; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.OData.Formatter; +using Microsoft.AspNetCore.OData.Formatter.Deserialization; using Microsoft.AspNetCore.OData.Tests.Commons; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.OData; +using Microsoft.OData.Edm; +using Microsoft.OData.UriParser; +using Moq; using Xunit; namespace Microsoft.AspNetCore.OData.Tests.Formatter; @@ -19,4 +30,54 @@ public void Ctor_ThrowsArgumentNull_Action() // Arrange & Act & Assert ExceptionAssert.ThrowsArgumentNull(() => new ODataUntypedActionParameters(null), "action"); } + + [Fact] + public async ValueTask BindAsync_ThrowsArgumentNull_ForInputs() + { + // Arrange & Act & Assert + ArgumentNullException exception = await ExceptionAssert.ThrowsAsync(async () => await ODataUntypedActionParameters.BindAsync(null, null), "httpContext", false, true); + Assert.Equal("The parameter cannot be null. (Parameter 'httpContext')", exception.Message); + + // Arrange & Act & Assert + HttpContext httpContext = new DefaultHttpContext(); + await ExceptionAssert.ThrowsAsync(async () => await ODataUntypedActionParameters.BindAsync(httpContext, null)); + Assert.Equal("The parameter cannot be null. (Parameter 'parameter')", exception.Message); + } + + [Fact] + public async ValueTask BindAsync_Returns_ValidODataUntypedActionParameter() + { + // Arrange + Mock deserializerProviderMock = new Mock(); + Mock mock = new Mock(deserializerProviderMock.Object); + + ODataUntypedActionParameters expectedParameters = new ODataUntypedActionParameters(new Mock().Object); + + mock.Setup(m => m.ReadAsync(It.IsAny(), typeof(ODataUntypedActionParameters), It.IsAny())) + .ReturnsAsync(expectedParameters); + + HttpContext httpContext = new DefaultHttpContext(); + + ODataMiniMetadata metadata = new ODataMiniMetadata(); + metadata.Model = EdmCoreModel.Instance; + metadata.PathFactory = (c, t) => new ODataPath(); + metadata.BaseAddressFactory = c => new Uri("http://localhost/odata/"); + metadata.Services = services => services.AddSingleton(s => mock.Object); + + Endpoint endpoint = new Endpoint( + (context) => Task.CompletedTask, + new EndpointMetadataCollection([metadata]), + "TestEndpoint"); + + httpContext.SetEndpoint(endpoint); + ParameterInfo parameter = typeof(ODataActionParametersTests).GetMethod("TestMethod", BindingFlags.NonPublic | BindingFlags.Static).GetParameters().First(); + + ODataUntypedActionParameters actualParameter = await ODataUntypedActionParameters.BindAsync(httpContext, parameter); + + // Act & Assert + Assert.Same(expectedParameters, actualParameter); + } + + // This empty method is used to provide a parameter for the BindAsync test. + private static void TestMethod(ODataUntypedActionParameters parameters) { } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.bsl b/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.bsl index 1cb9c92e..4909836f 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.bsl +++ b/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.bsl @@ -198,6 +198,7 @@ public class Microsoft.AspNetCore.OData.ODataMiniOptions { bool EnableCaseInsensitive { public get; } bool EnableNoDollarQueryOptions { public get; } Microsoft.AspNetCore.OData.Query.DefaultQueryConfigurations QueryConfigurations { public get; } + System.TimeZoneInfo TimeZone { public get; } Microsoft.OData.ODataVersion Version { public get; } public Microsoft.AspNetCore.OData.ODataMiniOptions Count () @@ -209,6 +210,7 @@ public class Microsoft.AspNetCore.OData.ODataMiniOptions { public Microsoft.AspNetCore.OData.ODataMiniOptions SetCaseInsensitive (bool enableCaseInsensitive) public Microsoft.AspNetCore.OData.ODataMiniOptions SetMaxTop (System.Nullable`1[[System.Int32]] maxTopValue) public Microsoft.AspNetCore.OData.ODataMiniOptions SetNoDollarQueryOptions (bool enableNoDollarQueryOptions) + public Microsoft.AspNetCore.OData.ODataMiniOptions SetTimeZoneInfo (System.TimeZoneInfo tzi) public Microsoft.AspNetCore.OData.ODataMiniOptions SetVersion (Microsoft.OData.ODataVersion version) public Microsoft.AspNetCore.OData.ODataMiniOptions SkipToken () } @@ -1157,6 +1159,11 @@ NonValidatingParameterBindingAttribute(), ] public class Microsoft.AspNetCore.OData.Formatter.ODataActionParameters : System.Collections.Generic.Dictionary`2[[System.String],[System.Object]], ICollection, IDictionary, IEnumerable, IDeserializationCallback, ISerializable, IDictionary`2, IReadOnlyDictionary`2, ICollection`1, IEnumerable`1, IReadOnlyCollection`1 { public ODataActionParameters () + + [ + AsyncStateMachineAttribute(), + ] + public static System.Threading.Tasks.ValueTask`1[[Microsoft.AspNetCore.OData.Formatter.ODataActionParameters]] BindAsync (Microsoft.AspNetCore.Http.HttpContext context, System.Reflection.ParameterInfo parameter) } public class Microsoft.AspNetCore.OData.Formatter.ODataInputFormatter : Microsoft.AspNetCore.Mvc.Formatters.TextInputFormatter, IApiRequestFormatMetadataProvider, IInputFormatter { @@ -1206,6 +1213,11 @@ public class Microsoft.AspNetCore.OData.Formatter.ODataUntypedActionParameters : public ODataUntypedActionParameters (Microsoft.OData.Edm.IEdmAction action) Microsoft.OData.Edm.IEdmAction Action { public get; } + + [ + AsyncStateMachineAttribute(), + ] + public static System.Threading.Tasks.ValueTask`1[[Microsoft.AspNetCore.OData.Formatter.ODataUntypedActionParameters]] BindAsync (Microsoft.AspNetCore.Http.HttpContext context, System.Reflection.ParameterInfo parameter) } public class Microsoft.AspNetCore.OData.Formatter.ResourceContext {