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
62 changes: 62 additions & 0 deletions JsonApiToolkit.Tests/Extensions/QueryableExtensionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,68 @@ public void ApplyFilters_WithGreaterThanFilter_FiltersCorrectly()
Assert.Equal(5, result[1].Id);
}

[Fact]
public void ApplyFilters_WithLessThanOrEqualDateOnlyFilter_IncludesWholeDay()
{
// A date-only Le value (no time component) must include every moment
// of that day, not just its exact midnight instant.
var testData = new List<TestEntity>
{
new TestEntity { Id = 1, CreatedAt = new DateTime(2026, 9, 14, 0, 0, 0) },
new TestEntity { Id = 2, CreatedAt = new DateTime(2026, 9, 14, 15, 30, 0) },
new TestEntity { Id = 3, CreatedAt = new DateTime(2026, 9, 14, 23, 59, 59) },
new TestEntity { Id = 4, CreatedAt = new DateTime(2026, 9, 15, 0, 0, 0) },
}.AsQueryable();

var filterGroup = new FilterGroup
{
Filters = new List<FilterParameter>
{
new FilterParameter
{
Field = "CreatedAt",
Operator = FilterOperator.Le,
Value = "2026-09-14",
},
},
};

var result = testData.ApplyFilters(filterGroup).ToList();

Assert.Equal(3, result.Count);
Assert.DoesNotContain(result, e => e.Id == 4);
}

[Fact]
public void ApplyFilters_WithLessThanOrEqualFullTimestampFilter_DoesNotExtendToEndOfDay()
{
// A full timestamp (time component present) must be used exactly as
// given — only the bare date-only case gets the end-of-day bump.
var testData = new List<TestEntity>
{
new TestEntity { Id = 1, CreatedAt = new DateTime(2026, 9, 14, 9, 0, 0) },
new TestEntity { Id = 2, CreatedAt = new DateTime(2026, 9, 14, 15, 30, 0) },
}.AsQueryable();

var filterGroup = new FilterGroup
{
Filters = new List<FilterParameter>
{
new FilterParameter
{
Field = "CreatedAt",
Operator = FilterOperator.Le,
Value = "2026-09-14T12:00:00",
},
},
};

var result = testData.ApplyFilters(filterGroup).ToList();

Assert.Single(result);
Assert.Equal(1, result[0].Id);
}

[Fact]
public void ApplyFilters_WithLikeFilter_FiltersCorrectly()
{
Expand Down
158 changes: 79 additions & 79 deletions JsonApiToolkit.TypeGen/packages.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,27 +40,27 @@
},
"Intility.Extensions.Logging": {
"type": "Transitive",
"resolved": "3.1.6",
"contentHash": "de+ySanItzm3jrpKOqtwWa86hoRQmwuAjyIQX9Tj4JA8tbeV+0Mjs2V4auJ9boCsLcqSv75sK1Ac6fsY9zkcDw==",
"resolved": "3.1.7",
"contentHash": "slDca7FHqWrV53U+B7rkKZJXlcORFxn7Ux/Ku37XuJdZxfAc3SyD/yP3Gj/G1Ji8qvcnqo/+wQ2ZTG8TFo4Jew==",
"dependencies": {
"Microsoft.Extensions.Hosting.Abstractions": "10.0.10",
"Microsoft.Extensions.Hosting.Abstractions": "10.0.11",
"Serilog": "4.4.0"
}
},
"Intility.Logging.AspNetCore": {
"type": "Transitive",
"resolved": "3.1.6",
"contentHash": "6dYcQnj/5ZLnomhyHUFWppVr8dwjgp8KTqhYaHVtXC3pC8P2OtqjQIFkhlHJCrgLLUBVhKwiNLhVzYGUR1s7/g==",
"resolved": "3.1.7",
"contentHash": "uVNl/D+HAVh6A9AIaFT5QxQwrFG5BypwUfMQ3GFZ5pHoILAsCAqY3Z/YDkksN4mfUkGc20pVqLlyc8rOeO6j4w==",
"dependencies": {
"Intility.Extensions.Logging": "3.1.6",
"Microsoft.Extensions.Hosting.Abstractions": "10.0.10",
"Microsoft.Extensions.Logging.Abstractions": "10.0.10",
"Intility.Extensions.Logging": "3.1.7",
"Microsoft.Extensions.Hosting.Abstractions": "10.0.11",
"Microsoft.Extensions.Logging.Abstractions": "10.0.11",
"Serilog.AspNetCore": "10.0.0",
"Serilog.Enrichers.AssemblyName": "2.0.0",
"Serilog.Enrichers.AzureClaims": "1.2.9",
"Serilog.Enrichers.Environment": "3.0.1",
"Serilog.Enrichers.Memory": "1.0.4",
"System.Security.Cryptography.Xml": "10.0.10"
"System.Security.Cryptography.Xml": "10.0.11"
}
},
"Microsoft.AspNetCore.Authentication.JwtBearer": {
Expand Down Expand Up @@ -120,54 +120,54 @@
},
"Microsoft.EntityFrameworkCore": {
"type": "Transitive",
"resolved": "10.0.11",
"contentHash": "VOSGU8en6HZJs8t7UMFN+9vGcRgVOOn6fA44Ngcg2NyvJ3P1KE94iAb0XzaVaGhXGtt+qaM/VtEn0/hzluQJeg==",
"resolved": "10.0.12",
"contentHash": "e7OrVN8U5yr4kuwdTHWBHGVQIVsGfMkAr0Ej1/BnrS989Q0XBVANpIzNZ2bc1GvMZ0/XwMt5InTnmKskxGy99Q==",
"dependencies": {
"Microsoft.EntityFrameworkCore.Abstractions": "10.0.11",
"Microsoft.EntityFrameworkCore.Analyzers": "10.0.11",
"Microsoft.Extensions.Caching.Memory": "10.0.11",
"Microsoft.Extensions.Logging": "10.0.11"
"Microsoft.EntityFrameworkCore.Abstractions": "10.0.12",
"Microsoft.EntityFrameworkCore.Analyzers": "10.0.12",
"Microsoft.Extensions.Caching.Memory": "10.0.12",
"Microsoft.Extensions.Logging": "10.0.12"
}
},
"Microsoft.EntityFrameworkCore.Abstractions": {
"type": "Transitive",
"resolved": "10.0.11",
"contentHash": "6auJR+9+9VunznKfH7WGrHMrnrmA0F7JZ22EXzwXvVhjfnbu9Xq7NSIWaOf3KJsOanM2qf5ajJ2JR5TlcPZTLA=="
"resolved": "10.0.12",
"contentHash": "kDrux7T6C3V/YYYe2LXr7x6AEsVyq5Y2YoTE9er1SwbDtap3z1g8RgxNh5eKQVyB4T+DKvBBJ4Ezd/VZzJQJZg=="
},
"Microsoft.EntityFrameworkCore.Analyzers": {
"type": "Transitive",
"resolved": "10.0.11",
"contentHash": "Bv7X4wSSnzCQED9WYXKJ8fwgyvKwf0xZM1GO8xkf6CF9zl+UBnvjxmcPnokJRy0JKjc1SlHSzzhx1HcL4jitTQ=="
"resolved": "10.0.12",
"contentHash": "Kd4o2oO1A6dfKxjz5LS16QyLtpCFTUiFWwmBABETAXRsSImBdRdWeKjSgbbRhAMiV3a3/nWr95ZtzClT4uaADQ=="
},
"Microsoft.EntityFrameworkCore.Relational": {
"type": "Transitive",
"resolved": "10.0.11",
"contentHash": "grznnTJgEYxaWpdKAsTzg6j+89jHgCXWYp+QGtlX5O92+w/VuhWM6JLPYb+uw8M9VhGUvOTsO76dYOy9vNPd5Q==",
"resolved": "10.0.12",
"contentHash": "OiHmr8XzX96dbgMsYGe8qoqg4KxibdZG2r0QUVJBGktFInOp8IrABe8jvcbo5hrqeWfbwpK7fCEBiEPbY/ik4A==",
"dependencies": {
"Microsoft.EntityFrameworkCore": "10.0.11",
"Microsoft.Extensions.Caching.Memory": "10.0.11",
"Microsoft.Extensions.Configuration.Abstractions": "10.0.11",
"Microsoft.Extensions.Logging": "10.0.11"
"Microsoft.EntityFrameworkCore": "10.0.12",
"Microsoft.Extensions.Caching.Memory": "10.0.12",
"Microsoft.Extensions.Configuration.Abstractions": "10.0.12",
"Microsoft.Extensions.Logging": "10.0.12"
}
},
"Microsoft.Extensions.Caching.Abstractions": {
"type": "Transitive",
"resolved": "10.0.11",
"contentHash": "vUl798SmruTqqlt/xH2gDk3tJlhk6k3HdOXAHirlRfbNKDym4g/kRpUL9S4sl6F6FsOTOMW+ZsDapqlZMOOiEw==",
"resolved": "10.0.12",
"contentHash": "sI3A6MUAyZwccLnwq0cXuRmC3WBtXieimBWafXSfhRJ4jnM2uGVXrqImDDVyW27u4vVaTOGlJxNJd8d1FGvCNQ==",
"dependencies": {
"Microsoft.Extensions.Primitives": "10.0.11"
"Microsoft.Extensions.Primitives": "10.0.12"
}
},
"Microsoft.Extensions.Caching.Memory": {
"type": "Transitive",
"resolved": "10.0.11",
"contentHash": "el1g0mBEbDBGY2bT9mcSfrTWO8QlPdq2nOCnvQugioOFwHV+bVBMeiakoI0dNOdj8d6Hi9K6HY2xzRUWJiDR3w==",
"resolved": "10.0.12",
"contentHash": "fhC3DHcBcFF4bXvHIxFtXJYNut7q7ZItqNFcgWfrIgmpVImlcEKNHhE7ztpNSPJYMHYYStS5QGND2I5mC26yeA==",
"dependencies": {
"Microsoft.Extensions.Caching.Abstractions": "10.0.11",
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.11",
"Microsoft.Extensions.Logging.Abstractions": "10.0.11",
"Microsoft.Extensions.Options": "10.0.11",
"Microsoft.Extensions.Primitives": "10.0.11"
"Microsoft.Extensions.Caching.Abstractions": "10.0.12",
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.12",
"Microsoft.Extensions.Logging.Abstractions": "10.0.12",
"Microsoft.Extensions.Options": "10.0.12",
"Microsoft.Extensions.Primitives": "10.0.12"
}
},
"Microsoft.Extensions.Configuration": {
Expand All @@ -181,10 +181,10 @@
},
"Microsoft.Extensions.Configuration.Abstractions": {
"type": "Transitive",
"resolved": "10.0.11",
"contentHash": "fVi053xdpda9Em7vSkmgVxO/PtgC2m78ekReKWsgcyskqY0U82Bz/MONwxpGzI0hElYKJfw+fupqMVeKW3fSaA==",
"resolved": "10.0.12",
"contentHash": "8xaGcvS/qZ1otoxPQCEJkNva389CVL/plNcvIETZhQTETYdRkYDPEYhUMoAGONo4FU45ufdfE0j29AfWVVj0wA==",
"dependencies": {
"Microsoft.Extensions.Primitives": "10.0.11"
"Microsoft.Extensions.Primitives": "10.0.12"
}
},
"Microsoft.Extensions.Configuration.Binder": {
Expand All @@ -198,16 +198,16 @@
},
"Microsoft.Extensions.DependencyInjection": {
"type": "Transitive",
"resolved": "10.0.11",
"contentHash": "PSmotV19c7E3lKed++uYo1kSiXFI+uTl37CBSrhq+CfLC3FCHjG7R91+xPnNehQfHS1b0Tzo/CCLPWH3qaEheg==",
"resolved": "10.0.12",
"contentHash": "lXyK2O5GoYvfxW8eCFcD16JFbcoSTM1sJkAM0UHS1jZyl9NYMW64Tqm6OQFT0IDBjZi+xHt95/Zg+nxZhGFhZg==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.11"
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.12"
}
},
"Microsoft.Extensions.DependencyInjection.Abstractions": {
"type": "Transitive",
"resolved": "10.0.11",
"contentHash": "/a1aJz4m7ylhEDf25ugQChLQoN5XwoGjWw/BoR/ZWWKsO1v4DdJElS1uyngahz4B/eOzjFk1KNTkarRLE5wsIg=="
"resolved": "10.0.12",
"contentHash": "9/qymSh7hVDMGTGwrLz8MRp5zRyXy9adGDOs4HwRdnLil3oZGYuWeZjbmHgCQ9BL1qBroVfgUK3U/nb61617Cw=="
},
"Microsoft.Extensions.DependencyModel": {
"type": "Transitive",
Expand All @@ -216,58 +216,58 @@
},
"Microsoft.Extensions.Diagnostics.Abstractions": {
"type": "Transitive",
"resolved": "10.0.10",
"contentHash": "9uWiKpeOVac355STyChWR/pliFX/5CeLqChW9kKsaxyDH4EUTZxMkT4Jwp/J/peLm0GBFmSX5c0WCse3yCnq1Q==",
"resolved": "10.0.11",
"contentHash": "se7Kx8QpJEt+nf26L4qIVAofGTDr1wbexxsh/Fm3Xc04xUkqUXK06KUS7FLwSQYSjqb7q9n+T7MEcXYBhI1Y5g==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10",
"Microsoft.Extensions.Options": "10.0.10"
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.11",
"Microsoft.Extensions.Options": "10.0.11"
}
},
"Microsoft.Extensions.FileProviders.Abstractions": {
"type": "Transitive",
"resolved": "10.0.10",
"contentHash": "c5zqFCY9DiIpMovLd7/d/CTiEtrMOuQ639dhv3PABtKQIKNQikSHwQt8+N679uii9q+B55lgK28Uv64FOwEu8w==",
"resolved": "10.0.11",
"contentHash": "JOjac6SQQgZmdmB8WGEw61/7siqMZoWJMkmq2p1goJGxqI59lO6oB4bOl0jNsbaPBdYy5Mlkb+6U7T4+CjnD8Q==",
"dependencies": {
"Microsoft.Extensions.Primitives": "10.0.10"
"Microsoft.Extensions.Primitives": "10.0.11"
}
},
"Microsoft.Extensions.Hosting.Abstractions": {
"type": "Transitive",
"resolved": "10.0.10",
"contentHash": "5LugpYGHk+mkn0a8IZgcyfBca8PCTAU9RQFoMrTdtOOidq88M2SI5f3px6ugnzgxC+eTkvYYJi8pzlUnG5xdAQ==",
"resolved": "10.0.11",
"contentHash": "pwtpF7iF/NNaOBcX+pvMZ7y2+JAVbH5KkNrH9uMZtuVxVJsFTDiWiCR7Tk3HVptsAijaetipUZVRVK2LLq+nvA==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "10.0.10",
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10",
"Microsoft.Extensions.Diagnostics.Abstractions": "10.0.10",
"Microsoft.Extensions.FileProviders.Abstractions": "10.0.10",
"Microsoft.Extensions.Logging.Abstractions": "10.0.10"
"Microsoft.Extensions.Configuration.Abstractions": "10.0.11",
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.11",
"Microsoft.Extensions.Diagnostics.Abstractions": "10.0.11",
"Microsoft.Extensions.FileProviders.Abstractions": "10.0.11",
"Microsoft.Extensions.Logging.Abstractions": "10.0.11"
}
},
"Microsoft.Extensions.Logging": {
"type": "Transitive",
"resolved": "10.0.11",
"contentHash": "nUOJwgFkSiLHiVGFpU22pIJtuWYewuSYQ3JVuP/gdK8ASMT807Px+TYQiRWs6uSsOmoyFTaVCwKXTasczV6BpA==",
"resolved": "10.0.12",
"contentHash": "6I46fTPfgYkrjRYfRXbho9WOvOelTnNjWuZws/hzGHDASH1LEJeA4VKK9k3wJvido8o7jJSB5WkMTonX7HM1bA==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "10.0.11",
"Microsoft.Extensions.Logging.Abstractions": "10.0.11",
"Microsoft.Extensions.Options": "10.0.11"
"Microsoft.Extensions.DependencyInjection": "10.0.12",
"Microsoft.Extensions.Logging.Abstractions": "10.0.12",
"Microsoft.Extensions.Options": "10.0.12"
}
},
"Microsoft.Extensions.Logging.Abstractions": {
"type": "Transitive",
"resolved": "10.0.11",
"contentHash": "Ljd0Uxoq5XpScD2Bg0nM/r3mwx7Ao5Uq24eo2ARxbGvqJ7Zht6rt2cJtwVRH4Cv+1ZVMdXz6TB43KbpmsxRrvQ==",
"resolved": "10.0.12",
"contentHash": "+24lC4plfbEDNfLAdTV/SWKS7dW+16X4HdydO3R++134kSNTzcbYA4KpR1Hdh6uWisB8Za3AzwyOn+K+NxWIug==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.11"
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.12"
}
},
"Microsoft.Extensions.Options": {
"type": "Transitive",
"resolved": "10.0.11",
"contentHash": "eY1GAKcTfD2maP27J84X9IovT3yjHJ2dVDzPmDg6/XqYvt3jMzJhtfQCLjG9pVsZGAd+8DQ2QrjaDcs2+VQLGw==",
"resolved": "10.0.12",
"contentHash": "TDYD33TSRpXKZWlmTXNlj5kCihxatmv2Ec1u6C+bMYLphCS7PoSLE9Pjd/nunDoE7yETk+LLKjVJX78HYtWjpA==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.11",
"Microsoft.Extensions.Primitives": "10.0.11"
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.12",
"Microsoft.Extensions.Primitives": "10.0.12"
}
},
"Microsoft.Extensions.Options.ConfigurationExtensions": {
Expand All @@ -284,8 +284,8 @@
},
"Microsoft.Extensions.Primitives": {
"type": "Transitive",
"resolved": "10.0.11",
"contentHash": "SXcz+kF+4Oo9b1+55zntpJFYfwb1jw66ioxptyNOOTDc8g2FHnBFWjZpsWfCvZIhzr0x+4e2trVTs4OKwQfBtw=="
"resolved": "10.0.12",
"contentHash": "dYfCLR52UA+3DL7C4I/pvSaRPkNqxrUAQmbFL2u0zvYKKzqgrFCJl08Df+F1aYc8leu9JvpC9bsURUdpExcBXQ=="
},
"Microsoft.Identity.Abstractions": {
"type": "Transitive",
Expand Down Expand Up @@ -603,8 +603,8 @@
},
"System.Security.Cryptography.Pkcs": {
"type": "Transitive",
"resolved": "10.0.10",
"contentHash": "KG8t5RHczdgB3IWvRndpI0nE1DSo7ikDYSDbITxfgRFyIvnYiKzuPjaZOsPnwFQ1aOGUl9sEulJul9wlHLEUoA=="
"resolved": "10.0.11",
"contentHash": "8IV+rI3xN/Mkq9MsSX7VZTv9T9Wt+tyhHLwBX9VNZBk8m8kknEs1JeTSjI2x9M2L/fSja+c2WSE5n5Yy0GXdoQ=="
},
"System.Security.Cryptography.ProtectedData": {
"type": "Transitive",
Expand All @@ -613,18 +613,18 @@
},
"System.Security.Cryptography.Xml": {
"type": "Transitive",
"resolved": "10.0.10",
"contentHash": "Mz6Y+s+uMQKj1H2BUF0OhlmaOQkLgSG34/IrUaBDirYBQ6bgwcFvO/XtXWcoDlBLFoFB0uKqDNhybHOd+fQK1A==",
"resolved": "10.0.11",
"contentHash": "TokfVsaU2fmcwvsU6ihLCkseVfwhLX6Tmbl3qh3nOBIqHSX4yqFllpslA+PuHERH/ENu3kwqo4deZiyT0Wvh3Q==",
"dependencies": {
"System.Security.Cryptography.Pkcs": "10.0.10"
"System.Security.Cryptography.Pkcs": "10.0.11"
}
},
"Intility.JsonApiToolkit": {
"type": "Project",
"dependencies": {
"Intility.Logging.AspNetCore": "[3.1.6, )",
"Microsoft.EntityFrameworkCore": "[10.0.11, )",
"Microsoft.EntityFrameworkCore.Relational": "[10.0.11, )"
"Intility.Logging.AspNetCore": "[3.1.7, )",
"Microsoft.EntityFrameworkCore": "[10.0.12, )",
"Microsoft.EntityFrameworkCore.Relational": "[10.0.12, )"
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,21 @@ int depth
return null;
}

// A date-only value (no time component, e.g. "2026-09-14") used with
// Le against a DateTime property means "up to and including that
// whole day" to callers — but it parses to that day's midnight, so
// an unadjusted <= would only match the exact midnight instant and
// silently exclude the rest of the day. Bump it to the last tick of
// the day so Le behaves as "on or before this date".
if (
filter.Operator == FilterOperator.Le
&& filterValue is DateTime dateOnlyBoundary
&& IsDateOnlyValue(filter.Value)
)
{
filterValue = dateOnlyBoundary.Date.AddDays(1).AddTicks(-1);
}

ConstantExpression constant = Expression.Constant(filterValue, targetType);

return filter.Operator switch
Expand Down Expand Up @@ -604,4 +619,14 @@ private static void ThrowIfTooDeep(int depth, FilterParameter filter)
}
);
}

/// <summary>
/// True if the raw filter value string carries no time-of-day component
/// (e.g. "2026-09-14"), as opposed to a full timestamp (e.g.
/// "2026-09-14T10:00:00"). ISO 8601 date-times always separate the time
/// with 'T' and represent time-of-day with ':', so the absence of both
/// is a reliable signal the caller only specified a calendar date.
/// </summary>
private static bool IsDateOnlyValue(string value) =>
!value.Contains('T') && !value.Contains(':');
}