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
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using EfficientDynamoDb.Attributes;
using EfficientDynamoDb.IntegrationTests.DataPlane;
using EfficientDynamoDb.Operations.Shared;
using NUnit.Framework;
using Shouldly;

namespace EfficientDynamoDb.IntegrationTests.DataPlane.Query.FluentApi;

[DynamoDbTable(TestHelper.TestTableName)]
public record TestUserWithNullable
{
[DynamoDbProperty("pk", DynamoDbAttributeType.PartitionKey)]
public required string PartitionKey { get; init; }

[DynamoDbProperty("sk", DynamoDbAttributeType.SortKey)]
public required string SortKey { get; init; }

[DynamoDbProperty("name")]
public string Name { get; init; } = "";

[DynamoDbProperty("nullableAge")]
public int? NullableAge { get; init; }
}

[TestFixture]
public class ExpressionShould
{
private const string KeyPrefix = "effddb_tests-expressions";
private DynamoDbContext _context = null!;
private List<TestUserWithNullable> _testUsers = null!;

[OneTimeSetUp]
public async Task SetUp()
{
_context = TestHelper.CreateContext();

_testUsers =
[
new() { PartitionKey = KeyPrefix, SortKey = "sk-1", Name = "User with age", NullableAge = 25 },
new() { PartitionKey = KeyPrefix, SortKey = "sk-2", Name = "User without age", NullableAge = null }
];

await _context.BatchWrite()
.WithItems(_testUsers.Select(Batch.PutItem))
.ExecuteAsync();
}

[OneTimeTearDown]
public async Task TearDown()
{
await _context.BatchWrite()
.WithItems(_testUsers.Select(item => Batch.DeleteItem<TestUserWithNullable>().WithPrimaryKey(item.PartitionKey, item.SortKey)))
.ExecuteAsync();
}

[Test(Description = "Should throw an exception when trying to filter on a nullable value type with a non-nullable value")]
public async Task ThrowExceptionOnNullableValueTypeMismatch()
{
await Should.ThrowAsync<InvalidCastException>(async () => await _context.Query<TestUserWithNullable>()
.WithKeyExpression(x => x.On(y => y.PartitionKey).EqualTo(KeyPrefix))
.WithFilterExpression(x => x.On(y => y.NullableAge).EqualTo(25))
.ToAsyncEnumerable()
.ToListAsync()
);
}

[Test(Description = "Casting int to nullable int should work in expressions")]
public async Task SupportNullableValueTypePropertiesInExpressions()
{
var results = await _context.Query<TestUserWithNullable>()
.WithKeyExpression(x => x.On(y => y.PartitionKey).EqualTo(KeyPrefix))
.WithFilterExpression(x => x.On(y => y.NullableAge).EqualTo((int?)25))
.ToAsyncEnumerable()
.ToListAsync();

results.ShouldBe([_testUsers[0]]);
}
}
43 changes: 41 additions & 2 deletions src/EfficientDynamoDb/FluentCondition/Core/FilterBase.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using EfficientDynamoDb.Converters;
Expand Down Expand Up @@ -28,8 +30,16 @@ internal FilterBase(Expression expression)
Expression = expression;
}

protected DdbConverter<TProperty> GetPropertyConverter<TProperty>(DdbExpressionVisitor visitor, bool useSize) =>
useSize ? visitor.Metadata.GetOrAddConverter<TProperty>() : (DdbConverter<TProperty>)visitor.ClassInfo.ConverterBase;
protected DdbConverter<TProperty> GetPropertyConverter<TProperty>(DdbExpressionVisitor visitor, bool useSize)
{
if (useSize)
return visitor.Metadata.GetOrAddConverter<TProperty>();

if (visitor.ClassInfo.ConverterBase is DdbConverter<TProperty> converter)
return converter;

throw BuildCastException<TProperty>(visitor.ClassInfo.ConverterBase.Type);
}

protected void WriteEncodedExpressionName(StringBuilder encodedExpressionName, bool useSize, ref NoAllocStringBuilder builder)
{
Expand All @@ -44,5 +54,34 @@ protected void WriteEncodedExpressionName(StringBuilder encodedExpressionName, b
builder.Append(encodedExpressionName);
}
}

private static InvalidCastException BuildCastException<TProperty>(Type propertyType)
{
var conditionTypeName = GetFriendlyTypeName(typeof(TProperty));
var propertyTypeName = GetFriendlyTypeName(propertyType);

return new InvalidCastException(
$"""Cannot cast type "{conditionTypeName}" provided in condition to property type "{propertyTypeName}". Consider casting "{conditionTypeName}" to "{propertyTypeName}" manually in filter expression."""
);
}

private static string GetFriendlyTypeName(Type type)
{
if (!type.IsGenericType)
return type.Name;

var typeNameSpan = type.Name.AsSpan();
var backtickIndex = typeNameSpan.IndexOf('`');
var genericTypeName = backtickIndex == -1 ? typeNameSpan : typeNameSpan[..backtickIndex];

var genericArguments = type.GetGenericArguments();
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
{
return $"Nullable<{GetFriendlyTypeName(genericArguments[0])}>";
}

var argumentNames = string.Join(", ", genericArguments.Select(GetFriendlyTypeName));
return $"{genericTypeName}<{argumentNames}>";
}
}
}