Compare commits
No commits in common. "627eb52a3bcd5249bf1dc0f0bab9d5af9f829672" and "6dfdf4a8629dda8664dfa2f0a7c2c5bf9ae5dcb7" have entirely different histories.
627eb52a3b
...
6dfdf4a862
3
.gitignore
vendored
3
.gitignore
vendored
@ -261,6 +261,5 @@ paket-files/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
/.cursor
|
||||
/.vscode
|
||||
|
||||
/staging
|
||||
15
CHANGELOG.md
15
CHANGELOG.md
@ -1,23 +1,10 @@
|
||||
# Changelog
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## v1.6.5 - 2026-02-02
|
||||
|
||||
### Changed
|
||||
- Replaced explicit `ArgumentNullException` throws with `ArgumentNullException.ThrowIfNull` in `ExpressionExtensions`, `NetworkConnection`, `Base32Encoder`, `StringExtensions.CSVToDataTable`, `FileLoggerProvider`, and `JsonFileLoggerProvider`.
|
||||
- **Base32Encoder**: empty input now throws `ArgumentException` instead of `ArgumentNullException` for clearer semantics.
|
||||
- **StringExtensions.CSVToDataTable**: null file path throws via `ThrowIfNull`; empty/whitespace path throws `ArgumentException`.
|
||||
- **ObjectExtensions**: `DeepClone` returns `default` for null input; `DeepEqual` explicitly treats (null, null) as true and (null, non-null) as false. Replaced obsolete `FormatterServices.GetUninitializedObject` with `RuntimeHelpers.GetUninitializedObject`. Fixed nullability in `ReferenceEqualityComparer` to match `IEqualityComparer<object>`.
|
||||
- **TotpGenerator**: recovery code generation uses range syntax (`code[..4]`, `code[4..8]`) instead of `Substring`.
|
||||
|
||||
### Fixed
|
||||
- **ExceptionExtensions.ExtractMessages**: null check added to avoid `NullReferenceException` when passed null.
|
||||
- **BaseFileLogger.RemoveExpiredLogFiles**: guard added before `Substring(4)` so malformed log file names do not throw.
|
||||
|
||||
## v1.6.4 - 2026-02-21
|
||||
|
||||
### Added
|
||||
|
||||
@ -46,14 +46,7 @@ This project uses the following commit message format:
|
||||
| `(feature):` | New feature or enhancement |
|
||||
| `(bugfix):` | Bug fix |
|
||||
| `(refactor):` | Code refactoring without functional changes |
|
||||
| `(perf):` | Performance improvement without changing behavior |
|
||||
| `(test):` | Add or update tests |
|
||||
| `(docs):` | Documentation-only changes |
|
||||
| `(build):` | Build system, dependencies, packaging, or project file changes |
|
||||
| `(ci):` | CI/CD pipeline or automation changes |
|
||||
| `(style):` | Formatting or non-functional code style changes |
|
||||
| `(revert):` | Revert a previous commit |
|
||||
| `(chore):` | General maintenance tasks that do not fit the types above |
|
||||
| `(chore):` | Maintenance tasks (dependencies, CI, documentation) |
|
||||
|
||||
### Examples
|
||||
|
||||
@ -61,13 +54,6 @@ This project uses the following commit message format:
|
||||
(feature): add support for custom JWT claims
|
||||
(bugfix): fix multithreading issue in file logger
|
||||
(refactor): simplify expression extension methods
|
||||
(perf): reduce allocations in Base32 encoder
|
||||
(test): add coverage for IQueryable predicate composition
|
||||
(docs): clarify release workflow prerequisites
|
||||
(build): update package metadata in MaksIT.Core.csproj
|
||||
(ci): update GitHub Actions workflow for .NET 10
|
||||
(style): normalize using directives in extension tests
|
||||
(revert): revert breaking change in network connection handling
|
||||
(chore): update copyright year to 2026
|
||||
```
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="150" height="20" role="img" aria-label="Branch Coverage: 50.3%">
|
||||
<title>Branch Coverage: 50.3%</title>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="150" height="20" role="img" aria-label="Branch Coverage: 49.6%">
|
||||
<title>Branch Coverage: 49.6%</title>
|
||||
<linearGradient id="s" x2="0" y2="100%">
|
||||
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
||||
<stop offset="1" stop-opacity=".1"/>
|
||||
@ -15,7 +15,7 @@
|
||||
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="11">
|
||||
<text aria-hidden="true" x="53.75" y="15" fill="#010101" fill-opacity=".3">Branch Coverage</text>
|
||||
<text x="53.75" y="14" fill="#fff">Branch Coverage</text>
|
||||
<text aria-hidden="true" x="128.75" y="15" fill="#010101" fill-opacity=".3">50.3%</text>
|
||||
<text x="128.75" y="14" fill="#fff">50.3%</text>
|
||||
<text aria-hidden="true" x="128.75" y="15" fill="#010101" fill-opacity=".3">49.6%</text>
|
||||
<text x="128.75" y="14" fill="#fff">49.6%</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
@ -1,21 +1,21 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="137" height="20" role="img" aria-label="Line Coverage: 60.1%">
|
||||
<title>Line Coverage: 60.1%</title>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="134.5" height="20" role="img" aria-label="Line Coverage: 60%">
|
||||
<title>Line Coverage: 60%</title>
|
||||
<linearGradient id="s" x2="0" y2="100%">
|
||||
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
||||
<stop offset="1" stop-opacity=".1"/>
|
||||
</linearGradient>
|
||||
<clipPath id="r">
|
||||
<rect width="137" height="20" rx="3" fill="#fff"/>
|
||||
<rect width="134.5" height="20" rx="3" fill="#fff"/>
|
||||
</clipPath>
|
||||
<g clip-path="url(#r)">
|
||||
<rect width="94.5" height="20" fill="#555"/>
|
||||
<rect x="94.5" width="42.5" height="20" fill="#97ca00"/>
|
||||
<rect width="137" height="20" fill="url(#s)"/>
|
||||
<rect x="94.5" width="40" height="20" fill="#97ca00"/>
|
||||
<rect width="134.5" height="20" fill="url(#s)"/>
|
||||
</g>
|
||||
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="11">
|
||||
<text aria-hidden="true" x="47.25" y="15" fill="#010101" fill-opacity=".3">Line Coverage</text>
|
||||
<text x="47.25" y="14" fill="#fff">Line Coverage</text>
|
||||
<text aria-hidden="true" x="115.75" y="15" fill="#010101" fill-opacity=".3">60.1%</text>
|
||||
<text x="115.75" y="14" fill="#fff">60.1%</text>
|
||||
<text aria-hidden="true" x="114.5" y="15" fill="#010101" fill-opacity=".3">60%</text>
|
||||
<text x="114.5" y="14" fill="#fff">60%</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
@ -77,10 +77,4 @@ public class ExceptionExtensionsTests {
|
||||
Assert.Single(messages);
|
||||
Assert.Equal("", messages[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExtractMessages_WhenNull_ThrowsArgumentNullException() {
|
||||
Exception? exception = null;
|
||||
Assert.Throws<ArgumentNullException>(() => exception!.ExtractMessages());
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Linq.Expressions;
|
||||
|
||||
using MaksIT.Core.Extensions;
|
||||
|
||||
@ -24,28 +23,6 @@ public class ExpressionExtensionsTests {
|
||||
Assert.False(compiledPredicate(new TestEntity { Age = 20, Name = "Bob" }));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures AndAlso produces an expression that works with IQueryable.Where (as used by EF Core).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AndAlso_ShouldWorkWithIQueryableWhere() {
|
||||
var source = new List<TestEntity> {
|
||||
new() { Age = 20, Name = "Alice" },
|
||||
new() { Age = 17, Name = "Alice" },
|
||||
new() { Age = 20, Name = "Bob" },
|
||||
new() { Age = 25, Name = "Amy" }
|
||||
};
|
||||
Expression<Func<TestEntity, bool>> first = x => x.Age > 18;
|
||||
Expression<Func<TestEntity, bool>> second = x => (x.Name ?? "").StartsWith("A");
|
||||
var combined = first.AndAlso(second);
|
||||
|
||||
var result = source.AsQueryable().Where(combined).ToList();
|
||||
|
||||
Assert.Equal(2, result.Count);
|
||||
Assert.Contains(result, e => e.Name == "Alice" && e.Age == 20);
|
||||
Assert.Contains(result, e => e.Name == "Amy" && e.Age == 25);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OrElse_ShouldCombineTwoPredicatesWithOrCondition() {
|
||||
// Arrange
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
@ -235,13 +235,6 @@ namespace MaksIT.Core.Tests.Extensions {
|
||||
Assert.Same(s, s2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DeepClone_WhenNull_ReturnsDefault() {
|
||||
Person? source = null;
|
||||
var result = source.DeepClone();
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DeepEqual_ShouldReturnTrue_ForEqualGraphs() {
|
||||
// Arrange
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
namespace MaksIT.Core.Extensions;
|
||||
namespace MaksIT.Core.Extensions;
|
||||
|
||||
public static class ExceptionExtensions {
|
||||
/// <summary>
|
||||
@ -7,7 +7,6 @@ public static class ExceptionExtensions {
|
||||
/// <param name="exception">The exception to extract messages from.</param>
|
||||
/// <returns>A list of exception messages.</returns>
|
||||
public static List<string> ExtractMessages(this Exception exception) {
|
||||
ArgumentNullException.ThrowIfNull(exception);
|
||||
var messages = new List<string>();
|
||||
var current = exception;
|
||||
|
||||
|
||||
@ -1,19 +1,10 @@
|
||||
using System.Linq.Expressions;
|
||||
using System.Linq.Expressions;
|
||||
|
||||
|
||||
namespace MaksIT.Core.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for combining and negating expression predicates.
|
||||
/// AndAlso and OrElse use parameter replacement (no Expression.Invoke), so the result
|
||||
/// is safe for use with IQueryable and EF Core (translatable to SQL).
|
||||
/// </summary>
|
||||
public static class ExpressionExtensions {
|
||||
|
||||
/// <summary>
|
||||
/// Combines two predicates with AND. Uses a single parameter and Expression.AndAlso;
|
||||
/// safe for IQueryable/EF Core (no Invoke).
|
||||
/// </summary>
|
||||
public static Expression<Func<T, bool>> AndAlso<T>(this Expression<Func<T, bool>> first, Expression<Func<T, bool>> second) {
|
||||
ArgumentNullException.ThrowIfNull(first);
|
||||
ArgumentNullException.ThrowIfNull(second);
|
||||
@ -26,10 +17,6 @@ public static class ExpressionExtensions {
|
||||
return Expression.Lambda<Func<T, bool>>(combinedBody, parameter);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Combines two predicates with OR. Uses a single parameter and Expression.OrElse;
|
||||
/// safe for IQueryable/EF Core (no Invoke).
|
||||
/// </summary>
|
||||
public static Expression<Func<T, bool>> OrElse<T>(this Expression<Func<T, bool>> first, Expression<Func<T, bool>> second) {
|
||||
ArgumentNullException.ThrowIfNull(first);
|
||||
ArgumentNullException.ThrowIfNull(second);
|
||||
@ -43,7 +30,7 @@ public static class ExpressionExtensions {
|
||||
}
|
||||
|
||||
public static Expression<Func<T, bool>> Not<T>(this Expression<Func<T, bool>> expression) {
|
||||
ArgumentNullException.ThrowIfNull(expression);
|
||||
if (expression == null) throw new ArgumentNullException(nameof(expression));
|
||||
|
||||
var parameter = expression.Parameters[0];
|
||||
var body = Expression.Not(expression.Body);
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
using System.Reflection;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
@ -41,7 +42,6 @@ public static class ObjectExtensions {
|
||||
/// Creates a deep clone of the object, preserving reference identity and supporting cycles.
|
||||
/// </summary>
|
||||
public static T DeepClone<T>(this T source) {
|
||||
if (source is null) return default!;
|
||||
return (T)DeepCloneInternal(source, new Dictionary<object, object>(ReferenceEqualityComparer.Instance));
|
||||
}
|
||||
|
||||
@ -49,9 +49,7 @@ public static class ObjectExtensions {
|
||||
/// Deeply compares two objects for structural equality (fields, including private ones).
|
||||
/// </summary>
|
||||
public static bool DeepEqual<T>(this T a, T b) {
|
||||
if (a is null && b is null) return true;
|
||||
if (a is null || b is null) return false;
|
||||
return DeepEqualInternal(a!, b!, new HashSet<(object, object)>(ReferencePairComparer.Instance));
|
||||
return DeepEqualInternal(a, b, new HashSet<(object, object)>(ReferencePairComparer.Instance));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -86,7 +84,7 @@ public static class ObjectExtensions {
|
||||
return CloneStruct(source, type, visited);
|
||||
|
||||
// Reference type: allocate uninitialized object, then copy fields
|
||||
var clone = RuntimeHelpers.GetUninitializedObject(type);
|
||||
var clone = FormatterServices.GetUninitializedObject(type);
|
||||
visited[source] = clone;
|
||||
CopyAllFields(source, clone, type, visited);
|
||||
return clone;
|
||||
@ -251,8 +249,8 @@ public static class ObjectExtensions {
|
||||
|
||||
private sealed class ReferenceEqualityComparer : IEqualityComparer<object> {
|
||||
public static readonly ReferenceEqualityComparer Instance = new ReferenceEqualityComparer();
|
||||
public new bool Equals(object? x, object? y) => ReferenceEquals(x, y);
|
||||
public int GetHashCode(object? obj) => RuntimeHelpers.GetHashCode(obj!);
|
||||
public new bool Equals(object x, object y) => ReferenceEquals(x, y);
|
||||
public int GetHashCode(object obj) => RuntimeHelpers.GetHashCode(obj);
|
||||
}
|
||||
|
||||
private sealed class ReferencePairComparer : IEqualityComparer<(object, object)> {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Data;
|
||||
using System.Globalization;
|
||||
using System.Security.Cryptography;
|
||||
@ -246,8 +246,8 @@ namespace MaksIT.Core.Extensions {
|
||||
public static string ToKebabCase(this string input) => input.ToCase(StringCaseStyle.KebabCase);
|
||||
|
||||
public static DataTable CSVToDataTable(this string filePath) {
|
||||
ArgumentNullException.ThrowIfNull(filePath);
|
||||
if (string.IsNullOrWhiteSpace(filePath)) throw new ArgumentException("File path cannot be empty.", nameof(filePath));
|
||||
if (string.IsNullOrEmpty(filePath))
|
||||
throw new ArgumentNullException(nameof(filePath));
|
||||
|
||||
using var sr = new StreamReader(filePath);
|
||||
|
||||
|
||||
@ -78,8 +78,7 @@ public abstract class BaseFileLogger : ILogger, IDisposable {
|
||||
|
||||
foreach (var logFile in logFiles) {
|
||||
var fileName = Path.GetFileNameWithoutExtension(logFile);
|
||||
if (fileName.Length >= 4 && fileName.StartsWith("log_", StringComparison.Ordinal) &&
|
||||
DateTime.TryParseExact(fileName.Substring(4), "yyyy-MM-dd", null, System.Globalization.DateTimeStyles.None, out var logDate)) {
|
||||
if (DateTime.TryParseExact(fileName.Substring(4), "yyyy-MM-dd", null, System.Globalization.DateTimeStyles.None, out var logDate)) {
|
||||
if (logDate < expirationDate) {
|
||||
File.Delete(logFile);
|
||||
}
|
||||
|
||||
@ -8,8 +8,7 @@ public class FileLoggerProvider : ILoggerProvider {
|
||||
private readonly TimeSpan _retentionPeriod;
|
||||
|
||||
public FileLoggerProvider(string folderPath, TimeSpan? retentionPeriod = null) {
|
||||
ArgumentNullException.ThrowIfNull(folderPath);
|
||||
_folderPath = folderPath;
|
||||
_folderPath = folderPath ?? throw new ArgumentNullException(nameof(folderPath));
|
||||
_retentionPeriod = retentionPeriod ?? TimeSpan.FromDays(7); // Default retention period is 7 days
|
||||
}
|
||||
|
||||
|
||||
@ -8,8 +8,7 @@ public class JsonFileLoggerProvider : ILoggerProvider {
|
||||
private readonly TimeSpan _retentionPeriod;
|
||||
|
||||
public JsonFileLoggerProvider(string folderPath, TimeSpan? retentionPeriod = null) {
|
||||
ArgumentNullException.ThrowIfNull(folderPath);
|
||||
_folderPath = folderPath;
|
||||
_folderPath = folderPath ?? throw new ArgumentNullException(nameof(folderPath));
|
||||
_retentionPeriod = retentionPeriod ?? TimeSpan.FromDays(7); // Default retention period is 7 days
|
||||
}
|
||||
|
||||
|
||||
@ -12,7 +12,7 @@
|
||||
|
||||
<!-- NuGet package metadata -->
|
||||
<PackageId>MaksIT.Core</PackageId>
|
||||
<Version>1.6.5</Version>
|
||||
<Version>1.6.4</Version>
|
||||
<Authors>Maksym Sadovnychyy</Authors>
|
||||
<Company>MAKS-IT</Company>
|
||||
<Product>MaksIT.Core</Product>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Net;
|
||||
using System.Runtime.InteropServices;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@ -27,9 +27,9 @@ public class NetworkConnection : IDisposable {
|
||||
throw new PlatformNotSupportedException("NetworkConnection is only supported on Windows.");
|
||||
}
|
||||
|
||||
ArgumentNullException.ThrowIfNull(logger);
|
||||
ArgumentNullException.ThrowIfNull(networkName);
|
||||
ArgumentNullException.ThrowIfNull(credentials);
|
||||
if (logger == null) throw new ArgumentNullException(nameof(logger));
|
||||
if (networkName == null) throw new ArgumentNullException(nameof(networkName));
|
||||
if (credentials == null) throw new ArgumentNullException(nameof(credentials));
|
||||
|
||||
var netResource = new NetResource {
|
||||
Scope = ResourceScope.GlobalNetwork,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
using System.Text;
|
||||
using System.Text;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
|
||||
@ -14,8 +14,9 @@ public static class Base32Encoder {
|
||||
[NotNullWhen(false)] out string? errorMessage
|
||||
) {
|
||||
try {
|
||||
ArgumentNullException.ThrowIfNull(data);
|
||||
if (data.Length == 0) throw new ArgumentException("Data cannot be empty.", nameof(data));
|
||||
if (data == null || data.Length == 0) {
|
||||
throw new ArgumentNullException(nameof(data));
|
||||
}
|
||||
|
||||
var result = new StringBuilder();
|
||||
int buffer = data[0];
|
||||
@ -65,8 +66,9 @@ public static class Base32Encoder {
|
||||
[NotNullWhen(false)] out string? errorMessage
|
||||
) {
|
||||
try {
|
||||
ArgumentNullException.ThrowIfNull(base32);
|
||||
if (string.IsNullOrWhiteSpace(base32)) throw new ArgumentException("Base32 string cannot be empty.", nameof(base32));
|
||||
if (string.IsNullOrEmpty(base32)) {
|
||||
throw new ArgumentNullException(nameof(base32));
|
||||
}
|
||||
|
||||
base32 = base32.TrimEnd(PaddingChar.ToCharArray());
|
||||
int byteCount = base32.Length * 5 / 8;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace MaksIT.Core.Security;
|
||||
@ -132,8 +132,8 @@ public static class TotpGenerator {
|
||||
recoveryCodes = new List<string>();
|
||||
|
||||
for (int i = 0; i < defaultCodeCount; i++) {
|
||||
var code = Guid.NewGuid().ToString("N")[..8]; // Generate an 8-character code
|
||||
var formattedCode = $"{code[..4]}-{code[4..8]}"; // Format as XXXX-XXXX
|
||||
var code = Guid.NewGuid().ToString("N").Substring(0, 8); // Generate an 8-character code
|
||||
var formattedCode = $"{code.Substring(0, 4)}-{code.Substring(4, 4)}"; // Format as XXXX-XXXX
|
||||
recoveryCodes.Add(formattedCode);
|
||||
}
|
||||
|
||||
|
||||
@ -1,15 +1,9 @@
|
||||
using System.Linq.Dynamic.Core;
|
||||
using System.Linq.Dynamic.Core;
|
||||
using System.Linq.Expressions;
|
||||
using System.Text.RegularExpressions;
|
||||
using MaksIT.Core.Abstractions.Webapi;
|
||||
using MaksIT.Core.Extensions;
|
||||
|
||||
namespace MaksIT.Core.Webapi.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Base request model for paged list operations with optional filter and sort.
|
||||
/// BuildFilterExpression produces expressions suitable for IQueryable/EF Core (single parameter, translatable operations).
|
||||
/// </summary>
|
||||
public class PagedRequest : RequestModelBase {
|
||||
public int PageSize { get; set; } = 100;
|
||||
public int PageNumber { get; set; } = 1;
|
||||
@ -22,11 +16,6 @@ public class PagedRequest : RequestModelBase {
|
||||
return BuildFilterExpression<T>(Filters);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses the filter string into an Expression<Func<T, bool>> for use with IQueryable/EF Core.
|
||||
/// Uses a single parameter; avoid filter syntax that would require non-translatable operations.
|
||||
/// Supported: property access, ==, !=, &&, ||, !, Contains, StartsWith, EndsWith, ToLower() for strings.
|
||||
/// </summary>
|
||||
public virtual Expression<Func<T, bool>> BuildFilterExpression<T>(string? filters) {
|
||||
if (string.IsNullOrWhiteSpace(filters))
|
||||
return x => true; // Returns an expression that doesn't filter anything.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user