Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project>

<PropertyGroup>
<TargetFrameworks>netstandard2.0;net8.0</TargetFrameworks>
<TargetFrameworks>netstandard2.0;net8.0;net10.0</TargetFrameworks>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
Expand Down
54 changes: 26 additions & 28 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -2,40 +2,38 @@
<ItemGroup>
<PackageVersion Include="Ardalis.SmartEnum" Version="2.1.0"/>
<PackageVersion Include="Ardalis.SmartEnum.SystemTextJson" Version="2.1.0"/>
<PackageVersion Include="coverlet.msbuild" Version="6.0.4"/>
<PackageVersion Include="DotNet.ReproducibleBuilds" Version="1.2.25"/>
<PackageVersion Include="DotNet.ReproducibleBuilds.Isolated" Version="1.2.25"/>
<PackageVersion Include="coverlet.msbuild" Version="8.0.1"/>
<PackageVersion Include="DotNet.ReproducibleBuilds" Version="2.0.2"/>
<PackageVersion Include="DotNet.ReproducibleBuilds.Isolated" Version="2.0.2"/>
<PackageVersion Include="FluentAssertions" Version="7.2.0"/>
<PackageVersion Include="IsExternalInit" Version="1.0.3"/>
<PackageVersion Include="Jetbrains.Annotations" Version="2025.2.2"/>
<PackageVersion Include="JunitXml.TestLogger" Version="6.1.0"/>
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="9.0.9"/>
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="9.0.9"/>
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.9"/>
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.9"/>
<PackageVersion Include="Microsoft.Extensions.Http" Version="9.0.9"/>
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="9.0.9"/>
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="9.0.9"/>
<PackageVersion Include="Microsoft.Extensions.Options.DataAnnotations" Version="9.0.9"/>
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.13.0"/>
<PackageVersion Include="Jetbrains.Annotations" Version="2025.2.4"/>
<PackageVersion Include="JunitXml.TestLogger" Version="8.0.0"/>
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.5"/>
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="10.0.5"/>
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.5"/>
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.5"/>
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.5"/>
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="10.0.5"/>
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.5"/>
<PackageVersion Include="Microsoft.Extensions.Options.DataAnnotations" Version="10.0.5"/>
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.3.0"/>
<PackageVersion Include="Newtonsoft.Json" Version="13.0.4"/>
<PackageVersion Include="NodaTime" Version="3.2.2"/>
<PackageVersion Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0"/>
<PackageVersion Include="NodaTime.Testing" Version="3.2.2"/>
<PackageVersion Include="NodaTime" Version="3.3.1"/>
<PackageVersion Include="NodaTime.Serialization.SystemTextJson" Version="1.3.1"/>
<PackageVersion Include="NodaTime.Testing" Version="3.3.1"/>
<PackageVersion Include="Nullable" Version="1.3.1"/>
<PackageVersion Include="NUnit" Version="3.14.0"/>
<PackageVersion Include="NUnit.Analyzers" Version="4.10.0"/>
<PackageVersion Include="NUnit3TestAdapter" Version="5.1.0"/>
<PackageVersion Include="NUnit" Version="4.5.1"/>
<PackageVersion Include="NUnit.Analyzers" Version="4.12.0"/>
<PackageVersion Include="NUnit3TestAdapter" Version="6.2.0"/>
<PackageVersion Include="System.ComponentModel.Annotations" Version="5.0.0"/>
<PackageVersion Include="System.Linq.Async" Version="6.0.3"/>
<PackageVersion Include="System.Net.Http" Version="4.3.4"/>
<PackageVersion Include="System.Net.Http.Json" Version="9.0.9"/>
<PackageVersion Include="System.Text.Json" Version="9.0.9"/>
<PackageVersion Include="System.Text.RegularExpressions" Version="4.3.1"/>
<PackageVersion Include="System.Linq.Async" Version="7.0.0"/>
<PackageVersion Include="System.Net.Http.Json" Version="10.0.5"/>
<PackageVersion Include="System.Text.Json" Version="10.0.5"/>
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556"/>
<PackageVersion Include="Testcontainers.Redis" Version="3.4.0"/>
<PackageVersion Include="VMelnalksnis.Testcontainers.Paperless" Version="0.2.0"/>
<PackageVersion Include="Testcontainers.Redis" Version="4.6.0"/>
<PackageVersion Include="VMelnalksnis.Testcontainers.Paperless" Version="0.3.0"/>
<PackageVersion Include="xunit" Version="2.9.3"/>
<PackageVersion Include="xunit.runner.visualstudio" Version="2.4.5"/>
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5"/>
</ItemGroup>
</Project>
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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": "",
Expand All @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion global.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"sdk": {
"version": "9.0.203",
"version": "10.0.201",
"rollForward": "latestFeature",
"allowPrerelease": false
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

/// <inheritdoc />
[OptionsValidator]
public sealed partial class PaperlessOptionsValidator : IValidateOptions<PaperlessOptions>;
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -39,19 +38,14 @@ static ServiceCollectionExtensions()
/// <param name="serviceCollection">The service collection in which to register the services.</param>
/// <param name="config">A delegate that is used to configure <see cref="PaperlessJsonSerializerOptions"/>.</param>
/// <returns>The <see cref="IHttpClientBuilder"/> for the <see cref="HttpClient"/> used by <see cref="IPaperlessClient"/>.</returns>
#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<PaperlessJsonSerializerOptions>? config = null)
{
serviceCollection
.AddSingleton<IValidateOptions<PaperlessOptions>, PaperlessOptionsValidator>()
.AddOptions<PaperlessOptions>()
.BindConfiguration(PaperlessOptions.Name)
.ValidateDataAnnotations();
.BindConfiguration(PaperlessOptions.Name);

return serviceCollection.AddClient(config);
}
Expand All @@ -61,20 +55,15 @@ public static IHttpClientBuilder AddPaperlessDotNet(
/// <param name="configuration">The configuration to which to bind options models.</param>
/// <param name="config">A delegate that is used to configure <see cref="PaperlessJsonSerializerOptions"/>.</param>
/// <returns>The <see cref="IHttpClientBuilder"/> for the <see cref="HttpClient"/> used by <see cref="IPaperlessClient"/>.</returns>
#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<PaperlessJsonSerializerOptions>? config = null)
{
serviceCollection
.AddSingleton<IValidateOptions<PaperlessOptions>, PaperlessOptionsValidator>()
.AddOptions<PaperlessOptions>()
.Bind(configuration.GetSection(PaperlessOptions.Name))
.ValidateDataAnnotations();
.Bind(configuration.GetSection(PaperlessOptions.Name));

return serviceCollection.AddClient(config);
}
Expand Down
169 changes: 143 additions & 26 deletions source/VMelnalksnis.PaperlessDotNet/Filters/ExpressionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
using System.Text.Encodings.Web;
using System.Text.Json.Serialization;

using NodaTime;

using VMelnalksnis.PaperlessDotNet.Documents;
using VMelnalksnis.PaperlessDotNet.DocumentTypes;

Expand Down Expand Up @@ -94,40 +96,43 @@ private static KeyValuePair<string, string> 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<bool>();
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<DateTime>();
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);
}
}

Expand All @@ -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<int> values)
{
suffix = "in";
Expand All @@ -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<int> values)
{
suffix = "in";
Expand Down Expand Up @@ -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<string?>();
}

if (expression.Type == typeof(Uri))
{
return expression.Evaluate<Uri?>();
}

if (expression.Type == typeof(int))
{
return expression.Evaluate<int>();
}

if (expression.Type == typeof(int?))
{
return expression.Evaluate<int?>();
}

if (expression.Type == typeof(uint))
{
return expression.Evaluate<uint>();
}

if (expression.Type == typeof(uint?))
{
return expression.Evaluate<uint?>();
}

if (expression.Type == typeof(float))
{
return expression.Evaluate<float>();
}

if (expression.Type == typeof(float?))
{
return expression.Evaluate<float?>();
}

if (expression.Type == typeof(double))
{
return expression.Evaluate<double>();
}

if (expression.Type == typeof(double?))
{
return expression.Evaluate<double?>();
}

if (expression.Type == typeof(decimal))
{
return expression.Evaluate<decimal>();
}

if (expression.Type == typeof(decimal?))
{
return expression.Evaluate<decimal?>();
}

if (expression.Type == typeof(bool))
{
return expression.Evaluate<bool>();
}

if (expression.Type == typeof(bool?))
{
return expression.Evaluate<bool?>();
}

if (expression.Type == typeof(DateTime))
{
return expression.Evaluate<DateTime>();
}

if (expression.Type == typeof(DateTime?))
{
return expression.Evaluate<DateTime?>();
}

if (expression.Type == typeof(OffsetDate))
{
return expression.Evaluate<OffsetDate>();
}

if (expression.Type == typeof(OffsetDate?))
{
return expression.Evaluate<OffsetDate?>();
}

if (expression.Type == typeof(LocalDate))
{
return expression.Evaluate<LocalDate>();
}

if (expression.Type == typeof(LocalDate?))
{
return expression.Evaluate<LocalDate?>();
}

if (typeof(IEnumerable<int>).IsAssignableFrom(expression.Type))
{
return expression.Evaluate<IEnumerable<int>>();
}

throw new ArgumentOutOfRangeException(nameof(expression.Type), expression.Type, "Unsupported expression type");
Comment thread
VMelnalksnis marked this conversation as resolved.
}

private static TValue Evaluate<TValue>(this Expression expression)
{
return Expression.Lambda<Func<TValue>>(expression).Compile().Invoke();
}
}
Loading
Loading