Skip to content

Commit

Permalink
Merge pull request #112 from turenkomv/feature/PREF-2510
Browse files Browse the repository at this point in the history
[PREF-2510] Option for correct datetime format (with offset)
  • Loading branch information
ZEXSM authored Apr 12, 2023
2 parents 339874f + 986f19e commit 7d9f7fb
Show file tree
Hide file tree
Showing 5 changed files with 102 additions and 37 deletions.
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -421,4 +421,11 @@ var uri = builder
&& o.In(s.IdType, new[] { 123, 512 }))
.ToUri()
```
> http://mock/odata/ODataType?$filter=IdType in (123,512)
> 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)
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
{
Expand All @@ -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())
{
Expand Down Expand Up @@ -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)
Expand All @@ -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())
Expand All @@ -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())
{
Expand All @@ -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())
{
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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);
}
}

Expand All @@ -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);
Expand Down
29 changes: 20 additions & 9 deletions src/OData.QueryBuilder/Extensions/StringExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using OData.QueryBuilder.Conventions.Constants;
using OData.QueryBuilder.Options;
using System;
using System.Collections;
using System.Collections.Generic;
Expand Down Expand Up @@ -40,24 +41,34 @@ public static IEnumerable<string> ReplaceWithStringBuilder(this ICollection<stri
return replaceValues;
}

public static string ToQuery(this object @object) => @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;
Expand All @@ -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++;
}

Expand Down
4 changes: 3 additions & 1 deletion src/OData.QueryBuilder/Options/ODataQueryBuilderOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
57 changes: 43 additions & 14 deletions test/OData.QueryBuilder.Test/ODataQueryCollectionTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand All @@ -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()
{
Expand All @@ -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()
{
Expand Down Expand Up @@ -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<ODataTypeEntity>(s => s.ODataType)
.ByList()
.Filter((s,f,_) => ODataProperty.FromPath<int>(propertyName) >= 3)
.Filter((s, f, _) => ODataProperty.FromPath<int>(propertyName) >= 3)
.ToUri();

uri.Should().Be("http://mock/odata/ODataType?$filter=ODataKind/ODataCode/IdCode ge 3");
Expand All @@ -376,20 +376,20 @@ public void ODataQueryBuilderList_Filter_Simple_Variable_DynamicProperty_WrongTy
.ByList()
.Filter((s, f, _) => ODataProperty.FromPath<string>(propertyName) == "test")
.ToUri()).Should().Throw<ArgumentException>();
}
}

[Fact(DisplayName = "Filter const dynamic property int=> Success")]
public void ODataQueryBuilderList_Filter_Simple_Const_DynamicProperty_Success()
{
var uri = _odataQueryBuilderDefault
.For<ODataTypeEntity>(s => s.ODataType)
.ByList()
.Filter((s,f,_) => ODataProperty.FromPath<int>("ODataKind.ODataCode.IdCode") >= 3)
.Filter((s, f, _) => ODataProperty.FromPath<int>("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()
{
Expand Down Expand Up @@ -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<ODataInfoContainer>(
_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<ODataTypeEntity>(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");
}
}
}

0 comments on commit 7d9f7fb

Please sign in to comment.