From 3a0ad99072712d3b85cd242286ce20e4cd5af699 Mon Sep 17 00:00:00 2001 From: Paul Trampert Date: Sat, 5 Sep 2026 22:58:22 -0400 Subject: [PATCH 1/6] Add InlineValue and BuildValueExpression to QueryAttribute Adds the shared machinery attributes will use to supply query values, without yet changing any attribute's behavior. BuildValueExpression emits the query value as a member access on the query object -- the shape the C# compiler produces for a captured variable -- which ORMs such as EF Core lift into a SQL parameter. It converts to the type the call site needs only when the declared query property type is not assignable to it, preserving what the previous Expression.Constant(value, targetType) calls were doing for cases like a Guid? query property against a Guid target. InlineValue opts a property back into a literal constant, for cases where inlining is preferable, such as a predicate over badly skewed data where a plan built for the specific value beats a reused one. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_018pK9kvAcxFsZ3KkYQcEZGP --- .../Attributes/QueryAttribute.cs | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/PTrampert.QueryObjects/Attributes/QueryAttribute.cs b/PTrampert.QueryObjects/Attributes/QueryAttribute.cs index 455802a..29d2401 100644 --- a/PTrampert.QueryObjects/Attributes/QueryAttribute.cs +++ b/PTrampert.QueryObjects/Attributes/QueryAttribute.cs @@ -27,6 +27,54 @@ protected QueryAttribute(string targetProperty = null) TargetProperty = targetProperty; } + /// + /// Embed the query value into the expression tree as a literal constant instead of referencing it + /// through the query object. Defaults to false. + /// + /// + /// By default the query value is referenced as a member access on the query object, which is the same + /// shape the C# compiler emits for a captured variable. ORMs such as Entity Framework Core lift that + /// shape into a SQL parameter, so the generated SQL is identical no matter what value is supplied, which + /// keeps both the ORM's compiled query cache and the database's plan cache effective. + /// + /// A literal constant is inlined into the generated SQL, producing different SQL for every value. That is + /// occasionally what you want -- for example when a predicate runs over badly skewed data and you would + /// rather the database build a plan for the specific value than reuse a plan built for a previous one. + /// + public bool InlineValue { get; set; } + + /// + /// Builds the expression that supplies the query value to a comparison, honoring . + /// + /// The instance of the query object. + /// The PropertyInfo of the property holding the query value. + /// + /// The value of on . Only used when + /// is true. + /// + /// + /// The type the resulting expression must have, or null to use the declared type of the query property. + /// + /// The expression supplying the query value. + protected Expression BuildValueExpression(object queryObject, PropertyInfo queryProperty, object queryValue, + Type valueType = null) + { + if (InlineValue) + { + return valueType == null + ? Expression.Constant(queryValue) + : Expression.Constant(queryValue, valueType); + } + + Expression value = Expression.Property(Expression.Constant(queryObject), queryProperty); + if (valueType != null && !valueType.IsAssignableFrom(value.Type)) + { + value = Expression.Convert(value, valueType); + } + + return value; + } + /// /// Builds the query expression for this query property. /// From 9dea79ef19fc0263367c719801c20905d9c37ef0 Mon Sep 17 00:00:00 2001 From: Paul Trampert Date: Sat, 5 Sep 2026 22:58:36 -0400 Subject: [PATCH 2/6] Parameterize values in the comparison and string attributes Routes SimpleComparisonQueryAttribute (Equals, NotEquals, GreaterThan, GreaterThanOrEqual, LessThan, LessThanOrEqual) and the string attributes through BuildValueExpression, so their values are referenced through the query object instead of inlined as constants. QueryExpressionBuilderTests asserts exact expression strings, so its expectations move to the new shape. TestQuery and TestAdvancedQuery become plain classes: a record's generated ToString dumps every property value, which the expression printer would then render inside the constant node, making the assertions long and value-dependent. Nothing relied on their value semantics. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_018pK9kvAcxFsZ3KkYQcEZGP --- .../QueryExpressionBuilderTests.cs | 19 ++++++++++++++----- .../TestAdvancedQuery.cs | 2 +- PTrampert.QueryObjects.Test/TestQuery.cs | 2 +- .../SimpleComparisonQueryAttribute.cs | 4 ++-- .../StringContainsQueryAttribute.cs | 4 ++-- .../StringStartsWithQueryAttribute.cs | 4 ++-- 6 files changed, 22 insertions(+), 13 deletions(-) diff --git a/PTrampert.QueryObjects.Test/QueryExpressionBuilderTests.cs b/PTrampert.QueryObjects.Test/QueryExpressionBuilderTests.cs index e482d8c..2141052 100644 --- a/PTrampert.QueryObjects.Test/QueryExpressionBuilderTests.cs +++ b/PTrampert.QueryObjects.Test/QueryExpressionBuilderTests.cs @@ -16,8 +16,15 @@ public void BuildQueryExpression_WithSimpleQueryObject_ReturnsCorrectExpression( }; var expression = new QueryExpressionBuilder().BuildQueryExpression(query); - - Assert.That(expression.ToString(), Is.EqualTo("Param_0 => ((((Param_0.IntProperty == 1) AndAlso (Param_0.AnotherProp > 1)) AndAlso (Param_0.AnotherProp < 4)) AndAlso (Param_0.StringProperty != \"Derp\"))")); + + // Query values are referenced as member accesses on the query object rather than inlined as + // constants, so that ORMs lift them into query parameters. + const string q = "value(PTrampert.QueryObjects.Test.TestQuery)"; + Assert.That(expression.ToString(), Is.EqualTo( + $"Param_0 => ((((Param_0.IntProperty == {q}.IntProperty) " + + $"AndAlso (Param_0.AnotherProp > {q}.AnotherPropLowerLimit)) " + + $"AndAlso (Param_0.AnotherProp < {q}.AnotherPropUpperLimit)) " + + $"AndAlso (Param_0.StringProperty != {q}.StringProperty))")); } [Test] @@ -29,7 +36,9 @@ public void BuildQueryExpression_WithAdvancedQueryObject_ReturnsCorrectExpressio }; var expression = new QueryExpressionBuilder().BuildQueryExpression(query); - - Assert.That(expression.ToString(), Is.EqualTo("Param_0 => ((Param_0.IntProperty == 1) AndAlso Param_0.StringProperty.Contains(\"Derp\"))")); + + const string q = "value(PTrampert.QueryObjects.Test.TestAdvancedQuery)"; + Assert.That(expression.ToString(), Is.EqualTo( + $"Param_0 => ((Param_0.IntProperty == {q}.IntProperty) AndAlso Param_0.StringProperty.Contains(\"Derp\"))")); } -} \ No newline at end of file +} diff --git a/PTrampert.QueryObjects.Test/TestAdvancedQuery.cs b/PTrampert.QueryObjects.Test/TestAdvancedQuery.cs index 0f5c92a..d84d9b7 100644 --- a/PTrampert.QueryObjects.Test/TestAdvancedQuery.cs +++ b/PTrampert.QueryObjects.Test/TestAdvancedQuery.cs @@ -3,7 +3,7 @@ namespace PTrampert.QueryObjects.Test; -internal record TestAdvancedQuery : IQueryObject +internal class TestAdvancedQuery : IQueryObject { [EqualsQuery] public int IntProperty { get; set; } diff --git a/PTrampert.QueryObjects.Test/TestQuery.cs b/PTrampert.QueryObjects.Test/TestQuery.cs index 47500b2..70820f5 100644 --- a/PTrampert.QueryObjects.Test/TestQuery.cs +++ b/PTrampert.QueryObjects.Test/TestQuery.cs @@ -2,7 +2,7 @@ namespace PTrampert.QueryObjects.Test; -internal record TestQuery +internal class TestQuery { [EqualsQuery] public int IntProperty { get; set; } diff --git a/PTrampert.QueryObjects/Attributes/SimpleComparisonQueryAttribute.cs b/PTrampert.QueryObjects/Attributes/SimpleComparisonQueryAttribute.cs index 986e68f..ced28f5 100644 --- a/PTrampert.QueryObjects/Attributes/SimpleComparisonQueryAttribute.cs +++ b/PTrampert.QueryObjects/Attributes/SimpleComparisonQueryAttribute.cs @@ -38,8 +38,8 @@ public override Expression BuildExpression(object queryObject, PropertyInfo quer var queryValue = queryProperty.GetValue(queryObject); if (IgnoreIfNull && queryValue == null) return null; - var constant = Expression.Constant(queryValue, targetProperty.PropertyType); - return ComparisonExpressionBuilder(Expression.Property(targetParameter, targetProperty), constant); + var value = BuildValueExpression(queryObject, queryProperty, queryValue, targetProperty.PropertyType); + return ComparisonExpressionBuilder(Expression.Property(targetParameter, targetProperty), value); } } } \ No newline at end of file diff --git a/PTrampert.QueryObjects/Attributes/StringContainsQueryAttribute.cs b/PTrampert.QueryObjects/Attributes/StringContainsQueryAttribute.cs index e7aa1f2..88f4a73 100644 --- a/PTrampert.QueryObjects/Attributes/StringContainsQueryAttribute.cs +++ b/PTrampert.QueryObjects/Attributes/StringContainsQueryAttribute.cs @@ -34,9 +34,9 @@ public override Expression BuildExpression(object queryObject, PropertyInfo quer var queryValue = queryProperty.GetValue(queryObject); if (queryValue == null) return IgnoreIfNull ? null : Expression.Constant(false); - var constant = Expression.Constant(queryValue); + var value = BuildValueExpression(queryObject, queryProperty, queryValue, typeof(string)); var containsMethod = typeof(string).GetMethod(nameof(string.Contains), [typeof(string)])!; - return Expression.Call(Expression.Property(targetParameter, targetProperty), containsMethod, constant); + return Expression.Call(Expression.Property(targetParameter, targetProperty), containsMethod, value); } } } \ No newline at end of file diff --git a/PTrampert.QueryObjects/Attributes/StringStartsWithQueryAttribute.cs b/PTrampert.QueryObjects/Attributes/StringStartsWithQueryAttribute.cs index 04ff374..db3912d 100644 --- a/PTrampert.QueryObjects/Attributes/StringStartsWithQueryAttribute.cs +++ b/PTrampert.QueryObjects/Attributes/StringStartsWithQueryAttribute.cs @@ -39,8 +39,8 @@ PropertyInfo targetProperty if (queryValue == null) return IgnoreIfNull ? null : Expression.Constant(false); - var constant = Expression.Constant(queryValue); + var value = BuildValueExpression(queryObject, queryProperty, queryValue, typeof(string)); var startsWithMethod = typeof(string).GetMethod(nameof(string.StartsWith), [typeof(string)])!; - return Expression.Call(Expression.Property(targetParameter, targetProperty), startsWithMethod, constant); + return Expression.Call(Expression.Property(targetParameter, targetProperty), startsWithMethod, value); } } \ No newline at end of file From d113f4a2d568e139d8993cc6bd3418ef878ce0bf Mon Sep 17 00:00:00 2001 From: Paul Trampert Date: Sat, 5 Sep 2026 22:58:36 -0400 Subject: [PATCH 3/6] Parameterize values in the collection attributes Routes ContainsQueryAttribute and AnyOfQueryAttribute through BuildValueExpression. NoneOfQueryAttribute needs no change; it wraps AnyOfQueryAttribute's result in a Not. The AnyOf paths now request IEnumerable for the element type rather than relying on the runtime type of the collection, so a query property declared as IEnumerable or object is converted explicitly instead of working by accident. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_018pK9kvAcxFsZ3KkYQcEZGP --- .../Attributes/AnyOfQueryAttribute.cs | 15 ++++++++++----- .../Attributes/ContainsQueryAttribute.cs | 4 ++-- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/PTrampert.QueryObjects/Attributes/AnyOfQueryAttribute.cs b/PTrampert.QueryObjects/Attributes/AnyOfQueryAttribute.cs index a1c06cc..bc4e8d6 100644 --- a/PTrampert.QueryObjects/Attributes/AnyOfQueryAttribute.cs +++ b/PTrampert.QueryObjects/Attributes/AnyOfQueryAttribute.cs @@ -1,5 +1,6 @@ using System; using System.Collections; +using System.Collections.Generic; using System.Linq.Expressions; using System.Reflection; using PTrampert.QueryObjects.Internals; @@ -36,16 +37,19 @@ PropertyInfo targetProperty && typeof(IEnumerable).IsAssignableFrom(targetElementType)) { targetElementType = targetElementType.GetCollectionElementType(); - return BuildCollectionExpression(queryValue, targetParameter, targetProperty, targetElementType); + return BuildCollectionExpression(queryObject, queryProperty, queryValue, targetParameter, targetProperty, targetElementType); } var containsMethod = targetElementType.GetContainsMethod(); - var constant = Expression.Constant(queryValue); + var value = BuildValueExpression(queryObject, queryProperty, queryValue, + typeof(IEnumerable<>).MakeGenericType(targetElementType)); var propertyAccess = Expression.Property(targetParameter, targetProperty); - return Expression.Call(containsMethod, constant, propertyAccess); + return Expression.Call(containsMethod, value, propertyAccess); } private Expression BuildCollectionExpression( + object queryObject, + PropertyInfo queryProperty, IEnumerable queryValue, ParameterExpression targetParameter, PropertyInfo targetProperty, @@ -54,9 +58,10 @@ Type targetElementType { var intersectMethod = targetElementType.GetIntersectMethod(); var anyMethod = targetElementType.GetAnyMethod(); - var constant = Expression.Constant(queryValue); + var value = BuildValueExpression(queryObject, queryProperty, queryValue, + typeof(IEnumerable<>).MakeGenericType(targetElementType)); var propertyAccess = Expression.Property(targetParameter, targetProperty); - var intersectCall = Expression.Call(intersectMethod, propertyAccess, constant); + var intersectCall = Expression.Call(intersectMethod, propertyAccess, value); var anyCall = Expression.Call(anyMethod, intersectCall); return anyCall; } diff --git a/PTrampert.QueryObjects/Attributes/ContainsQueryAttribute.cs b/PTrampert.QueryObjects/Attributes/ContainsQueryAttribute.cs index 16bd8bd..55b6b80 100644 --- a/PTrampert.QueryObjects/Attributes/ContainsQueryAttribute.cs +++ b/PTrampert.QueryObjects/Attributes/ContainsQueryAttribute.cs @@ -44,9 +44,9 @@ public override Expression BuildExpression(object queryObject, PropertyInfo quer } var elementType = targetProperty.PropertyType.GetCollectionElementType(); - var constant = Expression.Constant(queryValue); + var value = BuildValueExpression(queryObject, queryProperty, queryValue, elementType); var containsMethod = elementType.GetContainsMethod(); - return Expression.Call(containsMethod, Expression.Property(targetParameter, targetProperty), constant); + return Expression.Call(containsMethod, Expression.Property(targetParameter, targetProperty), value); } } } \ No newline at end of file From ebfaf21a5b4779f17a3e52b5b76eb64dd3102268 Mon Sep 17 00:00:00 2001 From: Paul Trampert Date: Sat, 5 Sep 2026 22:58:36 -0400 Subject: [PATCH 4/6] Add tests for value parameterization and InlineValue Covers the parameterized expression shape for each attribute family, the conversion applied to a nullable query property, InlineValue emitting literals instead, and both strategies filtering identically. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_018pK9kvAcxFsZ3KkYQcEZGP --- .../Attributes/InlineValueTests.cs | 134 ++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 PTrampert.QueryObjects.Test/Attributes/InlineValueTests.cs diff --git a/PTrampert.QueryObjects.Test/Attributes/InlineValueTests.cs b/PTrampert.QueryObjects.Test/Attributes/InlineValueTests.cs new file mode 100644 index 0000000..f8fe9c2 --- /dev/null +++ b/PTrampert.QueryObjects.Test/Attributes/InlineValueTests.cs @@ -0,0 +1,134 @@ +using PTrampert.QueryObjects.Attributes; +using PTrampert.QueryObjects.Internals; + +namespace PTrampert.QueryObjects.Test.Attributes; + +public class InlineValueTests +{ + private class InlineTestTarget + { + public Guid Id { get; init; } + public string Name { get; init; } = string.Empty; + public List Tags { get; init; } = new(); + } + + private class ParameterizedQuery + { + [EqualsQuery(nameof(InlineTestTarget.Id))] + public Guid? Id { get; set; } + + [StringContainsQuery(nameof(InlineTestTarget.Name))] + public string? NameContains { get; set; } + + [StringStartsWithQuery(nameof(InlineTestTarget.Name))] + public string? NameStartsWith { get; set; } + + [ContainsQuery(nameof(InlineTestTarget.Tags))] + public string? Tag { get; set; } + } + + private class AnyOfParameterizedQuery + { + [AnyOfQuery(nameof(InlineTestTarget.Name))] + public IEnumerable? Names { get; set; } + } + + private class AnyOfCollectionParameterizedQuery + { + [AnyOfQuery(nameof(InlineTestTarget.Tags))] + public IEnumerable? Tags { get; set; } + } + + private class InlinedQuery + { + [EqualsQuery(nameof(InlineTestTarget.Id), InlineValue = true)] + public Guid? Id { get; set; } + + [StringContainsQuery(nameof(InlineTestTarget.Name), InlineValue = true)] + public string? NameContains { get; set; } + } + + private static string BuildExpression(object query) => + new QueryExpressionBuilder().BuildQueryExpression(query).ToString(); + + [Test] + public void ByDefault_QueryValuesAreReferencedThroughTheQueryObject() + { + var query = new ParameterizedQuery + { + Id = Guid.NewGuid(), + NameContains = "app", + NameStartsWith = "a", + Tag = "fruit" + }; + + var expression = BuildExpression(query); + + const string q = "value(PTrampert.QueryObjects.Test.Attributes.InlineValueTests+ParameterizedQuery)"; + Assert.Multiple(() => + { + // The nullable query property is converted to the target property's type. + Assert.That(expression, Does.Contain($"Convert({q}.Id, Guid)")); + Assert.That(expression, Does.Contain($"Contains({q}.NameContains)")); + Assert.That(expression, Does.Contain($"StartsWith({q}.NameStartsWith)")); + Assert.That(expression, Does.Contain($"Param_0.Tags.Contains({q}.Tag)")); + Assert.That(expression, Does.Not.Contain("\"app\"")); + Assert.That(expression, Does.Not.Contain(query.Id.ToString())); + }); + } + + [Test] + public void ByDefault_AnyOfReferencesTheCollectionThroughTheQueryObject() + { + var query = new AnyOfParameterizedQuery { Names = ["apple", "banana"] }; + + var expression = BuildExpression(query); + + Assert.That(expression, Does.Contain( + "value(PTrampert.QueryObjects.Test.Attributes.InlineValueTests+AnyOfParameterizedQuery).Names.Contains(Param_0.Name)")); + } + + [Test] + public void ByDefault_AnyOfAgainstACollectionTargetReferencesTheCollectionThroughTheQueryObject() + { + var query = new AnyOfCollectionParameterizedQuery { Tags = ["fruit", "yellow"] }; + + var expression = BuildExpression(query); + + Assert.That(expression, Does.Contain( + "Param_0.Tags.Intersect(value(PTrampert.QueryObjects.Test.Attributes.InlineValueTests+AnyOfCollectionParameterizedQuery).Tags).Any()")); + } + + [Test] + public void WithInlineValue_QueryValuesAreEmbeddedAsConstants() + { + var id = Guid.NewGuid(); + var query = new InlinedQuery { Id = id, NameContains = "app" }; + + var expression = BuildExpression(query); + + Assert.Multiple(() => + { + Assert.That(expression, Does.Contain($"Param_0.Id == {id}")); + Assert.That(expression, Does.Contain("Contains(\"app\")")); + Assert.That(expression, Does.Not.Contain("value(PTrampert.QueryObjects.Test.Attributes.InlineValueTests+InlinedQuery)")); + }); + } + + [Test] + public void BothValueStrategies_ProduceTheSameResults() + { + var id = Guid.NewGuid(); + var data = new List + { + new() { Id = id, Name = "apple", Tags = ["fruit"] }, + new() { Id = Guid.NewGuid(), Name = "banana", Tags = ["fruit"] } + }; + + var parameterized = data.Where(new ParameterizedQuery { Id = id, NameContains = "app" }).ToList(); + var inlined = data.Where(new InlinedQuery { Id = id, NameContains = "app" }).ToList(); + + Assert.That(parameterized, Is.EqualTo(new[] { data[0] })); + Assert.That(inlined, Is.EqualTo(parameterized)); + } +} From b9b7de1a8529e618de90fb08dfee6567d8cef68e Mon Sep 17 00:00:00 2001 From: Paul Trampert Date: Sat, 5 Sep 2026 22:58:36 -0400 Subject: [PATCH 5/6] Document value parameterization and InlineValue Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_018pK9kvAcxFsZ3KkYQcEZGP --- README.md | 2 ++ docs/getting-started.md | 29 +++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/README.md b/README.md index ced1f12..354e093 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,8 @@ PTrampert.QueryObjects is a .NET library that enables you to define query object - Attribute-based query object filtering - Supports custom query logic via `IQueryObject` - Works with any `IQueryable` (e.g. Entity Framework, MongoDB) or `IEnumerable` +- Emits query values as parameterizable member accesses rather than inline constants, so ORM and database query + caches stay effective (opt out per property with `InlineValue = true`) ## Example Usage diff --git a/docs/getting-started.md b/docs/getting-started.md index 980b3ec..e290514 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -50,6 +50,35 @@ var filteredUsers = dbContext.Users.Where(query); ``` +## Query Values and Parameterization + +Query values are not baked into the generated expression tree as literal constants. Instead, each attribute +emits a member access on the query object itself -- the same shape the C# compiler produces for a captured +variable: + +```csharp +// x => x.Name == queryObject.Name +``` + +ORMs such as Entity Framework Core lift that shape into a SQL parameter, so the SQL produced for a given query +object type is identical regardless of the values supplied. That keeps EF Core's compiled query cache and the +database's query plan cache effective. A literal constant, by contrast, is inlined into the SQL, producing +different SQL text for every value. + +If you need the value inlined instead -- for example when a predicate runs over badly skewed data and you would +rather the database build a plan for the specific value than reuse one built for a previous value -- set +`InlineValue` on the attribute: + +```csharp +public class UserQuery +{ + [EqualsQuery(InlineValue = true)] + public bool? IsDeleted { get; set; } +} +``` + +`InlineValue` is available on every query attribute and defaults to `false`. + ## Supported Query Attributes For a complete and up-to-date list of supported query attributes, please refer to the [API documentation for PTrampert.QueryObjects.Attributes](../api/PTrampert.QueryObjects.Attributes.yml). From 56a8de73369dcacc47ea686711b7a86b00ed5cb6 Mon Sep 17 00:00:00 2001 From: Paul Trampert Date: Sat, 5 Sep 2026 23:08:12 -0400 Subject: [PATCH 6/6] Keep TestQuery and TestAdvancedQuery as records The expression printer renders a constant using the value's ToString, so a record fixture prints its full property dump inside the constant node. Interpolating query.ToString() into the expected string handles that without changing the fixtures. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_018pK9kvAcxFsZ3KkYQcEZGP --- PTrampert.QueryObjects.Test/QueryExpressionBuilderTests.cs | 4 ++-- PTrampert.QueryObjects.Test/TestAdvancedQuery.cs | 2 +- PTrampert.QueryObjects.Test/TestQuery.cs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/PTrampert.QueryObjects.Test/QueryExpressionBuilderTests.cs b/PTrampert.QueryObjects.Test/QueryExpressionBuilderTests.cs index 2141052..7cb15ce 100644 --- a/PTrampert.QueryObjects.Test/QueryExpressionBuilderTests.cs +++ b/PTrampert.QueryObjects.Test/QueryExpressionBuilderTests.cs @@ -19,7 +19,7 @@ public void BuildQueryExpression_WithSimpleQueryObject_ReturnsCorrectExpression( // Query values are referenced as member accesses on the query object rather than inlined as // constants, so that ORMs lift them into query parameters. - const string q = "value(PTrampert.QueryObjects.Test.TestQuery)"; + var q = query.ToString(); Assert.That(expression.ToString(), Is.EqualTo( $"Param_0 => ((((Param_0.IntProperty == {q}.IntProperty) " + $"AndAlso (Param_0.AnotherProp > {q}.AnotherPropLowerLimit)) " @@ -37,7 +37,7 @@ public void BuildQueryExpression_WithAdvancedQueryObject_ReturnsCorrectExpressio var expression = new QueryExpressionBuilder().BuildQueryExpression(query); - const string q = "value(PTrampert.QueryObjects.Test.TestAdvancedQuery)"; + var q = query.ToString(); Assert.That(expression.ToString(), Is.EqualTo( $"Param_0 => ((Param_0.IntProperty == {q}.IntProperty) AndAlso Param_0.StringProperty.Contains(\"Derp\"))")); } diff --git a/PTrampert.QueryObjects.Test/TestAdvancedQuery.cs b/PTrampert.QueryObjects.Test/TestAdvancedQuery.cs index d84d9b7..0f5c92a 100644 --- a/PTrampert.QueryObjects.Test/TestAdvancedQuery.cs +++ b/PTrampert.QueryObjects.Test/TestAdvancedQuery.cs @@ -3,7 +3,7 @@ namespace PTrampert.QueryObjects.Test; -internal class TestAdvancedQuery : IQueryObject +internal record TestAdvancedQuery : IQueryObject { [EqualsQuery] public int IntProperty { get; set; } diff --git a/PTrampert.QueryObjects.Test/TestQuery.cs b/PTrampert.QueryObjects.Test/TestQuery.cs index 70820f5..47500b2 100644 --- a/PTrampert.QueryObjects.Test/TestQuery.cs +++ b/PTrampert.QueryObjects.Test/TestQuery.cs @@ -2,7 +2,7 @@ namespace PTrampert.QueryObjects.Test; -internal class TestQuery +internal record TestQuery { [EqualsQuery] public int IntProperty { get; set; }