Skip to content
Open
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
15 changes: 15 additions & 0 deletions src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8631,6 +8631,11 @@
A value that corresponds to allowing 'MatchesPattern' function in $filter.
</summary>
</member>
<member name="F:Microsoft.AspNetCore.OData.Query.AllowedFunctions.TotalSeconds">
<summary>
A value that corresponds to allowing 'TotalSeconds' function in $filter.
</summary>
</member>
<member name="F:Microsoft.AspNetCore.OData.Query.AllowedFunctions.AllStringFunctions">
<summary>
A value that corresponds to allowing all string related functions in $filter.
Expand Down Expand Up @@ -10741,6 +10746,16 @@
<param name="context">The query binder context.</param>
<returns>The LINQ <see cref="T:System.Linq.Expressions.Expression"/> created.</returns>
</member>
<member name="M:Microsoft.AspNetCore.OData.Query.Expressions.QueryBinder.BindTotalSeconds(Microsoft.OData.UriParser.SingleValueFunctionCallNode,Microsoft.AspNetCore.OData.Query.Expressions.QueryBinderContext)">
<summary>
Binds the OData V4 canonical 'totalseconds' function to a LINQ <see cref="T:System.Linq.Expressions.Expression"/>.
Per the OData V4 spec, totalseconds(Edm.Duration) returns an Edm.Decimal with the duration
expressed as a fractional number of seconds. The CLR mapping is <see cref="P:System.TimeSpan.TotalSeconds"/>.
</summary>
<param name="node">The query node to bind.</param>
<param name="context">The query binder context.</param>
<returns>The LINQ <see cref="T:System.Linq.Expressions.Expression"/> created.</returns>
</member>
<member name="M:Microsoft.AspNetCore.OData.Query.Expressions.QueryBinder.BindRound(Microsoft.OData.UriParser.SingleValueFunctionCallNode,Microsoft.AspNetCore.OData.Query.Expressions.QueryBinderContext)">
<summary>
Binds 'round' function to create a LINQ <see cref="T:System.Linq.Expressions.Expression"/>.
Expand Down
6 changes: 6 additions & 0 deletions src/Microsoft.AspNetCore.OData/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Microsoft.AspNetCore.OData.Query.AllowedFunctions.TotalSeconds = 1073741824 -> Microsoft.AspNetCore.OData.Query.AllowedFunctions
virtual Microsoft.AspNetCore.OData.Query.Expressions.QueryBinder.BindTotalSeconds(Microsoft.OData.UriParser.SingleValueFunctionCallNode node, Microsoft.AspNetCore.OData.Query.Expressions.QueryBinderContext context) -> System.Linq.Expressions.Expression
*REMOVED*Microsoft.AspNetCore.OData.Query.AllowedFunctions.AllDateTimeFunctions = Microsoft.AspNetCore.OData.Query.AllowedFunctions.Year | Microsoft.AspNetCore.OData.Query.AllowedFunctions.Date | Microsoft.AspNetCore.OData.Query.AllowedFunctions.Month | Microsoft.AspNetCore.OData.Query.AllowedFunctions.Time | Microsoft.AspNetCore.OData.Query.AllowedFunctions.Day | Microsoft.AspNetCore.OData.Query.AllowedFunctions.Hour | Microsoft.AspNetCore.OData.Query.AllowedFunctions.Minute | Microsoft.AspNetCore.OData.Query.AllowedFunctions.Second | Microsoft.AspNetCore.OData.Query.AllowedFunctions.FractionalSeconds -> Microsoft.AspNetCore.OData.Query.AllowedFunctions
Microsoft.AspNetCore.OData.Query.AllowedFunctions.AllDateTimeFunctions = Microsoft.AspNetCore.OData.Query.AllowedFunctions.Year | Microsoft.AspNetCore.OData.Query.AllowedFunctions.Date | Microsoft.AspNetCore.OData.Query.AllowedFunctions.Month | Microsoft.AspNetCore.OData.Query.AllowedFunctions.Time | Microsoft.AspNetCore.OData.Query.AllowedFunctions.Day | Microsoft.AspNetCore.OData.Query.AllowedFunctions.Hour | Microsoft.AspNetCore.OData.Query.AllowedFunctions.Minute | Microsoft.AspNetCore.OData.Query.AllowedFunctions.Second | Microsoft.AspNetCore.OData.Query.AllowedFunctions.FractionalSeconds | Microsoft.AspNetCore.OData.Query.AllowedFunctions.TotalSeconds -> Microsoft.AspNetCore.OData.Query.AllowedFunctions
*REMOVED*Microsoft.AspNetCore.OData.Query.AllowedFunctions.AllFunctions = Microsoft.AspNetCore.OData.Query.AllowedFunctions.Cast | Microsoft.AspNetCore.OData.Query.AllowedFunctions.AllDateTimeFunctions | Microsoft.AspNetCore.OData.Query.AllowedFunctions.AllMathFunctions | Microsoft.AspNetCore.OData.Query.AllowedFunctions.IsOf | Microsoft.AspNetCore.OData.Query.AllowedFunctions.Any | Microsoft.AspNetCore.OData.Query.AllowedFunctions.All | Microsoft.AspNetCore.OData.Query.AllowedFunctions.AllStringFunctions -> Microsoft.AspNetCore.OData.Query.AllowedFunctions
Microsoft.AspNetCore.OData.Query.AllowedFunctions.AllFunctions = Microsoft.AspNetCore.OData.Query.AllowedFunctions.Cast | Microsoft.AspNetCore.OData.Query.AllowedFunctions.AllMathFunctions | Microsoft.AspNetCore.OData.Query.AllowedFunctions.IsOf | Microsoft.AspNetCore.OData.Query.AllowedFunctions.Any | Microsoft.AspNetCore.OData.Query.AllowedFunctions.All | Microsoft.AspNetCore.OData.Query.AllowedFunctions.AllStringFunctions | Microsoft.AspNetCore.OData.Query.AllowedFunctions.AllDateTimeFunctions -> Microsoft.AspNetCore.OData.Query.AllowedFunctions
7 changes: 6 additions & 1 deletion src/Microsoft.AspNetCore.OData/Query/AllowedFunctions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,11 @@ public enum AllowedFunctions
/// </summary>
MatchesPattern = 0x20000000,

/// <summary>
/// A value that corresponds to allowing 'TotalSeconds' function in $filter.
/// </summary>
TotalSeconds = 0x40000000,

/// <summary>
/// A value that corresponds to allowing all string related functions in $filter.
/// </summary>
Expand All @@ -163,7 +168,7 @@ public enum AllowedFunctions
/// <summary>
/// A value that corresponds to allowing all datetime related functions in $filter.
/// </summary>
AllDateTimeFunctions = Year | Month | Day | Hour | Minute | Second | FractionalSeconds | Date | Time,
AllDateTimeFunctions = Year | Month | Day | Hour | Minute | Second | FractionalSeconds | TotalSeconds | Date | Time,

/// <summary>
/// A value that corresponds to allowing math related functions in $filter.
Expand Down
4 changes: 4 additions & 0 deletions src/Microsoft.AspNetCore.OData/Query/ClrCanonicalFunctions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ internal class ClrCanonicalFunctions
internal const string SecondFunctionName = "second";
internal const string MillisecondFunctionName = "millisecond";
internal const string FractionalSecondsFunctionName = "fractionalseconds";
internal const string TotalSecondsFunctionName = "totalseconds";
internal const string RoundFunctionName = "round";
internal const string FloorFunctionName = "floor";
internal const string CeilingFunctionName = "ceiling";
Expand Down Expand Up @@ -150,6 +151,9 @@ internal class ClrCanonicalFunctions
new KeyValuePair<string, PropertyInfo>(MillisecondFunctionName, typeof(TimeSpan).GetProperty("Milliseconds")),
}.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);

// TimeSpan/Duration "total" property used by the OData V4 totalseconds() canonical function.
public static readonly PropertyInfo TimeSpanTotalSeconds = typeof(TimeSpan).GetProperty(nameof(TimeSpan.TotalSeconds));

// String Properties
public static readonly PropertyInfo Length = typeof(string).GetProperty("Length");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,10 @@ public virtual Expression BindSingleValueFunctionCallNode(SingleValueFunctionCal
result = BindFractionalSeconds(node, context);
break;

case ClrCanonicalFunctions.TotalSecondsFunctionName:
result = BindTotalSeconds(node, context);
break;

case ClrCanonicalFunctions.RoundFunctionName:
result = BindRound(node, context);
break;
Expand Down Expand Up @@ -522,6 +526,31 @@ protected virtual Expression BindFractionalSeconds(SingleValueFunctionCallNode n
return ExpressionBinderHelper.CreateFunctionCallWithNullPropagation(fractionalSeconds, arguments, context.QuerySettings);
}

/// <summary>
/// Binds the OData V4 canonical 'totalseconds' function to a LINQ <see cref="Expression"/>.
/// Per the OData V4 spec, totalseconds(Edm.Duration) returns an Edm.Decimal with the duration
/// expressed as a fractional number of seconds. The CLR mapping is <see cref="TimeSpan.TotalSeconds"/>.
/// </summary>
/// <param name="node">The query node to bind.</param>
/// <param name="context">The query binder context.</param>
/// <returns>The LINQ <see cref="Expression"/> created.</returns>
protected virtual Expression BindTotalSeconds(SingleValueFunctionCallNode node, QueryBinderContext context)
{
CheckArgumentNull(node, context, ClrCanonicalFunctions.TotalSecondsFunctionName);

Expression[] arguments = BindArguments(node.Parameters, context);
Contract.Assert(arguments.Length == 1 && ExpressionBinderHelper.IsTimeSpan(arguments[0].Type));

Expression parameter = arguments[0];

// TimeSpan.TotalSeconds is a double; the spec mandates Edm.Decimal.
Expression totalSeconds = ExpressionBinderHelper.MakePropertyAccess(
ClrCanonicalFunctions.TimeSpanTotalSeconds, parameter, context.QuerySettings);
Expression decimalTotalSeconds = Expression.Convert(totalSeconds, typeof(decimal));

return ExpressionBinderHelper.CreateFunctionCallWithNullPropagation(decimalTotalSeconds, arguments, context.QuerySettings);
}

/// <summary>
/// Binds 'round' function to create a LINQ <see cref="Expression"/>.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -831,6 +831,9 @@ private static AllowedFunctions ToODataFunction(string functionName)
case ClrCanonicalFunctions.FractionalSecondsFunctionName:
result = AllowedFunctions.FractionalSeconds;
break;
case ClrCanonicalFunctions.TotalSecondsFunctionName:
result = AllowedFunctions.TotalSeconds;
break;
default:
// should never be here
Contract.Assert(true, "ToODataFunction should never be here.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,9 @@ private static AllowedFunctions ToODataFunction(string functionName)
case ClrCanonicalFunctions.FractionalSecondsFunctionName:
result = AllowedFunctions.FractionalSeconds;
break;
case ClrCanonicalFunctions.TotalSecondsFunctionName:
result = AllowedFunctions.TotalSeconds;
break;
default:
// Originally, we think it should never be here.
// But, ODL supports the customized function, if we are here, it might mean it's a customized function.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//-----------------------------------------------------------------------------
// <copyright file="DurationFunctionsController.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.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.OData.Query;
using Microsoft.AspNetCore.OData.Routing.Controllers;

namespace Microsoft.AspNetCore.OData.E2E.Tests.DurationFunctions;

public class DurationCustomersController : ODataController
{
[EnableQuery]
public ActionResult<IEnumerable<DurationCustomer>> Get()
{
return Ok(DurationFunctionsDataSource.Customers);
}
}

/// <summary>
/// Every function is allowed but 'totalseconds'.
/// </summary>
public class RestrictedCustomersController : ODataController
{
[EnableQuery(AllowedFunctions = AllowedFunctions.AllFunctions & ~AllowedFunctions.TotalSeconds)]
public ActionResult<IEnumerable<DurationCustomer>> Get()
{
return Ok(DurationFunctionsDataSource.Customers);
}
}

/// <summary>
/// Only the date/time functions are allowed, which is where 'totalseconds' belongs.
/// </summary>
public class AllowedCustomersController : ODataController
{
[EnableQuery(AllowedFunctions = AllowedFunctions.AllDateTimeFunctions)]
public ActionResult<IEnumerable<DurationCustomer>> Get()
{
return Ok(DurationFunctionsDataSource.Customers);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//-----------------------------------------------------------------------------
// <copyright file="DurationFunctionsDataModel.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;

namespace Microsoft.AspNetCore.OData.E2E.Tests.DurationFunctions;

public class DurationCustomer
{
public int Id { get; set; }

public string Name { get; set; }

/// <summary>
/// Non-nullable Edm.Duration.
/// </summary>
public TimeSpan Duration { get; set; }

/// <summary>
/// Nullable Edm.Duration. Exercises the null propagation branch of the 'totalseconds' binding.
/// </summary>
public TimeSpan? NullableDuration { get; set; }

public DateTimeOffset Start { get; set; }

/// <summary>
/// 'End sub Start' is typed as Edm.Duration by the URI parser.
/// </summary>
public DateTimeOffset End { get; set; }

/// <summary>
/// 'NullableEnd sub Start' produces a lifted subtraction, so the null check happens
/// inside the argument of 'totalseconds'.
/// </summary>
public DateTimeOffset? NullableEnd { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
//-----------------------------------------------------------------------------
// <copyright file="DurationFunctionsDataSource.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;

namespace Microsoft.AspNetCore.OData.E2E.Tests.DurationFunctions;

public class DurationFunctionsDataSource
{
private static IList<DurationCustomer> customers;

// totalseconds(Duration) => 1.5, 30, 3600, 9000, 0
// totalseconds(NullableDuration) => 30, null, 1.5, null, 9000
// totalseconds(End sub Start) => 30, 3600, 7200, 86400, -60
// totalseconds(NullableEnd sub Start) => 30, null, 7200, null, -60
static DurationFunctionsDataSource()
{
customers = new List<DurationCustomer>
{
new DurationCustomer
{
Id = 1,
Name = "Alpha",
Duration = TimeSpan.FromSeconds(1.5),
NullableDuration = new TimeSpan(0, 0, 30),
Start = new DateTimeOffset(2023, 1, 1, 0, 0, 0, TimeSpan.Zero),
End = new DateTimeOffset(2023, 1, 1, 0, 0, 30, TimeSpan.Zero),
NullableEnd = new DateTimeOffset(2023, 1, 1, 0, 0, 30, TimeSpan.Zero)
},
new DurationCustomer
{
Id = 2,
Name = "Beta",
Duration = new TimeSpan(0, 0, 30),
NullableDuration = null,
Start = new DateTimeOffset(2023, 1, 1, 0, 0, 0, TimeSpan.Zero),
End = new DateTimeOffset(2023, 1, 1, 1, 0, 0, TimeSpan.Zero),
NullableEnd = null
},
new DurationCustomer
{
Id = 3,
Name = "Gamma",
Duration = new TimeSpan(1, 0, 0),
NullableDuration = TimeSpan.FromSeconds(1.5),
Start = new DateTimeOffset(2023, 1, 1, 0, 0, 0, TimeSpan.Zero),
End = new DateTimeOffset(2023, 1, 1, 2, 0, 0, TimeSpan.Zero),
NullableEnd = new DateTimeOffset(2023, 1, 1, 2, 0, 0, TimeSpan.Zero)
},
new DurationCustomer
{
Id = 4,
Name = "Delta",
Duration = new TimeSpan(2, 30, 0),
NullableDuration = null,
Start = new DateTimeOffset(2023, 1, 1, 0, 0, 0, TimeSpan.Zero),
End = new DateTimeOffset(2023, 1, 2, 0, 0, 0, TimeSpan.Zero),
NullableEnd = null
},
new DurationCustomer
{
Id = 5,
Name = "Epsilon",
Duration = TimeSpan.Zero,
NullableDuration = new TimeSpan(2, 30, 0),
// End is before Start on purpose: totalseconds(End sub Start) is negative.
Start = new DateTimeOffset(2023, 1, 1, 12, 0, 0, TimeSpan.Zero),
End = new DateTimeOffset(2023, 1, 1, 11, 59, 0, TimeSpan.Zero),
NullableEnd = new DateTimeOffset(2023, 1, 1, 11, 59, 0, TimeSpan.Zero)
}
};
}

public static IList<DurationCustomer> Customers => customers;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//-----------------------------------------------------------------------------
// <copyright file="DurationFunctionsEdmModel.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 Microsoft.OData.Edm;
using Microsoft.OData.ModelBuilder;

namespace Microsoft.AspNetCore.OData.E2E.Tests.DurationFunctions;

public class DurationFunctionsEdmModel
{
public static IEdmModel GetEdmModel()
{
var builder = new ODataConventionModelBuilder();
builder.EntitySet<DurationCustomer>("DurationCustomers");

// Same CLR type served by controllers with different AllowedFunctions settings.
builder.EntitySet<DurationCustomer>("RestrictedCustomers");
builder.EntitySet<DurationCustomer>("AllowedCustomers");

return builder.GetEdmModel();
}
}
Loading