From 4b5477207dc39a03679b9d17762a05dba8730271 Mon Sep 17 00:00:00 2001 From: Roman Efremov Date: Wed, 5 Feb 2025 10:40:34 +0500 Subject: [PATCH 1/2] =?UTF-8?q?=D0=A0=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D1=82=D1=8C=20=D0=BE=D0=B1=D1=85=D0=BE=D0=B4=20?= =?UTF-8?q?=D0=B1=D0=B8=D0=BD=D0=B0=D1=80=D0=BD=D0=BE=D0=B3=D0=BE=20=D0=B4?= =?UTF-8?q?=D0=B5=D1=80=D0=B5=D0=B2=D0=B0=20=D0=B8=20=D1=81=D0=BE=D0=B7?= =?UTF-8?q?=D0=B4=D0=B0=D0=BD=D0=B8=D0=B5=20=D1=84=D0=B8=D0=BB=D1=8C=D1=82?= =?UTF-8?q?=D1=80=D0=B0=20=D0=BD=D0=B0=20=D0=BE=D1=81=D0=BD=D0=BE=D0=B2?= =?UTF-8?q?=D0=B5=20=D1=81=D0=BF=D0=B5=D1=86=D0=B8=D1=84=D0=B8=D0=BA=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Extensions/SpecificationExtensions.cs | 83 ++++++ .../Helpers/FilterBuilder.cs | 137 ++++++++++ .../DD.Persistence.Database.csproj | 1 + .../Entity/TimestampedValues.cs | 2 +- .../EntityAbstractions/IValuesItem.cs | 19 ++ .../Specifications/EmptySpecification.cs | 14 + .../ValuesItem/ValueEqaulSpecification.cs | 28 ++ .../ValueGreateOrEqualSpecification.cs | 22 ++ .../ValuesItem/ValueGreateSpecification.cs | 22 ++ .../ValueLessOrEqualSpecification.cs | 22 ++ .../ValuesItem/ValueLessSpecification.cs | 22 ++ .../ValuesItem/ValueNotEqaulSpecification.cs | 27 ++ .../DD.Persistence.Test.csproj | 3 +- DD.Persistence.Test/FilterBuilderShould.cs | 250 ++++++++++++++++++ 14 files changed, 649 insertions(+), 3 deletions(-) create mode 100644 DD.Persistence.Database.Postgres/Extensions/SpecificationExtensions.cs create mode 100644 DD.Persistence.Database.Postgres/Helpers/FilterBuilder.cs create mode 100644 DD.Persistence.Database/EntityAbstractions/IValuesItem.cs create mode 100644 DD.Persistence.Database/Specifications/EmptySpecification.cs create mode 100644 DD.Persistence.Database/Specifications/ValuesItem/ValueEqaulSpecification.cs create mode 100644 DD.Persistence.Database/Specifications/ValuesItem/ValueGreateOrEqualSpecification.cs create mode 100644 DD.Persistence.Database/Specifications/ValuesItem/ValueGreateSpecification.cs create mode 100644 DD.Persistence.Database/Specifications/ValuesItem/ValueLessOrEqualSpecification.cs create mode 100644 DD.Persistence.Database/Specifications/ValuesItem/ValueLessSpecification.cs create mode 100644 DD.Persistence.Database/Specifications/ValuesItem/ValueNotEqaulSpecification.cs create mode 100644 DD.Persistence.Test/FilterBuilderShould.cs diff --git a/DD.Persistence.Database.Postgres/Extensions/SpecificationExtensions.cs b/DD.Persistence.Database.Postgres/Extensions/SpecificationExtensions.cs new file mode 100644 index 0000000..2c13c10 --- /dev/null +++ b/DD.Persistence.Database.Postgres/Extensions/SpecificationExtensions.cs @@ -0,0 +1,83 @@ +using Ardalis.Specification; +using DD.Persistence.Database.Specifications; +using System.Linq.Expressions; + +namespace DD.Persistence.Database.Postgres.Extensions; + +// ToDo: рассмотреть возможность вынести логику в спецификации +public static class SpecificationExtensions +{ + public static ISpecification Or(this ISpecification spec, ISpecification otherSpec) + { + var newSpec = new EmptySpecification(); + + var parameter = Expression.Parameter(typeof(T), "x"); + + var exprSpec1 = CombineWhereExpressions(spec.WhereExpressions, parameter); + var exprSpec2 = CombineWhereExpressions(otherSpec.WhereExpressions, parameter); + + Expression? orExpression = exprSpec1 is not null && exprSpec2 is not null + ? Expression.OrElse(exprSpec1, exprSpec2) + : exprSpec1 ?? exprSpec2; + + if (orExpression is null) return newSpec; + + var lambdaExpr = Expression.Lambda>(orExpression, parameter); + + newSpec.Query.Where(lambdaExpr); + + return newSpec; + } + + // ToDo: Рефакторинг + public static ISpecification And(this ISpecification spec, ISpecification otherSpec) + { + var newSpec = new EmptySpecification(); + + var parameter = Expression.Parameter(typeof(T), "x"); + + var exprSpec1 = CombineWhereExpressions(spec.WhereExpressions, parameter); + var exprSpec2 = CombineWhereExpressions(otherSpec.WhereExpressions, parameter); + + Expression? andExpression = exprSpec1 is not null && exprSpec2 is not null + ? Expression.AndAlso(exprSpec1, exprSpec2) + : exprSpec1 ?? exprSpec2; + + if (andExpression is null) return newSpec; + + var lambdaExpr = Expression.Lambda>(andExpression, parameter); + + newSpec.Query.Where(lambdaExpr); + + return newSpec; + } + + public static Expression? CombineWhereExpressions(IEnumerable> whereExpressions, ParameterExpression parameter) + { + Expression? newExpr = null; + foreach (var where in whereExpressions) + { + var expr = ParameterReplacerVisitor.Replace(where.Filter.Body, where.Filter.Parameters[0], parameter); + newExpr = newExpr is null ? expr : Expression.AndAlso(newExpr, expr); + } + return newExpr; + } +} + +public class ParameterReplacerVisitor : ExpressionVisitor +{ + private readonly Expression _newExpression; + private readonly ParameterExpression _oldParameter; + + private ParameterReplacerVisitor(ParameterExpression oldParameter, Expression newExpression) + { + _oldParameter = oldParameter; + _newExpression = newExpression; + } + + internal static Expression Replace(Expression expression, ParameterExpression oldParameter, Expression newExpression) + => new ParameterReplacerVisitor(oldParameter, newExpression).Visit(expression); + + protected override Expression VisitParameter(ParameterExpression p) + => p == _oldParameter ? _newExpression : p; +} diff --git a/DD.Persistence.Database.Postgres/Helpers/FilterBuilder.cs b/DD.Persistence.Database.Postgres/Helpers/FilterBuilder.cs new file mode 100644 index 0000000..e8a7f57 --- /dev/null +++ b/DD.Persistence.Database.Postgres/Helpers/FilterBuilder.cs @@ -0,0 +1,137 @@ +using Ardalis.Specification; +using DD.Persistence.Database.Entity; +using DD.Persistence.Database.EntityAbstractions; +using DD.Persistence.Database.Postgres.Extensions; +using DD.Persistence.Database.Specifications.ValuesItem; +using DD.Persistence.Filter.Models.Abstractions; +using DD.Persistence.Filter.Models.Enumerations; +using DD.Persistence.Filter.Visitors; +using DD.Persistence.Models; + +namespace DD.Persistence.Database.Postgres.Helpers; +public static class FilterBuilder +{ + public static ISpecification? BuildFilter(this DataScheme dataScheme, TNode root) + where TEntity : IValuesItem + { + var result = dataScheme.BuildSpecificationByNextNode(root); + + return result; + } + + private static ISpecification? BuildSpecificationByNextNode(this DataScheme dataScheme, TNode node) + where TEntity : IValuesItem + { + var propIndexMap = dataScheme.PropNames + .Select((name, index) => new { name, index }) + .ToDictionary(x => x.name, x => x.index); + + var visitor = new NodeVisitor?>( + v => + { + var leftSpecification = dataScheme.BuildSpecificationByNextNode(v.Left); + var rigthSpecification = dataScheme.BuildSpecificationByNextNode(v.Rigth); + if (leftSpecification is null) + return rigthSpecification; + if (rigthSpecification is null) + return leftSpecification; + + ISpecification? result = null; + switch (v.Operation) + { + case OperationEnum.And: + result = leftSpecification.And(rigthSpecification); + break; + case OperationEnum.Or: + result = leftSpecification.Or(rigthSpecification); + break; + } + + return result; + }, + t => + { + int keyIndex; + if (!propIndexMap.TryGetValue(t.PropName, out keyIndex)) + throw new ArgumentException($"Свойство {t.PropName} не найдено в схеме данных"); + var type = dataScheme.PropTypes[keyIndex]; + + ISpecification? result = null; + switch (type) + { + case PropTypeEnum.String: + var stringValue = Convert.ToString(t.Value); + switch (t.Operation) + { + case OperationEnum.Equal: + result = new ValueEqaulSpecification(keyIndex, stringValue); + break; + case OperationEnum.NotEqual: + result = new ValueNotEqaulSpecification(keyIndex, stringValue); + break; + } + break; + case PropTypeEnum.Double: + var doubleValue = Convert.ToDouble(t.Value); + switch (t.Operation) + // ToDo: можно схлопнуть в один Generic - метод, где TValue : struct + // Но в таком случае придётся продумать аналогичное решение на уровне спецификаций + { + case OperationEnum.Equal: + result = new ValueEqaulSpecification(keyIndex, doubleValue); + break; + case OperationEnum.NotEqual: + result = new ValueNotEqaulSpecification(keyIndex, doubleValue); + break; + case OperationEnum.Greate: + result = new ValueGreateSpecification(keyIndex, doubleValue); + break; + case OperationEnum.GreateOrEqual: + result = new ValueGreateOrEqualSpecification(keyIndex, doubleValue); + break; + case OperationEnum.Less: + result = new ValueLessSpecification(keyIndex, doubleValue); + break; + case OperationEnum.LessOrEqual: + result = new ValueLessOrEqualSpecification(keyIndex, doubleValue); + break; + } + break; + case PropTypeEnum.DateTime: + stringValue = Convert.ToString(t.Value); + DateTimeOffset? dateTimeValue = string.IsNullOrEmpty(stringValue) + ? null + : DateTimeOffset.Parse(stringValue); + switch (t.Operation) + { + case OperationEnum.Equal: + result = new ValueEqaulSpecification(keyIndex, dateTimeValue); + break; + case OperationEnum.NotEqual: + result = new ValueNotEqaulSpecification(keyIndex, dateTimeValue); + break; + case OperationEnum.Greate: + result = new ValueGreateSpecification(keyIndex, dateTimeValue); + break; + case OperationEnum.GreateOrEqual: + result = new ValueGreateOrEqualSpecification(keyIndex, dateTimeValue); + break; + case OperationEnum.Less: + result = new ValueLessSpecification(keyIndex, dateTimeValue); + break; + case OperationEnum.LessOrEqual: + result = new ValueLessOrEqualSpecification(keyIndex, dateTimeValue); + break; + } + break; + } + + return result; + } + ); + + var result = node.AcceptVisitor(visitor); + + return result; + } +} diff --git a/DD.Persistence.Database/DD.Persistence.Database.csproj b/DD.Persistence.Database/DD.Persistence.Database.csproj index 9303e6a..485a40b 100644 --- a/DD.Persistence.Database/DD.Persistence.Database.csproj +++ b/DD.Persistence.Database/DD.Persistence.Database.csproj @@ -7,6 +7,7 @@ + all diff --git a/DD.Persistence.Database/Entity/TimestampedValues.cs b/DD.Persistence.Database/Entity/TimestampedValues.cs index a2835f7..84778c1 100644 --- a/DD.Persistence.Database/Entity/TimestampedValues.cs +++ b/DD.Persistence.Database/Entity/TimestampedValues.cs @@ -7,7 +7,7 @@ namespace DD.Persistence.Database.Entity; [Table("timestamped_values")] [PrimaryKey(nameof(DiscriminatorId), nameof(Timestamp))] -public class TimestampedValues : ITimestampedItem +public class TimestampedValues : ITimestampedItem, IValuesItem { [Comment("Временная отметка"), Key] public DateTimeOffset Timestamp { get; set; } diff --git a/DD.Persistence.Database/EntityAbstractions/IValuesItem.cs b/DD.Persistence.Database/EntityAbstractions/IValuesItem.cs new file mode 100644 index 0000000..f2a801c --- /dev/null +++ b/DD.Persistence.Database/EntityAbstractions/IValuesItem.cs @@ -0,0 +1,19 @@ +using DD.Persistence.Database.Entity; + +namespace DD.Persistence.Database.EntityAbstractions; + +/// +/// Сущность с данными, принадлежащими к определенной схеме +/// +public interface IValuesItem +{ + /// + /// Схема данных + /// + DataScheme? DataScheme { get; set; } + + /// + /// Значения + /// + object[] Values { get; set; } +} \ No newline at end of file diff --git a/DD.Persistence.Database/Specifications/EmptySpecification.cs b/DD.Persistence.Database/Specifications/EmptySpecification.cs new file mode 100644 index 0000000..010d0a9 --- /dev/null +++ b/DD.Persistence.Database/Specifications/EmptySpecification.cs @@ -0,0 +1,14 @@ +using Ardalis.Specification; + +namespace DD.Persistence.Database.Specifications; + +/// +/// Пустая спецификация (вспомогательная) +/// +/// +public class EmptySpecification : Specification +{ + public EmptySpecification() + { + } +} \ No newline at end of file diff --git a/DD.Persistence.Database/Specifications/ValuesItem/ValueEqaulSpecification.cs b/DD.Persistence.Database/Specifications/ValuesItem/ValueEqaulSpecification.cs new file mode 100644 index 0000000..f5fd385 --- /dev/null +++ b/DD.Persistence.Database/Specifications/ValuesItem/ValueEqaulSpecification.cs @@ -0,0 +1,28 @@ +using Ardalis.Specification; +using DD.Persistence.Database.EntityAbstractions; + +namespace DD.Persistence.Database.Specifications.ValuesItem; + +/// +/// Спецификация эквивалентности значений IValuesItem в соответствии с индексацией +/// +/// +public class ValueEqaulSpecification : Specification + where TEntity : IValuesItem +{ + public ValueEqaulSpecification(int index, string? value) + { + Query.Where(e => Convert.ToString(e.Values[index]) == value); + } + + public ValueEqaulSpecification(int index, double? value) + { + Query.Where(e => Convert.ToDouble(e.Values[index]) == value); + } + + public ValueEqaulSpecification(int index, DateTimeOffset? value) + { + // ToDo: рассмотреть возможность более вменяемого парсинга для даты + Query.Where(e => DateTimeOffset.Parse(Convert.ToString(e.Values[index])!) == value); + } +} diff --git a/DD.Persistence.Database/Specifications/ValuesItem/ValueGreateOrEqualSpecification.cs b/DD.Persistence.Database/Specifications/ValuesItem/ValueGreateOrEqualSpecification.cs new file mode 100644 index 0000000..ada97ad --- /dev/null +++ b/DD.Persistence.Database/Specifications/ValuesItem/ValueGreateOrEqualSpecification.cs @@ -0,0 +1,22 @@ +using Ardalis.Specification; +using DD.Persistence.Database.EntityAbstractions; + +namespace DD.Persistence.Database.Specifications.ValuesItem; + +/// +/// Спецификация "больше либо равно" для значений IValuesItem в соответствии с индексацией +/// +/// +public class ValueGreateOrEqualSpecification : Specification + where TEntity : IValuesItem +{ + public ValueGreateOrEqualSpecification(int index, double? value) + { + Query.Where(e => Convert.ToDouble(e.Values[index]) >= value); + } + + public ValueGreateOrEqualSpecification(int index, DateTimeOffset? value) + { + Query.Where(e => DateTimeOffset.Parse(Convert.ToString(e.Values[index])!) >= value); + } +} diff --git a/DD.Persistence.Database/Specifications/ValuesItem/ValueGreateSpecification.cs b/DD.Persistence.Database/Specifications/ValuesItem/ValueGreateSpecification.cs new file mode 100644 index 0000000..c0ee199 --- /dev/null +++ b/DD.Persistence.Database/Specifications/ValuesItem/ValueGreateSpecification.cs @@ -0,0 +1,22 @@ +using Ardalis.Specification; +using DD.Persistence.Database.EntityAbstractions; + +namespace DD.Persistence.Database.Specifications.ValuesItem; + +/// +/// Спецификация "больше" для значений IValuesItem в соответствии с индексацией +/// +/// +public class ValueGreateSpecification : Specification + where TEntity : IValuesItem +{ + public ValueGreateSpecification(int index, double? value) + { + Query.Where(e => Convert.ToDouble(e.Values[index]) > value); + } + + public ValueGreateSpecification(int index, DateTimeOffset? value) + { + Query.Where(e => DateTimeOffset.Parse(Convert.ToString(e.Values[index])!) > value); + } +} diff --git a/DD.Persistence.Database/Specifications/ValuesItem/ValueLessOrEqualSpecification.cs b/DD.Persistence.Database/Specifications/ValuesItem/ValueLessOrEqualSpecification.cs new file mode 100644 index 0000000..c749955 --- /dev/null +++ b/DD.Persistence.Database/Specifications/ValuesItem/ValueLessOrEqualSpecification.cs @@ -0,0 +1,22 @@ +using Ardalis.Specification; +using DD.Persistence.Database.EntityAbstractions; + +namespace DD.Persistence.Database.Specifications.ValuesItem; + +/// +/// Спецификация "меньше либо равно" для значений IValuesItem в соответствии с индексацией +/// +/// +public class ValueLessOrEqualSpecification : Specification + where TEntity : IValuesItem +{ + public ValueLessOrEqualSpecification(int index, double? value) + { + Query.Where(e => Convert.ToDouble(e.Values[index]) <= value); + } + + public ValueLessOrEqualSpecification(int index, DateTimeOffset? value) + { + Query.Where(e => DateTimeOffset.Parse(Convert.ToString(e.Values[index])!) <= value); + } +} diff --git a/DD.Persistence.Database/Specifications/ValuesItem/ValueLessSpecification.cs b/DD.Persistence.Database/Specifications/ValuesItem/ValueLessSpecification.cs new file mode 100644 index 0000000..ac29eb2 --- /dev/null +++ b/DD.Persistence.Database/Specifications/ValuesItem/ValueLessSpecification.cs @@ -0,0 +1,22 @@ +using Ardalis.Specification; +using DD.Persistence.Database.EntityAbstractions; + +namespace DD.Persistence.Database.Specifications.ValuesItem; + +/// +/// Спецификация "меньше" для значений IValuesItem в соответствии с индексацией +/// +/// +public class ValueLessSpecification : Specification + where TEntity : IValuesItem +{ + public ValueLessSpecification(int index, double? value) + { + Query.Where(e => Convert.ToDouble(e.Values[index]) < value); + } + + public ValueLessSpecification(int index, DateTimeOffset? value) + { + Query.Where(e => DateTimeOffset.Parse(Convert.ToString(e.Values[index])!) < value); + } +} diff --git a/DD.Persistence.Database/Specifications/ValuesItem/ValueNotEqaulSpecification.cs b/DD.Persistence.Database/Specifications/ValuesItem/ValueNotEqaulSpecification.cs new file mode 100644 index 0000000..bbe65d7 --- /dev/null +++ b/DD.Persistence.Database/Specifications/ValuesItem/ValueNotEqaulSpecification.cs @@ -0,0 +1,27 @@ +using Ardalis.Specification; +using DD.Persistence.Database.EntityAbstractions; + +namespace DD.Persistence.Database.Specifications.ValuesItem; + +/// +/// Спецификация неравенства значений IValuesItem в соответствии с индексацией +/// +/// +public class ValueNotEqaulSpecification : Specification + where TEntity : IValuesItem +{ + public ValueNotEqaulSpecification(int index, string? value) + { + Query.Where(e => Convert.ToString(e.Values[index]) != value); + } + + public ValueNotEqaulSpecification(int index, double? value) + { + Query.Where(e => Convert.ToDouble(e.Values[index]) != value); + } + + public ValueNotEqaulSpecification(int index, DateTimeOffset? value) + { + Query.Where(e => DateTimeOffset.Parse(Convert.ToString(e.Values[index])!) != value); + } +} diff --git a/DD.Persistence.Test/DD.Persistence.Test.csproj b/DD.Persistence.Test/DD.Persistence.Test.csproj index bd3079d..f71b186 100644 --- a/DD.Persistence.Test/DD.Persistence.Test.csproj +++ b/DD.Persistence.Test/DD.Persistence.Test.csproj @@ -16,8 +16,7 @@ - - + diff --git a/DD.Persistence.Test/FilterBuilderShould.cs b/DD.Persistence.Test/FilterBuilderShould.cs new file mode 100644 index 0000000..f32f09c --- /dev/null +++ b/DD.Persistence.Test/FilterBuilderShould.cs @@ -0,0 +1,250 @@ +using Ardalis.Specification.EntityFrameworkCore; +using DD.Persistence.Database.Entity; +using DD.Persistence.Filter.Models; +using DD.Persistence.Filter.Models.Enumerations; +using DD.Persistence.Models; +using DD.Persistence.Database.Postgres.Helpers; + +namespace DD.Persistence.Test; +public class FilterBuilderShould +{ + private readonly SpecificationEvaluator SpecificationEvaluator; + public FilterBuilderShould() + { + this.SpecificationEvaluator = new SpecificationEvaluator(); + } + + [Fact] + public void TestFilterBuilding() + { + //arrange + var discriminatorId = Guid.NewGuid(); + var dataScheme = new DataScheme() + { + DiscriminatorId = discriminatorId, + PropNames = ["A", "B", "C"], + PropTypes = [PropTypeEnum.DateTime, PropTypeEnum.Double, PropTypeEnum.String] + }; + var filterDate = DateTime.Now.AddMinutes(-1); + var root = new TVertex( + OperationEnum.Or, + new TVertex( + OperationEnum.And, + new TLeaf(OperationEnum.Greate, "A", filterDate), + new TLeaf(OperationEnum.Less, "B", 2.22) + ), + new TLeaf(OperationEnum.Equal, "C", "IsEqualText") + ); + var queryableData = new[] + { + new TimestampedValues { + DiscriminatorId = discriminatorId, + Timestamp = DateTimeOffset.Now.AddMinutes(-1), + Values = new object[] { filterDate.AddMinutes(-1), 200, "IsEqualText" }, // true + DataScheme = dataScheme + }, + new TimestampedValues { + DiscriminatorId = discriminatorId, + Timestamp = DateTimeOffset.Now.AddMinutes(-2), + Values = new object[] { filterDate.AddMinutes(1), 2.21, "IsNotEqualText" }, // true + DataScheme = dataScheme + }, + new TimestampedValues { + DiscriminatorId = discriminatorId, + Timestamp = DateTimeOffset.Now.AddMinutes(-3), + Values = new object[] { filterDate.AddMinutes(-1), 2.22, "IsNotEqualText" }, // false + DataScheme = dataScheme + }, + new TimestampedValues { + DiscriminatorId = discriminatorId, + Timestamp = DateTimeOffset.Now.AddMinutes(-4), + Values = new object[] { filterDate.AddMinutes(-1), 2.21, "IsNotEqualText" }, // false + DataScheme = dataScheme + } + } + .AsQueryable(); + + //act + var specification = dataScheme.BuildFilter(root); + + //assert + Assert.NotNull(specification); + + var query = SpecificationEvaluator.GetQuery(queryableData, specification); + var result = query.ToList(); + + Assert.NotNull(result); + Assert.NotEmpty(result); + + var expectedCount = 2; + var actualCount = result.Count(); + Assert.Equal(expectedCount, actualCount); + } + + [Fact] + public void TestFilterOperations() + { + //arrange + var discriminatorId = Guid.NewGuid(); + var dataScheme = new DataScheme() + { + DiscriminatorId = discriminatorId, + PropNames = ["A"], + PropTypes = [PropTypeEnum.Double] + }; + var root = new TVertex( + OperationEnum.Or, + new TVertex( + OperationEnum.And, + new TVertex( + OperationEnum.And, + new TVertex( + OperationEnum.And, + new TVertex( + OperationEnum.And, + new TLeaf(OperationEnum.Less, "A", 2), + new TLeaf(OperationEnum.LessOrEqual, "A", 1.99) + ), + new TLeaf(OperationEnum.GreateOrEqual, "A", 1.97) + ), + new TLeaf(OperationEnum.Greate, "A", 1.96) + ), + new TLeaf(OperationEnum.NotEqual, "A", 1.98) + ), + new TLeaf(OperationEnum.Equal, "A", 1) + ); + var queryableData = new[] + { + new TimestampedValues { + DiscriminatorId = discriminatorId, + Timestamp = DateTimeOffset.Now.AddMinutes(-1), + Values = new object[] { 1 }, // true + DataScheme = dataScheme + }, + new TimestampedValues { + DiscriminatorId = discriminatorId, + Timestamp = DateTimeOffset.Now.AddMinutes(-2), + Values = new object[] { 1.96 }, // false + DataScheme = dataScheme + }, + new TimestampedValues { + DiscriminatorId = discriminatorId, + Timestamp = DateTimeOffset.Now.AddMinutes(-3), + Values = new object[] { 1.97 }, // true + DataScheme = dataScheme + }, + new TimestampedValues { + DiscriminatorId = discriminatorId, + Timestamp = DateTimeOffset.Now.AddMinutes(-4), + Values = new object[] { 1.98 }, // false + DataScheme = dataScheme + }, + new TimestampedValues { + DiscriminatorId = discriminatorId, + Timestamp = DateTimeOffset.Now.AddMinutes(-5), + Values = new object[] { 1.99 }, // true + DataScheme = dataScheme + }, + new TimestampedValues { + DiscriminatorId = discriminatorId, + Timestamp = DateTimeOffset.Now.AddMinutes(-6), + Values = new object[] { 2 }, // false + DataScheme = dataScheme + } + } + .AsQueryable(); + + //act + var specification = dataScheme.BuildFilter(root); + + //assert + Assert.NotNull(specification); + + var query = SpecificationEvaluator.GetQuery(queryableData, specification); + var result = query.ToList(); + + Assert.NotNull(result); + Assert.NotEmpty(result); + + var expectedCount = 3; + var actualCount = result.Count(); + Assert.Equal(expectedCount, actualCount); + } + + [Fact] + public void TestFilterValues() + { + //arrange + var discriminatorId = Guid.NewGuid(); + var filterDate = DateTimeOffset.Now; + var dataScheme = new DataScheme() + { + DiscriminatorId = discriminatorId, + PropNames = ["A", "B", "C", "D"], + PropTypes = [PropTypeEnum.Double, PropTypeEnum.Double, PropTypeEnum.String, PropTypeEnum.DateTime] + }; + var root = new TVertex( + OperationEnum.Or, + new TVertex( + OperationEnum.Or, + new TVertex( + OperationEnum.Or, + new TLeaf(OperationEnum.Equal, "A", 1), + new TLeaf(OperationEnum.Equal, "B", 1.11) + ), + new TLeaf(OperationEnum.Equal, "C", "IsEqualText") + ), + new TLeaf(OperationEnum.Equal, "D", filterDate) + ); + var queryableData = new[] + { + new TimestampedValues { + DiscriminatorId = discriminatorId, + Timestamp = DateTimeOffset.Now.AddMinutes(-1), + Values = new object[] { 1, 2.22, "IsNotEqualText", DateTimeOffset.Now.AddMinutes(-1) }, // true + DataScheme = dataScheme + }, + new TimestampedValues { + DiscriminatorId = discriminatorId, + Timestamp = DateTimeOffset.Now.AddMinutes(-2), + Values = new object[] { 2, 1.11, "IsNotEqualText", DateTimeOffset.Now.AddMinutes(-1) }, // true + DataScheme = dataScheme + }, + new TimestampedValues { + DiscriminatorId = discriminatorId, + Timestamp = DateTimeOffset.Now.AddMinutes(-3), + Values = new object[] { 2, 2.22, "IsEqualText", DateTimeOffset.Now.AddMinutes(-1) }, // true + DataScheme = dataScheme + }, + new TimestampedValues { + DiscriminatorId = discriminatorId, + Timestamp = DateTimeOffset.Now.AddMinutes(-4), + Values = new object[] { 2, 2.22, "IsNotEqualText", filterDate }, // true + DataScheme = dataScheme + }, + new TimestampedValues { + DiscriminatorId = discriminatorId, + Timestamp = DateTimeOffset.Now.AddMinutes(-1), + Values = new object[] { 2, 2.22, "IsNotEqualText", DateTimeOffset.Now.AddMinutes(-1) }, // false + DataScheme = dataScheme + } + } + .AsQueryable(); + + //act + var specification = dataScheme.BuildFilter(root); + + //assert + Assert.NotNull(specification); + + var query = SpecificationEvaluator.GetQuery(queryableData, specification); + var result = query.ToList(); + + Assert.NotNull(result); + Assert.NotEmpty(result); + + var expectedCount = 4; + var actualCount = result.Count(); + Assert.Equal(expectedCount, actualCount); + } +} -- 2.45.2 From bcb9749b1a36114bd60a1ff95448780873d20e89 Mon Sep 17 00:00:00 2001 From: Roman Efremov Date: Thu, 6 Feb 2025 12:32:28 +0500 Subject: [PATCH 2/2] =?UTF-8?q?=D0=9F=D1=80=D0=B0=D0=B2=D0=BA=D0=B8=20?= =?UTF-8?q?=D0=BF=D0=BE=20=D1=80=D0=B5=D0=B2=D1=8C=D1=8E=20#4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Extensions/SpecificationExtensions.cs | 83 ----------- .../Helpers/FilterBuilder.cs | 137 ------------------ .../DD.Persistence.Database.csproj | 2 + .../EntityAbstractions/IValuesItem.cs | 5 - .../Extensions/SpecificationExtensions.cs | 38 +++++ .../Helpers/FilterBuilder.cs | 106 ++++++++++++++ .../Helpers/ParameterReplacerVisitor.cs | 20 +++ .../Helpers/QueryBuilders.cs | 1 - .../Specifications/AndSpecification.cs | 22 +++ .../Specifications/EmptySpecification.cs | 14 -- .../Specifications/OrSpecification.cs | 15 ++ .../ValuesItem/ValueEqaulSpecification.cs | 6 - .../ValueGreateOrEqualSpecification.cs | 10 +- .../ValuesItem/ValueGreateSpecification.cs | 10 +- .../ValueLessOrEqualSpecification.cs | 10 +- .../ValuesItem/ValueLessSpecification.cs | 10 +- ...ation.cs => ValueNotEqualSpecification.cs} | 11 +- DD.Persistence.Test/FilterBuilderShould.cs | 115 +++++++++------ .../TimestampedValuesServiceShould.cs | 16 +- 19 files changed, 311 insertions(+), 320 deletions(-) delete mode 100644 DD.Persistence.Database.Postgres/Extensions/SpecificationExtensions.cs delete mode 100644 DD.Persistence.Database.Postgres/Helpers/FilterBuilder.cs create mode 100644 DD.Persistence.Database/Extensions/SpecificationExtensions.cs create mode 100644 DD.Persistence.Database/Helpers/FilterBuilder.cs create mode 100644 DD.Persistence.Database/Helpers/ParameterReplacerVisitor.cs create mode 100644 DD.Persistence.Database/Specifications/AndSpecification.cs delete mode 100644 DD.Persistence.Database/Specifications/EmptySpecification.cs create mode 100644 DD.Persistence.Database/Specifications/OrSpecification.cs rename DD.Persistence.Database/Specifications/ValuesItem/{ValueNotEqaulSpecification.cs => ValueNotEqualSpecification.cs} (60%) diff --git a/DD.Persistence.Database.Postgres/Extensions/SpecificationExtensions.cs b/DD.Persistence.Database.Postgres/Extensions/SpecificationExtensions.cs deleted file mode 100644 index 2c13c10..0000000 --- a/DD.Persistence.Database.Postgres/Extensions/SpecificationExtensions.cs +++ /dev/null @@ -1,83 +0,0 @@ -using Ardalis.Specification; -using DD.Persistence.Database.Specifications; -using System.Linq.Expressions; - -namespace DD.Persistence.Database.Postgres.Extensions; - -// ToDo: рассмотреть возможность вынести логику в спецификации -public static class SpecificationExtensions -{ - public static ISpecification Or(this ISpecification spec, ISpecification otherSpec) - { - var newSpec = new EmptySpecification(); - - var parameter = Expression.Parameter(typeof(T), "x"); - - var exprSpec1 = CombineWhereExpressions(spec.WhereExpressions, parameter); - var exprSpec2 = CombineWhereExpressions(otherSpec.WhereExpressions, parameter); - - Expression? orExpression = exprSpec1 is not null && exprSpec2 is not null - ? Expression.OrElse(exprSpec1, exprSpec2) - : exprSpec1 ?? exprSpec2; - - if (orExpression is null) return newSpec; - - var lambdaExpr = Expression.Lambda>(orExpression, parameter); - - newSpec.Query.Where(lambdaExpr); - - return newSpec; - } - - // ToDo: Рефакторинг - public static ISpecification And(this ISpecification spec, ISpecification otherSpec) - { - var newSpec = new EmptySpecification(); - - var parameter = Expression.Parameter(typeof(T), "x"); - - var exprSpec1 = CombineWhereExpressions(spec.WhereExpressions, parameter); - var exprSpec2 = CombineWhereExpressions(otherSpec.WhereExpressions, parameter); - - Expression? andExpression = exprSpec1 is not null && exprSpec2 is not null - ? Expression.AndAlso(exprSpec1, exprSpec2) - : exprSpec1 ?? exprSpec2; - - if (andExpression is null) return newSpec; - - var lambdaExpr = Expression.Lambda>(andExpression, parameter); - - newSpec.Query.Where(lambdaExpr); - - return newSpec; - } - - public static Expression? CombineWhereExpressions(IEnumerable> whereExpressions, ParameterExpression parameter) - { - Expression? newExpr = null; - foreach (var where in whereExpressions) - { - var expr = ParameterReplacerVisitor.Replace(where.Filter.Body, where.Filter.Parameters[0], parameter); - newExpr = newExpr is null ? expr : Expression.AndAlso(newExpr, expr); - } - return newExpr; - } -} - -public class ParameterReplacerVisitor : ExpressionVisitor -{ - private readonly Expression _newExpression; - private readonly ParameterExpression _oldParameter; - - private ParameterReplacerVisitor(ParameterExpression oldParameter, Expression newExpression) - { - _oldParameter = oldParameter; - _newExpression = newExpression; - } - - internal static Expression Replace(Expression expression, ParameterExpression oldParameter, Expression newExpression) - => new ParameterReplacerVisitor(oldParameter, newExpression).Visit(expression); - - protected override Expression VisitParameter(ParameterExpression p) - => p == _oldParameter ? _newExpression : p; -} diff --git a/DD.Persistence.Database.Postgres/Helpers/FilterBuilder.cs b/DD.Persistence.Database.Postgres/Helpers/FilterBuilder.cs deleted file mode 100644 index e8a7f57..0000000 --- a/DD.Persistence.Database.Postgres/Helpers/FilterBuilder.cs +++ /dev/null @@ -1,137 +0,0 @@ -using Ardalis.Specification; -using DD.Persistence.Database.Entity; -using DD.Persistence.Database.EntityAbstractions; -using DD.Persistence.Database.Postgres.Extensions; -using DD.Persistence.Database.Specifications.ValuesItem; -using DD.Persistence.Filter.Models.Abstractions; -using DD.Persistence.Filter.Models.Enumerations; -using DD.Persistence.Filter.Visitors; -using DD.Persistence.Models; - -namespace DD.Persistence.Database.Postgres.Helpers; -public static class FilterBuilder -{ - public static ISpecification? BuildFilter(this DataScheme dataScheme, TNode root) - where TEntity : IValuesItem - { - var result = dataScheme.BuildSpecificationByNextNode(root); - - return result; - } - - private static ISpecification? BuildSpecificationByNextNode(this DataScheme dataScheme, TNode node) - where TEntity : IValuesItem - { - var propIndexMap = dataScheme.PropNames - .Select((name, index) => new { name, index }) - .ToDictionary(x => x.name, x => x.index); - - var visitor = new NodeVisitor?>( - v => - { - var leftSpecification = dataScheme.BuildSpecificationByNextNode(v.Left); - var rigthSpecification = dataScheme.BuildSpecificationByNextNode(v.Rigth); - if (leftSpecification is null) - return rigthSpecification; - if (rigthSpecification is null) - return leftSpecification; - - ISpecification? result = null; - switch (v.Operation) - { - case OperationEnum.And: - result = leftSpecification.And(rigthSpecification); - break; - case OperationEnum.Or: - result = leftSpecification.Or(rigthSpecification); - break; - } - - return result; - }, - t => - { - int keyIndex; - if (!propIndexMap.TryGetValue(t.PropName, out keyIndex)) - throw new ArgumentException($"Свойство {t.PropName} не найдено в схеме данных"); - var type = dataScheme.PropTypes[keyIndex]; - - ISpecification? result = null; - switch (type) - { - case PropTypeEnum.String: - var stringValue = Convert.ToString(t.Value); - switch (t.Operation) - { - case OperationEnum.Equal: - result = new ValueEqaulSpecification(keyIndex, stringValue); - break; - case OperationEnum.NotEqual: - result = new ValueNotEqaulSpecification(keyIndex, stringValue); - break; - } - break; - case PropTypeEnum.Double: - var doubleValue = Convert.ToDouble(t.Value); - switch (t.Operation) - // ToDo: можно схлопнуть в один Generic - метод, где TValue : struct - // Но в таком случае придётся продумать аналогичное решение на уровне спецификаций - { - case OperationEnum.Equal: - result = new ValueEqaulSpecification(keyIndex, doubleValue); - break; - case OperationEnum.NotEqual: - result = new ValueNotEqaulSpecification(keyIndex, doubleValue); - break; - case OperationEnum.Greate: - result = new ValueGreateSpecification(keyIndex, doubleValue); - break; - case OperationEnum.GreateOrEqual: - result = new ValueGreateOrEqualSpecification(keyIndex, doubleValue); - break; - case OperationEnum.Less: - result = new ValueLessSpecification(keyIndex, doubleValue); - break; - case OperationEnum.LessOrEqual: - result = new ValueLessOrEqualSpecification(keyIndex, doubleValue); - break; - } - break; - case PropTypeEnum.DateTime: - stringValue = Convert.ToString(t.Value); - DateTimeOffset? dateTimeValue = string.IsNullOrEmpty(stringValue) - ? null - : DateTimeOffset.Parse(stringValue); - switch (t.Operation) - { - case OperationEnum.Equal: - result = new ValueEqaulSpecification(keyIndex, dateTimeValue); - break; - case OperationEnum.NotEqual: - result = new ValueNotEqaulSpecification(keyIndex, dateTimeValue); - break; - case OperationEnum.Greate: - result = new ValueGreateSpecification(keyIndex, dateTimeValue); - break; - case OperationEnum.GreateOrEqual: - result = new ValueGreateOrEqualSpecification(keyIndex, dateTimeValue); - break; - case OperationEnum.Less: - result = new ValueLessSpecification(keyIndex, dateTimeValue); - break; - case OperationEnum.LessOrEqual: - result = new ValueLessOrEqualSpecification(keyIndex, dateTimeValue); - break; - } - break; - } - - return result; - } - ); - - var result = node.AcceptVisitor(visitor); - - return result; - } -} diff --git a/DD.Persistence.Database/DD.Persistence.Database.csproj b/DD.Persistence.Database/DD.Persistence.Database.csproj index 4d751e4..5a34ccc 100644 --- a/DD.Persistence.Database/DD.Persistence.Database.csproj +++ b/DD.Persistence.Database/DD.Persistence.Database.csproj @@ -7,6 +7,8 @@ + + all diff --git a/DD.Persistence.Database/EntityAbstractions/IValuesItem.cs b/DD.Persistence.Database/EntityAbstractions/IValuesItem.cs index f2a801c..0f2d219 100644 --- a/DD.Persistence.Database/EntityAbstractions/IValuesItem.cs +++ b/DD.Persistence.Database/EntityAbstractions/IValuesItem.cs @@ -7,11 +7,6 @@ namespace DD.Persistence.Database.EntityAbstractions; /// public interface IValuesItem { - /// - /// Схема данных - /// - DataScheme? DataScheme { get; set; } - /// /// Значения /// diff --git a/DD.Persistence.Database/Extensions/SpecificationExtensions.cs b/DD.Persistence.Database/Extensions/SpecificationExtensions.cs new file mode 100644 index 0000000..3bb0bc3 --- /dev/null +++ b/DD.Persistence.Database/Extensions/SpecificationExtensions.cs @@ -0,0 +1,38 @@ +using Ardalis.Specification; +using DD.Persistence.Database.Helpers; +using System.Linq.Expressions; + +namespace DD.Persistence.Database.Postgres.Extensions; + +public static class SpecificationExtensions +{ + public static Expression>? Or(this ISpecification spec, ISpecification otherSpec) + { + var parameter = Expression.Parameter(typeof(T), "x"); + + var exprSpec1 = CombineWhereExpressions(spec.WhereExpressions, parameter); + var exprSpec2 = CombineWhereExpressions(otherSpec.WhereExpressions, parameter); + + Expression? orExpression = exprSpec1 is not null && exprSpec2 is not null + ? Expression.OrElse(exprSpec1, exprSpec2) + : exprSpec1 ?? exprSpec2; + + if (orExpression is null) + return null; + + var lambdaExpr = Expression.Lambda>(orExpression, parameter); + + return lambdaExpr; + } + + public static Expression? CombineWhereExpressions(IEnumerable> whereExpressions, ParameterExpression parameter) + { + Expression? newExpr = null; + foreach (var where in whereExpressions) + { + var expr = ParameterReplacerVisitor.Replace(where.Filter.Body, where.Filter.Parameters[0], parameter); + newExpr = newExpr is null ? expr : Expression.AndAlso(newExpr, expr); + } + return newExpr; + } +} diff --git a/DD.Persistence.Database/Helpers/FilterBuilder.cs b/DD.Persistence.Database/Helpers/FilterBuilder.cs new file mode 100644 index 0000000..2d90284 --- /dev/null +++ b/DD.Persistence.Database/Helpers/FilterBuilder.cs @@ -0,0 +1,106 @@ +using Ardalis.Specification; +using DD.Persistence.Database.EntityAbstractions; +using DD.Persistence.Database.Postgres.Extensions; +using DD.Persistence.Database.Specifications; +using DD.Persistence.Database.Specifications.ValuesItem; +using DD.Persistence.Filter.Models; +using DD.Persistence.Filter.Models.Abstractions; +using DD.Persistence.Filter.Models.Enumerations; +using DD.Persistence.Filter.Visitors; +using DD.Persistence.Models; +using System.Text.Json; + +namespace DD.Persistence.Database.Postgres.Helpers; +public static class FilterBuilder +{ + public static ISpecification? BuildFilter(this DataSchemeDto dataSchemeDto, TNode root) + where TEntity : IValuesItem + { + var result = dataSchemeDto.BuildSpecificationByNextNode(root); + + return result; + } + + private static ISpecification? BuildSpecificationByNextNode(this DataSchemeDto dataSchemeDto, TNode node) + where TEntity : IValuesItem + { + var visitor = new NodeVisitor?>( + dataSchemeDto.VertexProcessing, + dataSchemeDto.LeafProcessing + ); + + var result = node.AcceptVisitor(visitor); + + return result; + } + + private static ISpecification? VertexProcessing(this DataSchemeDto dataSchemeDto, TVertex vertex) + where TEntity : IValuesItem + { + var leftSpecification = dataSchemeDto.BuildSpecificationByNextNode(vertex.Left); + var rigthSpecification = dataSchemeDto.BuildSpecificationByNextNode(vertex.Rigth); + if (leftSpecification is null) + return rigthSpecification; + if (rigthSpecification is null) + return leftSpecification; + + ISpecification? result = null; + switch (vertex.Operation) + { + case OperationEnum.And: + result = new AndSpecification(leftSpecification, rigthSpecification); + break; + case OperationEnum.Or: + result = new OrSpecification(leftSpecification, rigthSpecification); + break; + } + + return result; + } + + private static ISpecification? LeafProcessing(this DataSchemeDto dataSchemeDto, TLeaf leaf) + where TEntity : IValuesItem + { + var schemeProperty = dataSchemeDto.FirstOrDefault(e => e.PropertyName.Equals(leaf.PropName)); + if (schemeProperty is null) + throw new ArgumentException($"Свойство {leaf.PropName} не найдено в схеме данных"); + + ISpecification? result = null; + switch (schemeProperty.PropertyKind) + { + case JsonValueKind.String: + var stringValue = Convert.ToString(leaf.Value); + var stringSpecifications = StringSpecifications(); + result = stringSpecifications[leaf.Operation](schemeProperty.Index, stringValue); + break; + case JsonValueKind.Number: + var doubleValue = Convert.ToDouble(leaf.Value); + var doubleSpecifications = DoubleSpecifications(); + result = doubleSpecifications[leaf.Operation](schemeProperty.Index, doubleValue); + break; + } + + return result; + } + + private static Dictionary>> StringSpecifications() + where TEntity : IValuesItem => new() + { + { OperationEnum.Equal, (int index, string? value) => new ValueEqaulSpecification(index, value) }, + { OperationEnum.NotEqual, (int index, string? value) => new ValueNotEqualSpecification(index, value) }, + { OperationEnum.Greate, (int index, string? value) => new ValueGreateSpecification(index, value) }, + { OperationEnum.GreateOrEqual, (int index, string? value) => new ValueGreateOrEqualSpecification(index, value) }, + { OperationEnum.Less, (int index, string? value) => new ValueLessSpecification(index, value) }, + { OperationEnum.LessOrEqual, (int index, string? value) => new ValueLessOrEqualSpecification(index, value) } + }; + private static Dictionary>> DoubleSpecifications() + where TEntity : IValuesItem => new() + { + { OperationEnum.Equal, (int index, double? value) => new ValueEqaulSpecification(index, value) }, + { OperationEnum.NotEqual, (int index, double? value) => new ValueNotEqualSpecification(index, value) }, + { OperationEnum.Greate, (int index, double? value) => new ValueGreateSpecification(index, value) }, + { OperationEnum.GreateOrEqual, (int index, double? value) => new ValueGreateOrEqualSpecification(index, value) }, + { OperationEnum.Less, (int index, double? value) => new ValueLessSpecification(index, value) }, + { OperationEnum.LessOrEqual, (int index, double? value) => new ValueLessOrEqualSpecification(index, value) } + }; +} diff --git a/DD.Persistence.Database/Helpers/ParameterReplacerVisitor.cs b/DD.Persistence.Database/Helpers/ParameterReplacerVisitor.cs new file mode 100644 index 0000000..d64ac11 --- /dev/null +++ b/DD.Persistence.Database/Helpers/ParameterReplacerVisitor.cs @@ -0,0 +1,20 @@ +using System.Linq.Expressions; + +namespace DD.Persistence.Database.Helpers; +public class ParameterReplacerVisitor : ExpressionVisitor +{ + private readonly Expression _newExpression; + private readonly ParameterExpression _oldParameter; + + private ParameterReplacerVisitor(ParameterExpression oldParameter, Expression newExpression) + { + _oldParameter = oldParameter; + _newExpression = newExpression; + } + + internal static Expression Replace(Expression expression, ParameterExpression oldParameter, Expression newExpression) + => new ParameterReplacerVisitor(oldParameter, newExpression).Visit(expression); + + protected override Expression VisitParameter(ParameterExpression p) + => p == _oldParameter ? _newExpression : p; +} diff --git a/DD.Persistence.Database/Helpers/QueryBuilders.cs b/DD.Persistence.Database/Helpers/QueryBuilders.cs index 8529230..64783b0 100644 --- a/DD.Persistence.Database/Helpers/QueryBuilders.cs +++ b/DD.Persistence.Database/Helpers/QueryBuilders.cs @@ -23,7 +23,6 @@ public static class QueryBuilders return query; } - public static async Task> ApplyPagination( this IQueryable query, PaginationRequest request, diff --git a/DD.Persistence.Database/Specifications/AndSpecification.cs b/DD.Persistence.Database/Specifications/AndSpecification.cs new file mode 100644 index 0000000..776ce3a --- /dev/null +++ b/DD.Persistence.Database/Specifications/AndSpecification.cs @@ -0,0 +1,22 @@ +using Ardalis.Specification; + +namespace DD.Persistence.Database.Specifications; +public class AndSpecification : Specification +{ + public AndSpecification(ISpecification first, ISpecification second) + { + if (first is null || second is null) + return; + + ApplyCriteria(first); + ApplyCriteria(second); + } + + private void ApplyCriteria(ISpecification specification) + { + foreach (var criteria in specification.WhereExpressions) + { + Query.Where(criteria.Filter); + } + } +} diff --git a/DD.Persistence.Database/Specifications/EmptySpecification.cs b/DD.Persistence.Database/Specifications/EmptySpecification.cs deleted file mode 100644 index 010d0a9..0000000 --- a/DD.Persistence.Database/Specifications/EmptySpecification.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Ardalis.Specification; - -namespace DD.Persistence.Database.Specifications; - -/// -/// Пустая спецификация (вспомогательная) -/// -/// -public class EmptySpecification : Specification -{ - public EmptySpecification() - { - } -} \ No newline at end of file diff --git a/DD.Persistence.Database/Specifications/OrSpecification.cs b/DD.Persistence.Database/Specifications/OrSpecification.cs new file mode 100644 index 0000000..75e573e --- /dev/null +++ b/DD.Persistence.Database/Specifications/OrSpecification.cs @@ -0,0 +1,15 @@ +using Ardalis.Specification; +using DD.Persistence.Database.Postgres.Extensions; + +namespace DD.Persistence.Database.Specifications; +public class OrSpecification : Specification +{ + public OrSpecification(ISpecification first, ISpecification second) + { + var orExpression = first.Or(second); + if (orExpression == null) + return; + + Query.Where(orExpression); + } +} diff --git a/DD.Persistence.Database/Specifications/ValuesItem/ValueEqaulSpecification.cs b/DD.Persistence.Database/Specifications/ValuesItem/ValueEqaulSpecification.cs index f5fd385..f85153f 100644 --- a/DD.Persistence.Database/Specifications/ValuesItem/ValueEqaulSpecification.cs +++ b/DD.Persistence.Database/Specifications/ValuesItem/ValueEqaulSpecification.cs @@ -19,10 +19,4 @@ public class ValueEqaulSpecification : Specification { Query.Where(e => Convert.ToDouble(e.Values[index]) == value); } - - public ValueEqaulSpecification(int index, DateTimeOffset? value) - { - // ToDo: рассмотреть возможность более вменяемого парсинга для даты - Query.Where(e => DateTimeOffset.Parse(Convert.ToString(e.Values[index])!) == value); - } } diff --git a/DD.Persistence.Database/Specifications/ValuesItem/ValueGreateOrEqualSpecification.cs b/DD.Persistence.Database/Specifications/ValuesItem/ValueGreateOrEqualSpecification.cs index ada97ad..695bffe 100644 --- a/DD.Persistence.Database/Specifications/ValuesItem/ValueGreateOrEqualSpecification.cs +++ b/DD.Persistence.Database/Specifications/ValuesItem/ValueGreateOrEqualSpecification.cs @@ -10,13 +10,13 @@ namespace DD.Persistence.Database.Specifications.ValuesItem; public class ValueGreateOrEqualSpecification : Specification where TEntity : IValuesItem { + public ValueGreateOrEqualSpecification(int index, string? value) + { + Query.Where(e => string.Compare(Convert.ToString(e.Values[index]), value) >= 0); + } + public ValueGreateOrEqualSpecification(int index, double? value) { Query.Where(e => Convert.ToDouble(e.Values[index]) >= value); } - - public ValueGreateOrEqualSpecification(int index, DateTimeOffset? value) - { - Query.Where(e => DateTimeOffset.Parse(Convert.ToString(e.Values[index])!) >= value); - } } diff --git a/DD.Persistence.Database/Specifications/ValuesItem/ValueGreateSpecification.cs b/DD.Persistence.Database/Specifications/ValuesItem/ValueGreateSpecification.cs index c0ee199..f42a7e2 100644 --- a/DD.Persistence.Database/Specifications/ValuesItem/ValueGreateSpecification.cs +++ b/DD.Persistence.Database/Specifications/ValuesItem/ValueGreateSpecification.cs @@ -10,13 +10,13 @@ namespace DD.Persistence.Database.Specifications.ValuesItem; public class ValueGreateSpecification : Specification where TEntity : IValuesItem { + public ValueGreateSpecification(int index, string? value) + { + Query.Where(e => string.Compare(Convert.ToString(e.Values[index]), value) > 0); + } + public ValueGreateSpecification(int index, double? value) { Query.Where(e => Convert.ToDouble(e.Values[index]) > value); } - - public ValueGreateSpecification(int index, DateTimeOffset? value) - { - Query.Where(e => DateTimeOffset.Parse(Convert.ToString(e.Values[index])!) > value); - } } diff --git a/DD.Persistence.Database/Specifications/ValuesItem/ValueLessOrEqualSpecification.cs b/DD.Persistence.Database/Specifications/ValuesItem/ValueLessOrEqualSpecification.cs index c749955..81709f0 100644 --- a/DD.Persistence.Database/Specifications/ValuesItem/ValueLessOrEqualSpecification.cs +++ b/DD.Persistence.Database/Specifications/ValuesItem/ValueLessOrEqualSpecification.cs @@ -10,13 +10,13 @@ namespace DD.Persistence.Database.Specifications.ValuesItem; public class ValueLessOrEqualSpecification : Specification where TEntity : IValuesItem { + public ValueLessOrEqualSpecification(int index, string? value) + { + Query.Where(e => string.Compare(Convert.ToString(e.Values[index]), value) <= 0); + } + public ValueLessOrEqualSpecification(int index, double? value) { Query.Where(e => Convert.ToDouble(e.Values[index]) <= value); } - - public ValueLessOrEqualSpecification(int index, DateTimeOffset? value) - { - Query.Where(e => DateTimeOffset.Parse(Convert.ToString(e.Values[index])!) <= value); - } } diff --git a/DD.Persistence.Database/Specifications/ValuesItem/ValueLessSpecification.cs b/DD.Persistence.Database/Specifications/ValuesItem/ValueLessSpecification.cs index ac29eb2..4c308d4 100644 --- a/DD.Persistence.Database/Specifications/ValuesItem/ValueLessSpecification.cs +++ b/DD.Persistence.Database/Specifications/ValuesItem/ValueLessSpecification.cs @@ -10,13 +10,13 @@ namespace DD.Persistence.Database.Specifications.ValuesItem; public class ValueLessSpecification : Specification where TEntity : IValuesItem { + public ValueLessSpecification(int index, string? value) + { + Query.Where(e => string.Compare(Convert.ToString(e.Values[index]), value) < 0); + } + public ValueLessSpecification(int index, double? value) { Query.Where(e => Convert.ToDouble(e.Values[index]) < value); } - - public ValueLessSpecification(int index, DateTimeOffset? value) - { - Query.Where(e => DateTimeOffset.Parse(Convert.ToString(e.Values[index])!) < value); - } } diff --git a/DD.Persistence.Database/Specifications/ValuesItem/ValueNotEqaulSpecification.cs b/DD.Persistence.Database/Specifications/ValuesItem/ValueNotEqualSpecification.cs similarity index 60% rename from DD.Persistence.Database/Specifications/ValuesItem/ValueNotEqaulSpecification.cs rename to DD.Persistence.Database/Specifications/ValuesItem/ValueNotEqualSpecification.cs index bbe65d7..668d415 100644 --- a/DD.Persistence.Database/Specifications/ValuesItem/ValueNotEqaulSpecification.cs +++ b/DD.Persistence.Database/Specifications/ValuesItem/ValueNotEqualSpecification.cs @@ -7,21 +7,16 @@ namespace DD.Persistence.Database.Specifications.ValuesItem; /// Спецификация неравенства значений IValuesItem в соответствии с индексацией /// /// -public class ValueNotEqaulSpecification : Specification +public class ValueNotEqualSpecification : Specification where TEntity : IValuesItem { - public ValueNotEqaulSpecification(int index, string? value) + public ValueNotEqualSpecification(int index, string? value) { Query.Where(e => Convert.ToString(e.Values[index]) != value); } - public ValueNotEqaulSpecification(int index, double? value) + public ValueNotEqualSpecification(int index, double? value) { Query.Where(e => Convert.ToDouble(e.Values[index]) != value); } - - public ValueNotEqaulSpecification(int index, DateTimeOffset? value) - { - Query.Where(e => DateTimeOffset.Parse(Convert.ToString(e.Values[index])!) != value); - } } diff --git a/DD.Persistence.Test/FilterBuilderShould.cs b/DD.Persistence.Test/FilterBuilderShould.cs index f32f09c..09735c3 100644 --- a/DD.Persistence.Test/FilterBuilderShould.cs +++ b/DD.Persistence.Test/FilterBuilderShould.cs @@ -4,8 +4,11 @@ using DD.Persistence.Filter.Models; using DD.Persistence.Filter.Models.Enumerations; using DD.Persistence.Models; using DD.Persistence.Database.Postgres.Helpers; +using System.Text.Json; namespace DD.Persistence.Test; + +/// ToDo: переписать под Theory public class FilterBuilderShould { private readonly SpecificationEvaluator SpecificationEvaluator; @@ -19,12 +22,28 @@ public class FilterBuilderShould { //arrange var discriminatorId = Guid.NewGuid(); - var dataScheme = new DataScheme() + var dataSchemeProperties = new SchemePropertyDto[] { - DiscriminatorId = discriminatorId, - PropNames = ["A", "B", "C"], - PropTypes = [PropTypeEnum.DateTime, PropTypeEnum.Double, PropTypeEnum.String] + new SchemePropertyDto() + { + Index = 0, + PropertyName = "A", + PropertyKind = JsonValueKind.String + }, + new SchemePropertyDto() + { + Index = 1, + PropertyName = "B", + PropertyKind = JsonValueKind.Number + }, + new SchemePropertyDto() + { + Index = 2, + PropertyName = "C", + PropertyKind = JsonValueKind.String + } }; + var dataScheme = new DataSchemeDto(discriminatorId, dataSchemeProperties); var filterDate = DateTime.Now.AddMinutes(-1); var root = new TVertex( OperationEnum.Or, @@ -40,26 +59,22 @@ public class FilterBuilderShould new TimestampedValues { DiscriminatorId = discriminatorId, Timestamp = DateTimeOffset.Now.AddMinutes(-1), - Values = new object[] { filterDate.AddMinutes(-1), 200, "IsEqualText" }, // true - DataScheme = dataScheme + Values = new object[] { filterDate.AddMinutes(-1), 200, "IsEqualText" } // true }, new TimestampedValues { DiscriminatorId = discriminatorId, Timestamp = DateTimeOffset.Now.AddMinutes(-2), - Values = new object[] { filterDate.AddMinutes(1), 2.21, "IsNotEqualText" }, // true - DataScheme = dataScheme + Values = new object[] { filterDate.AddMinutes(1), 2.21, "IsNotEqualText" } // true }, new TimestampedValues { DiscriminatorId = discriminatorId, Timestamp = DateTimeOffset.Now.AddMinutes(-3), - Values = new object[] { filterDate.AddMinutes(-1), 2.22, "IsNotEqualText" }, // false - DataScheme = dataScheme + Values = new object[] { filterDate.AddMinutes(-1), 2.22, "IsNotEqualText" } // false }, new TimestampedValues { DiscriminatorId = discriminatorId, Timestamp = DateTimeOffset.Now.AddMinutes(-4), - Values = new object[] { filterDate.AddMinutes(-1), 2.21, "IsNotEqualText" }, // false - DataScheme = dataScheme + Values = new object[] { filterDate.AddMinutes(-1), 2.21, "IsNotEqualText" } // false } } .AsQueryable(); @@ -86,12 +101,16 @@ public class FilterBuilderShould { //arrange var discriminatorId = Guid.NewGuid(); - var dataScheme = new DataScheme() + var dataSchemeProperties = new SchemePropertyDto[] { - DiscriminatorId = discriminatorId, - PropNames = ["A"], - PropTypes = [PropTypeEnum.Double] + new SchemePropertyDto() + { + Index = 0, + PropertyName = "A", + PropertyKind = JsonValueKind.Number + } }; + var dataScheme = new DataSchemeDto(discriminatorId, dataSchemeProperties); var root = new TVertex( OperationEnum.Or, new TVertex( @@ -118,38 +137,32 @@ public class FilterBuilderShould new TimestampedValues { DiscriminatorId = discriminatorId, Timestamp = DateTimeOffset.Now.AddMinutes(-1), - Values = new object[] { 1 }, // true - DataScheme = dataScheme + Values = new object[] { 1 } // true }, new TimestampedValues { DiscriminatorId = discriminatorId, Timestamp = DateTimeOffset.Now.AddMinutes(-2), - Values = new object[] { 1.96 }, // false - DataScheme = dataScheme + Values = new object[] { 1.96 } // false }, new TimestampedValues { DiscriminatorId = discriminatorId, Timestamp = DateTimeOffset.Now.AddMinutes(-3), - Values = new object[] { 1.97 }, // true - DataScheme = dataScheme + Values = new object[] { 1.97 } // true }, new TimestampedValues { DiscriminatorId = discriminatorId, Timestamp = DateTimeOffset.Now.AddMinutes(-4), - Values = new object[] { 1.98 }, // false - DataScheme = dataScheme + Values = new object[] { 1.98 } // false }, new TimestampedValues { DiscriminatorId = discriminatorId, Timestamp = DateTimeOffset.Now.AddMinutes(-5), - Values = new object[] { 1.99 }, // true - DataScheme = dataScheme + Values = new object[] { 1.99 } // true }, new TimestampedValues { DiscriminatorId = discriminatorId, Timestamp = DateTimeOffset.Now.AddMinutes(-6), - Values = new object[] { 2 }, // false - DataScheme = dataScheme + Values = new object[] { 2 } // false } } .AsQueryable(); @@ -177,12 +190,35 @@ public class FilterBuilderShould //arrange var discriminatorId = Guid.NewGuid(); var filterDate = DateTimeOffset.Now; - var dataScheme = new DataScheme() + var dataSchemeProperties = new SchemePropertyDto[] { - DiscriminatorId = discriminatorId, - PropNames = ["A", "B", "C", "D"], - PropTypes = [PropTypeEnum.Double, PropTypeEnum.Double, PropTypeEnum.String, PropTypeEnum.DateTime] + new SchemePropertyDto() + { + Index = 0, + PropertyName = "A", + PropertyKind = JsonValueKind.Number + }, + new SchemePropertyDto() + { + Index = 1, + PropertyName = "B", + PropertyKind = JsonValueKind.Number + }, + new SchemePropertyDto() + { + Index = 2, + PropertyName = "C", + PropertyKind = JsonValueKind.String + }, + new SchemePropertyDto() + { + Index = 3, + PropertyName = "D", + PropertyKind = JsonValueKind.String + } }; + var dataScheme = new DataSchemeDto(discriminatorId, dataSchemeProperties); + var root = new TVertex( OperationEnum.Or, new TVertex( @@ -201,32 +237,27 @@ public class FilterBuilderShould new TimestampedValues { DiscriminatorId = discriminatorId, Timestamp = DateTimeOffset.Now.AddMinutes(-1), - Values = new object[] { 1, 2.22, "IsNotEqualText", DateTimeOffset.Now.AddMinutes(-1) }, // true - DataScheme = dataScheme + Values = new object[] { 1, 2.22, "IsNotEqualText", DateTimeOffset.Now.AddMinutes(-1) } // true }, new TimestampedValues { DiscriminatorId = discriminatorId, Timestamp = DateTimeOffset.Now.AddMinutes(-2), - Values = new object[] { 2, 1.11, "IsNotEqualText", DateTimeOffset.Now.AddMinutes(-1) }, // true - DataScheme = dataScheme + Values = new object[] { 2, 1.11, "IsNotEqualText", DateTimeOffset.Now.AddMinutes(-1) } // true }, new TimestampedValues { DiscriminatorId = discriminatorId, Timestamp = DateTimeOffset.Now.AddMinutes(-3), - Values = new object[] { 2, 2.22, "IsEqualText", DateTimeOffset.Now.AddMinutes(-1) }, // true - DataScheme = dataScheme + Values = new object[] { 2, 2.22, "IsEqualText", DateTimeOffset.Now.AddMinutes(-1) } // true }, new TimestampedValues { DiscriminatorId = discriminatorId, Timestamp = DateTimeOffset.Now.AddMinutes(-4), - Values = new object[] { 2, 2.22, "IsNotEqualText", filterDate }, // true - DataScheme = dataScheme + Values = new object[] { 2, 2.22, "IsNotEqualText", filterDate } // true }, new TimestampedValues { DiscriminatorId = discriminatorId, Timestamp = DateTimeOffset.Now.AddMinutes(-1), - Values = new object[] { 2, 2.22, "IsNotEqualText", DateTimeOffset.Now.AddMinutes(-1) }, // false - DataScheme = dataScheme + Values = new object[] { 2, 2.22, "IsNotEqualText", DateTimeOffset.Now.AddMinutes(-1) } // false } } .AsQueryable(); diff --git a/DD.Persistence.Test/TimestampedValuesServiceShould.cs b/DD.Persistence.Test/TimestampedValuesServiceShould.cs index 1a51eb1..e1e7cdc 100644 --- a/DD.Persistence.Test/TimestampedValuesServiceShould.cs +++ b/DD.Persistence.Test/TimestampedValuesServiceShould.cs @@ -2,6 +2,7 @@ using DD.Persistence.Models; using DD.Persistence.Repositories; using DD.Persistence.Services; using NSubstitute; +using System.Text.Json; namespace DD.Persistence.Test; public class TimestampedValuesServiceShould @@ -44,10 +45,10 @@ public class TimestampedValuesServiceShould { var values = new Dictionary() { - { "A", i }, - { "B", i * 1.1 }, - { "C", $"Any{i}" }, - { "D", DateTimeOffset.Now }, + { "A", GetJsonFromObject(i) }, + { "B", GetJsonFromObject(i * 1.1) }, + { "C", GetJsonFromObject($"Any{i}") }, + { "D", GetJsonFromObject(DateTimeOffset.Now) } }; yield return new TimestampedValuesDto() @@ -57,4 +58,11 @@ public class TimestampedValuesServiceShould }; } } + + private static JsonElement GetJsonFromObject(object value) + { + var jsonString = JsonSerializer.Serialize(value); + var doc = JsonDocument.Parse(jsonString); + return doc.RootElement; + } } -- 2.45.2