diff --git a/README.md b/README.md index a42fc827..1a1e71df 100644 --- a/README.md +++ b/README.md @@ -421,4 +421,11 @@ var uri = builder && o.In(s.IdType, new[] { 123, 512 })) .ToUri() ``` -> http://mock/odata/ODataType?$filter=IdType in (123,512) \ No newline at end of file +> http://mock/odata/ODataType?$filter=IdType in (123,512) + +## ODataQueryBuilderOptions + +### UseCorrectDateTimeFormat +> You should always manually set this option to `true` (__default__: `false`) +* `true` DateTime format `$"{dateTime:yyyy-MM-ddTHH:mm:sszzz}"` (with UTC offset) +* `false` DateTime format `$"{dateTime:s}Z"` (without UTC offset) \ No newline at end of file diff --git a/src/OData.QueryBuilder/Expressions/Visitors/ODataOptionFilterExpressionVisitor.cs b/src/OData.QueryBuilder/Expressions/Visitors/ODataOptionFilterExpressionVisitor.cs index b3677ca2..eb636bae 100644 --- a/src/OData.QueryBuilder/Expressions/Visitors/ODataOptionFilterExpressionVisitor.cs +++ b/src/OData.QueryBuilder/Expressions/Visitors/ODataOptionFilterExpressionVisitor.cs @@ -5,7 +5,6 @@ using OData.QueryBuilder.Options; using System; using System.Collections.Generic; -using System.Linq; using System.Linq.Expressions; namespace OData.QueryBuilder.Expressions.Visitors @@ -49,10 +48,12 @@ protected override string VisitBinaryExpression(LambdaExpression topExpression, } protected override string VisitMemberExpression(LambdaExpression topExpression, MemberExpression memberExpression) => - IsMemberExpressionBelongsResource(memberExpression) ? base.VisitMemberExpression(topExpression, memberExpression) : _valueExpression.GetValue(memberExpression).ToQuery(); + IsMemberExpressionBelongsResource(memberExpression) + ? base.VisitMemberExpression(topExpression, memberExpression) + : _valueExpression.GetValue(memberExpression).ToQuery(_odataQueryBuilderOptions); protected override string VisitConstantExpression(LambdaExpression topExpression, ConstantExpression constantExpression) => - constantExpression.Value.ToQuery(); + constantExpression.Value.ToQuery(_odataQueryBuilderOptions); protected override string VisitMethodCallExpression(LambdaExpression topExpression, MethodCallExpression methodCallExpression) { @@ -64,7 +65,9 @@ protected override string VisitMethodCallExpression(LambdaExpression topExpressi { case nameof(IODataOperator.In): var in0 = VisitExpression(topExpression, methodCallExpression.Arguments[0]); - var in1 = _valueExpression.GetValue(methodCallExpression.Arguments[1]).ToQuery(); + var in1 = _valueExpression + .GetValue(methodCallExpression.Arguments[1]) + .ToQuery(_odataQueryBuilderOptions); if (in1.IsNullOrQuotes()) { @@ -103,8 +106,8 @@ protected override string VisitMethodCallExpression(LambdaExpression topExpressi return $"{any0}/{nameof(IODataOperator.Any).ToLowerInvariant()}({any1})"; } - } - + } + if (declaringType.IsAssignableFrom(typeof(IODataFunction))) { switch (methodCallExpression.Method.Name) @@ -114,7 +117,9 @@ protected override string VisitMethodCallExpression(LambdaExpression topExpressi return $"{nameof(IODataFunction.Date).ToLowerInvariant()}({date0})"; case nameof(IODataFunction.SubstringOf): - var substringOf0 = _valueExpression.GetValue(methodCallExpression.Arguments[0]).ToQuery(); + var substringOf0 = _valueExpression + .GetValue(methodCallExpression.Arguments[0]) + .ToQuery(_odataQueryBuilderOptions); var substringOf1 = VisitExpression(topExpression, methodCallExpression.Arguments[1]); if (substringOf0.IsNullOrQuotes()) @@ -131,7 +136,9 @@ protected override string VisitMethodCallExpression(LambdaExpression topExpressi $"{nameof(IODataFunction.SubstringOf).ToLowerInvariant()}({substringOf0},{substringOf1})"; case nameof(IODataFunction.Contains): var contains0 = VisitExpression(topExpression, methodCallExpression.Arguments[0]); - var contains1 = _valueExpression.GetValue(methodCallExpression.Arguments[1]).ToQuery(); + var contains1 = _valueExpression + .GetValue(methodCallExpression.Arguments[1]) + .ToQuery(_odataQueryBuilderOptions); if (contains1.IsNullOrQuotes()) { @@ -146,7 +153,9 @@ protected override string VisitMethodCallExpression(LambdaExpression topExpressi return $"{nameof(IODataFunction.Contains).ToLowerInvariant()}({contains0},{contains1})"; case nameof(IODataFunction.StartsWith): var startsWith0 = VisitExpression(topExpression, methodCallExpression.Arguments[0]); - var startsWith1 = _valueExpression.GetValue(methodCallExpression.Arguments[1]).ToQuery(); + var startsWith1 = _valueExpression + .GetValue(methodCallExpression.Arguments[1]) + .ToQuery(_odataQueryBuilderOptions); if (startsWith1.IsNullOrQuotes()) { @@ -204,7 +213,9 @@ protected override string VisitMethodCallExpression(LambdaExpression topExpressi return dateTimeOffset.ToString( (string)_valueExpression.GetValue(methodCallExpression.Arguments[1])); case nameof(ICustomFunction.ReplaceCharacters): - var @symbol0 = _valueExpression.GetValue(methodCallExpression.Arguments[0]).ToQuery(); + var @symbol0 = _valueExpression + .GetValue(methodCallExpression.Arguments[0]) + .ToQuery(_odataQueryBuilderOptions); var @symbol1 = _valueExpression.GetValue(methodCallExpression.Arguments[1]); if (@symbol1 == default) @@ -236,7 +247,10 @@ protected override string VisitMethodCallExpression(LambdaExpression topExpressi switch (methodCallExpression.Method.Name) { case nameof(object.ToString): - return _valueExpression.GetValue(methodCallExpression.Object).ToString().ToQuery(); + return _valueExpression + .GetValue(methodCallExpression.Object) + .ToString() + .ToQuery(_odataQueryBuilderOptions); } } @@ -254,7 +268,9 @@ protected override string VisitNewExpression(LambdaExpression topExpression, New arguments[i] = _valueExpression.GetValue(newExpression.Arguments[i]); } - return (arguments.Length == 0 ? Activator.CreateInstance(newExpression.Type) : newExpression.Constructor.Invoke(arguments)).ToQuery(); + return (arguments.Length == 0 + ? Activator.CreateInstance(newExpression.Type) + : newExpression.Constructor.Invoke(arguments)).ToQuery(_odataQueryBuilderOptions); } return base.VisitNewExpression(topExpression, newExpression); diff --git a/src/OData.QueryBuilder/Extensions/StringExtensions.cs b/src/OData.QueryBuilder/Extensions/StringExtensions.cs index 70c45c6a..5cf1c35c 100644 --- a/src/OData.QueryBuilder/Extensions/StringExtensions.cs +++ b/src/OData.QueryBuilder/Extensions/StringExtensions.cs @@ -1,4 +1,5 @@ using OData.QueryBuilder.Conventions.Constants; +using OData.QueryBuilder.Options; using System; using System.Collections; using System.Collections.Generic; @@ -40,24 +41,34 @@ public static IEnumerable ReplaceWithStringBuilder(this ICollection @object switch + public static string ToQuery(this object @object, ODataQueryBuilderOptions options) => @object switch { null => Null, bool @bool => @bool ? "true" : "false", - DateTime dateTime => $"{dateTime:s}Z", - DateTimeOffset dateTimeOffset => $"{dateTimeOffset:s}Z", + DateTime dateTime => options switch + { + { UseCorrectDateTimeFormat: true } => $"{dateTime:yyyy-MM-ddTHH:mm:sszzz}", + _ => $"{dateTime:s}Z" + }, + DateTimeOffset dateTimeOffset => options switch + { + { UseCorrectDateTimeFormat: true } => $"{dateTimeOffset:yyyy-MM-ddTHH:mm:sszzz}", + _ => $"{dateTimeOffset:s}Z" + }, string @string => $"'{@string}'", - ICollection collection => collection.CollectionToQuery(), - IEnumerable enumerable => enumerable.EnumerableToQuery(), + ICollection collection => collection.CollectionToQuery(options), + IEnumerable enumerable => enumerable.EnumerableToQuery(options, initCount: 0), Guid @guid => $"{@guid}", decimal @decimal => Convert.ToString(@decimal, CultureInfo.InvariantCulture), _ => @object.GetType().IsPrimitive ? Convert.ToString(@object, CultureInfo.InvariantCulture) : $"'{@object}'", }; - private static string CollectionToQuery(this ICollection collection) => - collection.EnumerableToQuery(collection.Count); + private static string CollectionToQuery( + this ICollection collection, ODataQueryBuilderOptions options) => + collection.EnumerableToQuery(options, initCount: collection.Count); - private static string EnumerableToQuery(this IEnumerable enumerable, int initCount = 0) + private static string EnumerableToQuery( + this IEnumerable enumerable, ODataQueryBuilderOptions options, int initCount) { var index = 0; var count = 0; @@ -78,7 +89,7 @@ private static string EnumerableToQuery(this IEnumerable enumerable, int initCou foreach (var item in enumerable) { - queries[index] = item.ToQuery(); + queries[index] = item.ToQuery(options); index++; } diff --git a/src/OData.QueryBuilder/Options/ODataQueryBuilderOptions.cs b/src/OData.QueryBuilder/Options/ODataQueryBuilderOptions.cs index a377de5a..bc1d87f9 100644 --- a/src/OData.QueryBuilder/Options/ODataQueryBuilderOptions.cs +++ b/src/OData.QueryBuilder/Options/ODataQueryBuilderOptions.cs @@ -4,6 +4,8 @@ public class ODataQueryBuilderOptions { public bool SuppressExceptionOfNullOrEmptyFunctionArgs { get; set; } = false; - public bool SuppressExceptionOfNullOrEmptyOperatorArgs { get; set; } = false; + public bool SuppressExceptionOfNullOrEmptyOperatorArgs { get; set; } = false; + + public bool UseCorrectDateTimeFormat { get; set; } = false; } } diff --git a/test/OData.QueryBuilder.Test/ODataQueryCollectionTest.cs b/test/OData.QueryBuilder.Test/ODataQueryCollectionTest.cs index b28b7891..8c869c40 100644 --- a/test/OData.QueryBuilder.Test/ODataQueryCollectionTest.cs +++ b/test/OData.QueryBuilder.Test/ODataQueryCollectionTest.cs @@ -45,8 +45,8 @@ public void ODataQueryBuilderList_Expand_DynamicProperty_Success() .ToUri(); uri.Should().Be("http://mock/odata/ODataType?$expand=ODataKind"); - } - + } + [Fact(DisplayName = "Select simple => Success")] public void ODataQueryBuilderList_Select_Simple_Success() { @@ -69,8 +69,8 @@ public void ODataQueryBuilderList_Select_DynamicProperty_Success() .ToUri(); uri.Should().Be("http://mock/odata/ODataType?$select=IdType"); - } - + } + [Fact(DisplayName = "OrderBy simple => Success")] public void ODataQueryBuilderList_OrderBy_Simple_Success() { @@ -93,8 +93,8 @@ public void ODataQueryBuilderList_OrderBy_DynamicProperty_Success() .ToUri(); uri.Should().Be("http://mock/odata/ODataType?$orderby=IdType asc"); - } - + } + [Fact(DisplayName = "Filter orderBy multiple sort => Success")] public void ODataQueryBuilderList_Filter_OrderBy_Multiple_Sort_Success() { @@ -354,12 +354,12 @@ public void ODataQueryBuilderList_Filter_With_ReplaceCharacters_KeyValuePairs_Ar [Fact(DisplayName = "Filter variable dynamic property int=> Success")] public void ODataQueryBuilderList_Filter_Simple_Variable_DynamicProperty_Success() { - string propertyName = "ODataKind.ODataCode.IdCode"; - + string propertyName = "ODataKind.ODataCode.IdCode"; + var uri = _odataQueryBuilderDefault .For(s => s.ODataType) .ByList() - .Filter((s,f,_) => ODataProperty.FromPath(propertyName) >= 3) + .Filter((s, f, _) => ODataProperty.FromPath(propertyName) >= 3) .ToUri(); uri.Should().Be("http://mock/odata/ODataType?$filter=ODataKind/ODataCode/IdCode ge 3"); @@ -376,20 +376,20 @@ public void ODataQueryBuilderList_Filter_Simple_Variable_DynamicProperty_WrongTy .ByList() .Filter((s, f, _) => ODataProperty.FromPath(propertyName) == "test") .ToUri()).Should().Throw(); - } - + } + [Fact(DisplayName = "Filter const dynamic property int=> Success")] public void ODataQueryBuilderList_Filter_Simple_Const_DynamicProperty_Success() { var uri = _odataQueryBuilderDefault .For(s => s.ODataType) .ByList() - .Filter((s,f,_) => ODataProperty.FromPath("ODataKind.ODataCode.IdCode") >= 3) + .Filter((s, f, _) => ODataProperty.FromPath("ODataKind.ODataCode.IdCode") >= 3) .ToUri(); uri.Should().Be("http://mock/odata/ODataType?$filter=ODataKind/ODataCode/IdCode ge 3"); - } - + } + [Fact(DisplayName = "Filter simple const int=> Success")] public void ODataQueryBuilderList_Filter_Simple_Const_Int_Success() { @@ -1560,6 +1560,35 @@ public void ODataQueryBuilder_Function_Cast_Skip_Exception(string value) .ToUri(); uri.Should().Be("http://mock/odata/ODataType?$filter=contains(,'55')"); + } + + [Fact(DisplayName = "UseCorrectDateTimeFormat Convert => Success")] + public void ODataQueryBuilderList_UseCorrectDatetimeFormat_Convert_Success() + { + var builder = new ODataQueryBuilder( + _commonFixture.BaseUri, + new ODataQueryBuilderOptions { UseCorrectDateTimeFormat = true }); + + var dateTimeLocal = new DateTime( + year: 2023, month: 04, day: 07, hour: 12, minute: 30, second: 20, kind: DateTimeKind.Local); + var dateTimeUtc = new DateTime( + year: 2023, month: 04, day: 07, hour: 12, minute: 30, second: 20, kind: DateTimeKind.Utc); + var dateTimeOffset = new DateTimeOffset( + year: 2023, month: 04, day: 07, hour: 12, minute: 30, second: 20, offset: TimeSpan.FromHours(+7)); + + var uri = builder + .For(s => s.ODataType) + .ByList() + .Filter((o) => + o.DateTime == dateTimeLocal + && o.DateTime == dateTimeUtc + && o.DateTime == dateTimeOffset) + .ToUri(); + + uri.Should().Be($"http://mock/odata/ODataType?$filter=" + + $"DateTime eq 2023-04-07T12:30:20{DateTimeOffset.Now:zzz} and " + + $"DateTime eq 2023-04-07T12:30:20+00:00 and " + + $"DateTime eq 2023-04-07T12:30:20+07:00"); } } } \ No newline at end of file