diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 508a802..854801c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,9 +27,9 @@ jobs: - uses: actions/setup-dotnet@v5.2.0 with: dotnet-version: | - 6.0.x 8.0.x 9.0.x + 10.0.x - run: dotnet restore - run: dotnet build --configuration Release --no-restore /warnAsError /nologo /clp:NoSummary diff --git a/Directory.Build.props b/Directory.Build.props index b9b1049..68b243d 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,7 +1,7 @@ - netstandard2.0;net8.0 + netstandard2.0;net8.0;net10.0 latest enable true diff --git a/Directory.Packages.props b/Directory.Packages.props index 983db85..d6e9ce0 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -2,40 +2,38 @@ - - - + + + - - - - - - - - - - - + + + + + + + + + + + - - - + + + - - - + + + - - - - - + + + - - + + - + diff --git a/README.md b/README.md index eb781bf..1e41039 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ and [IServiceCollection](https://docs.microsoft.com/en-us/dotnet/api/microsoft.e For use outside of ASP.NET Core, see the [example in tests](tests/VMelnalksnis.PaperlessDotNet.Tests.Integration/MinimalExampleTests.cs). -1. Add configuration (see [options](source/VMelnalksnis.PaperlessDotNet.DependencyInjection/PaperlessOptions.cs)) +1. Add configuration (see [options](source/VMelnalksnis.PaperlessDotNet/PaperlessOptions.cs)) ```yaml "Paperless": { "BaseAddress": "", @@ -38,7 +38,7 @@ For use outside of ASP.NET Core, see the ## Filtering Some objects, such as documents, support filtering on various fields. The filter format slightly differs from the object itself, and can be seen in a respective `Filter` object; -for example [DocumentFilter](source/VMelnalksnis.PaperlessDotNet/Filters/DocumentFilter.cs) for documents. +for example [DocumentFilter](source/VMelnalksnis.PaperlessDotNet/Documents/DocumentFilter.cs) for documents. Filters can be written inline as expressions: ```csharp var filteredDocuments = await Client.Documents.Get( diff --git a/global.json b/global.json index c0ecc45..cf4ab84 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "9.0.203", + "version": "10.0.201", "rollForward": "latestFeature", "allowPrerelease": false } diff --git a/source/VMelnalksnis.PaperlessDotNet.DependencyInjection/PaperlessOptionsValidator.cs b/source/VMelnalksnis.PaperlessDotNet.DependencyInjection/PaperlessOptionsValidator.cs new file mode 100644 index 0000000..46dfd69 --- /dev/null +++ b/source/VMelnalksnis.PaperlessDotNet.DependencyInjection/PaperlessOptionsValidator.cs @@ -0,0 +1,11 @@ +// Copyright 2022 Valters Melnalksnis +// Licensed under the Apache License 2.0. +// See LICENSE file in the project root for full license information. + +using Microsoft.Extensions.Options; + +namespace VMelnalksnis.PaperlessDotNet.DependencyInjection; + +/// +[OptionsValidator] +public sealed partial class PaperlessOptionsValidator : IValidateOptions; diff --git a/source/VMelnalksnis.PaperlessDotNet.DependencyInjection/ServiceCollectionExtensions.cs b/source/VMelnalksnis.PaperlessDotNet.DependencyInjection/ServiceCollectionExtensions.cs index d45f464..22ab580 100644 --- a/source/VMelnalksnis.PaperlessDotNet.DependencyInjection/ServiceCollectionExtensions.cs +++ b/source/VMelnalksnis.PaperlessDotNet.DependencyInjection/ServiceCollectionExtensions.cs @@ -3,7 +3,6 @@ // See LICENSE file in the project root for full license information. using System; -using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Net.Http; using System.Net.Http.Headers; @@ -39,19 +38,14 @@ static ServiceCollectionExtensions() /// The service collection in which to register the services. /// A delegate that is used to configure . /// The for the used by . -#if NETSTANDARD2_0 - [SuppressMessage("Trimming", "IL2026", Justification = $"{nameof(PaperlessOptions)} contains only system types.")] -#else - [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026", Justification = $"{nameof(PaperlessOptions)} contains only system types.")] -#endif public static IHttpClientBuilder AddPaperlessDotNet( this IServiceCollection serviceCollection, Action? config = null) { serviceCollection + .AddSingleton, PaperlessOptionsValidator>() .AddOptions() - .BindConfiguration(PaperlessOptions.Name) - .ValidateDataAnnotations(); + .BindConfiguration(PaperlessOptions.Name); return serviceCollection.AddClient(config); } @@ -61,20 +55,15 @@ public static IHttpClientBuilder AddPaperlessDotNet( /// The configuration to which to bind options models. /// A delegate that is used to configure . /// The for the used by . -#if NETSTANDARD2_0 - [SuppressMessage("Trimming", "IL2026", Justification = $"{nameof(PaperlessOptions)} contains only system types.")] -#else - [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026", Justification = $"{nameof(PaperlessOptions)} contains only system types.")] -#endif public static IHttpClientBuilder AddPaperlessDotNet( this IServiceCollection serviceCollection, IConfiguration configuration, Action? config = null) { serviceCollection + .AddSingleton, PaperlessOptionsValidator>() .AddOptions() - .Bind(configuration.GetSection(PaperlessOptions.Name)) - .ValidateDataAnnotations(); + .Bind(configuration.GetSection(PaperlessOptions.Name)); return serviceCollection.AddClient(config); } diff --git a/source/VMelnalksnis.PaperlessDotNet/Filters/ExpressionExtensions.cs b/source/VMelnalksnis.PaperlessDotNet/Filters/ExpressionExtensions.cs index b57d24e..af629e1 100644 --- a/source/VMelnalksnis.PaperlessDotNet/Filters/ExpressionExtensions.cs +++ b/source/VMelnalksnis.PaperlessDotNet/Filters/ExpressionExtensions.cs @@ -12,6 +12,8 @@ using System.Text.Encodings.Web; using System.Text.Json.Serialization; +using NodaTime; + using VMelnalksnis.PaperlessDotNet.Documents; using VMelnalksnis.PaperlessDotNet.DocumentTypes; @@ -94,40 +96,43 @@ private static KeyValuePair ToKeyValuePair(this Expression expre { if (expression is BinaryExpression binaryExpression) { - var suffix = binaryExpression.GetSuffix(); - if (binaryExpression.Left is MemberExpression memberExpression) { - var value = binaryExpression.Right.Evaluate(); - if (value is bool boolValue) + var memberName = memberExpression.GetFilterMemberName(); + + var suffix = binaryExpression.GetSuffix(); + if (suffix is not null) { - value = binaryExpression.NodeType is NotEqual - ? !boolValue - : boolValue; + memberName += $"__{suffix}"; } - else if (value is null) + + if (binaryExpression.Right.Type == typeof(bool)) { - value = binaryExpression.NodeType is Equal; + var booleanValue = binaryExpression.Right.Evaluate(); + booleanValue = binaryExpression.NodeType is NotEqual + ? !booleanValue + : booleanValue; + + return new(memberName, booleanValue ? "true" : "false"); } - var memberName = memberExpression.GetFilterMemberName(); + if (binaryExpression.Right.Type == typeof(DateTime)) + { + var dateValue = binaryExpression.Right.Evaluate(); + var dateString = memberName.Contains("__date") + ? dateValue.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture) + : dateValue.ToString("yyyy-MM-ddTHH:mm:ss", CultureInfo.InvariantCulture); - if (suffix is not null) + return new(memberName, dateString); + } + + var value = binaryExpression.Right.Evaluate(); + if (value is null) { - memberName += $"__{suffix}"; + return new(memberName, binaryExpression.NodeType is Equal ? "true" : "false"); } - return new( - memberName, - value switch - { - DateTime dateTime when memberName.Contains("__date") => dateTime.ToString( - "yyyy-MM-dd", - CultureInfo.InvariantCulture), - DateTime dateTime => dateTime.ToString("yyyy-MM-ddTHH:mm:ss", CultureInfo.InvariantCulture), - bool boolean => boolean.ToString().ToLowerInvariant(), - _ => value.ToString() ?? string.Empty, - }); + return new(memberName, value.ToString() ?? string.Empty); } } @@ -144,7 +149,8 @@ DateTime dateTime when memberName.Contains("__date") => dateTime.ToString( _ => throw new NotImplementedException(), }; - var value = valueExpression.Evaluate() ?? throw new NotSupportedException("Method calls with null arguments are not supported"); + var value = valueExpression.Evaluate() ?? + throw new NotSupportedException("Method calls with null arguments are not supported"); if (value is IEnumerable values) { suffix = "in"; @@ -158,7 +164,8 @@ DateTime dateTime when memberName.Contains("__date") => dateTime.ToString( } else { - var value = methodCallExpression.Arguments[0].Evaluate() ?? throw new InvalidOperationException("Extension method calls on null instances are not supported"); + var value = methodCallExpression.Arguments[0].Evaluate() ?? + throw new InvalidOperationException("Extension method calls on null instances are not supported"); if (value is IEnumerable values) { suffix = "in"; @@ -232,6 +239,116 @@ private static string GetOrderMemberName(this MemberExpression expression) private static object? Evaluate(this Expression expression) { - return Expression.Lambda(expression).Compile().DynamicInvoke(); + if (expression.Type == typeof(string)) + { + return expression.Evaluate(); + } + + if (expression.Type == typeof(Uri)) + { + return expression.Evaluate(); + } + + if (expression.Type == typeof(int)) + { + return expression.Evaluate(); + } + + if (expression.Type == typeof(int?)) + { + return expression.Evaluate(); + } + + if (expression.Type == typeof(uint)) + { + return expression.Evaluate(); + } + + if (expression.Type == typeof(uint?)) + { + return expression.Evaluate(); + } + + if (expression.Type == typeof(float)) + { + return expression.Evaluate(); + } + + if (expression.Type == typeof(float?)) + { + return expression.Evaluate(); + } + + if (expression.Type == typeof(double)) + { + return expression.Evaluate(); + } + + if (expression.Type == typeof(double?)) + { + return expression.Evaluate(); + } + + if (expression.Type == typeof(decimal)) + { + return expression.Evaluate(); + } + + if (expression.Type == typeof(decimal?)) + { + return expression.Evaluate(); + } + + if (expression.Type == typeof(bool)) + { + return expression.Evaluate(); + } + + if (expression.Type == typeof(bool?)) + { + return expression.Evaluate(); + } + + if (expression.Type == typeof(DateTime)) + { + return expression.Evaluate(); + } + + if (expression.Type == typeof(DateTime?)) + { + return expression.Evaluate(); + } + + if (expression.Type == typeof(OffsetDate)) + { + return expression.Evaluate(); + } + + if (expression.Type == typeof(OffsetDate?)) + { + return expression.Evaluate(); + } + + if (expression.Type == typeof(LocalDate)) + { + return expression.Evaluate(); + } + + if (expression.Type == typeof(LocalDate?)) + { + return expression.Evaluate(); + } + + if (typeof(IEnumerable).IsAssignableFrom(expression.Type)) + { + return expression.Evaluate>(); + } + + throw new ArgumentOutOfRangeException(nameof(expression.Type), expression.Type, "Unsupported expression type"); + } + + private static TValue Evaluate(this Expression expression) + { + return Expression.Lambda>(expression).Compile().Invoke(); } } diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props index f4b9d97..2a20908 100644 --- a/tests/Directory.Build.props +++ b/tests/Directory.Build.props @@ -3,7 +3,7 @@ - net6.0;net8.0;net9.0 + net8.0;net9.0;net10.0 false false false @@ -23,8 +23,6 @@ - - diff --git a/tests/VMelnalksnis.PaperlessDotNet.Tests.Integration/VMelnalksnis.PaperlessDotNet.Tests.Integration.csproj b/tests/VMelnalksnis.PaperlessDotNet.Tests.Integration/VMelnalksnis.PaperlessDotNet.Tests.Integration.csproj index b9daf2a..e8543f2 100644 --- a/tests/VMelnalksnis.PaperlessDotNet.Tests.Integration/VMelnalksnis.PaperlessDotNet.Tests.Integration.csproj +++ b/tests/VMelnalksnis.PaperlessDotNet.Tests.Integration/VMelnalksnis.PaperlessDotNet.Tests.Integration.csproj @@ -18,11 +18,14 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + + + + diff --git a/tests/VMelnalksnis.PaperlessDotNet.Tests/Filters/FilterExpressionTestCases.cs b/tests/VMelnalksnis.PaperlessDotNet.Tests/Filters/FilterExpressionTestCases.cs index 1d2f960..6b7b5f1 100644 --- a/tests/VMelnalksnis.PaperlessDotNet.Tests/Filters/FilterExpressionTestCases.cs +++ b/tests/VMelnalksnis.PaperlessDotNet.Tests/Filters/FilterExpressionTestCases.cs @@ -38,9 +38,9 @@ public FilterExpressionTestCases() Add(filter => filter.Correspondent!.Name.Contains("foo"), null, "correspondent__name__icontains=foo"); Add(filter => filter.Correspondent!.Name == "foo", null, "correspondent__name__iexact=foo"); - Add(filter => _ids.Contains(filter.Correspondent!.Id), null, "correspondent__id__in=5,23"); + Add(filter => _ids.AsEnumerable().Contains(filter.Correspondent!.Id), null, "correspondent__id__in=5,23"); Add(filter => new List { 1 }.Contains(filter.Correspondent!.Id), null, "correspondent__id__in=1"); - Add(filter => new[] { 1, 2 }.Contains(filter.Correspondent!.Id), null, "correspondent__id__in=1,2"); + Add(filter => new[] { 1, 2 }.AsEnumerable().Contains(filter.Correspondent!.Id), null, "correspondent__id__in=1,2"); Add(filter => filter.Added.Date <= date, null, "added__date__lte=2025-04-23"); Add(filter => filter.Added <= date, null, "added__lte=2025-04-23T12%3A23%3A45");