QueryExpressionBuilder

Library for building Predicate Expression from query models
git clone git://185.198.27.126/QueryExpressionBuilder.git
Log | Files | Refs | README

commit 5e9102bf6a46763768e47b8454037413dfeddac7
Author: novickii.sergei.nure@gmail.com <novickii.sergei.nure@gmail.com>
Date:   Fri, 26 Apr 2024 18:43:04 +0300

Init

Diffstat:
A.gitignore | 5+++++
AQueryExpressionBuilder.sln | 25+++++++++++++++++++++++++
AQueryExpressionBuilder/Attributes.cs | 72++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
AQueryExpressionBuilder/ExpressionBuilder.cs | 143+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
AQueryExpressionBuilder/QueryExpressionBuilder.csproj | 9+++++++++
AQueryExpressionBuilder/QueryExpressionBuilder.csproj.user | 7+++++++
AREADME.md | 115+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
7 files changed, 376 insertions(+), 0 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -0,0 +1,4 @@ +.vs/* +QueryExpressionBuilder/bin/* +QueryExpressionBuilder/obj/* +QueryExpressionBuilder/Properties/* +\ No newline at end of file diff --git a/QueryExpressionBuilder.sln b/QueryExpressionBuilder.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.9.34511.98 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QueryExpressionBuilder", "QueryExpressionBuilder\QueryExpressionBuilder.csproj", "{C6B925B3-E450-4830-9B40-305102A14309}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {C6B925B3-E450-4830-9B40-305102A14309}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C6B925B3-E450-4830-9B40-305102A14309}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C6B925B3-E450-4830-9B40-305102A14309}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C6B925B3-E450-4830-9B40-305102A14309}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {7E174301-9FE3-4552-830F-D0AEB96012A0} + EndGlobalSection +EndGlobal diff --git a/QueryExpressionBuilder/Attributes.cs b/QueryExpressionBuilder/Attributes.cs @@ -0,0 +1,72 @@ +namespace QueryExpressionBuilder.Attributes +{ + /// <summary> + /// Attribute to filtration strings + /// </summary> + public class String + { + + /// <summary> + /// Filtration func StartWith + /// </summary> + [AttributeUsage(AttributeTargets.Property)] + public class StartWithAttribute : BasePredicateAttribute + { + /// <summary> + /// Add attribute func StartWith + /// </summary> + /// <param name="propertyName">Name of property in DB</param> + public StartWithAttribute(string propertyName) : base(propertyName) + { + } + } + } + + /// <summary> + /// Attribute to filtration numbers and DateTime + /// </summary> + public class Numbers + { + /// <summary> + /// Filtration func >= + /// </summary> + [AttributeUsage(AttributeTargets.Property)] + public class GreaterOrEqualAttribute : BasePredicateAttribute + { + /// <summary> + /// Add attribute func >= + /// </summary> + public GreaterOrEqualAttribute(string propertyName) : base(propertyName) + { + } + } + + /// <summary> + /// Filtration func <= + /// </summary> + [AttributeUsage(AttributeTargets.Property)] + public class LessOrEqualAttribute : BasePredicateAttribute + { + /// <summary> + /// Add attribute func <= + /// </summary> + public LessOrEqualAttribute(string propertyName) : base(propertyName) + { + } + } + } + + /// <summary> + /// Base filtration class + /// </summary> + [AttributeUsage(AttributeTargets.Property)] + public class BasePredicateAttribute : Attribute + { + public string PropertyName { get; set; } + + public BasePredicateAttribute(string propertyName) + { + PropertyName = propertyName; + } + } +} diff --git a/QueryExpressionBuilder/ExpressionBuilder.cs b/QueryExpressionBuilder/ExpressionBuilder.cs @@ -0,0 +1,143 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using static QueryExpressionBuilder.Attributes.Numbers; +using static QueryExpressionBuilder.Attributes.String; + +namespace QueryExpressionBuilder +{ + /// <summary> + /// Class converter from Query model with attributes to predicate func + /// </summary> + public static class ExpressionBuilder + { + /// <summary> + /// Extension method + /// </summary> + /// <typeparam name="TDB">Type in DB</typeparam> + /// <typeparam name="TQE">Type of Query object</typeparam> + /// <param name="queryParams">Dictionary</param> + /// <returns>Predicate func</returns> + public static Expression<Func<TDB, bool>>? ToPredicate<TDB, TQE>(this Dictionary<string, string> queryParams) + { + return GetPredicateFromDictionary<TDB, TQE>(queryParams); + } + + /// <summary> + /// Make predicate from dictionary + /// </summary> + /// <typeparam name="TDB">Type in DB</typeparam> + /// <typeparam name="TQE">Type of Query object</typeparam> + /// <param name="queryParams">Dictionary</param> + /// <returns>Predicate func</returns> + public static Expression<Func<TDB, bool>>? GetPredicateFromDictionary<TDB, TQE>(Dictionary<string, string> queryParams) + { + var query = GetQueryObject<TQE>(queryParams); + return GetPredicate<TDB, TQE>(query); + } + + /// <summary> + /// Make Query instance + /// </summary> + /// <typeparam name="T">Type of Query</typeparam> + /// <param name="queryParams">Dictionary</param> + /// <returns>Instance</returns> + public static T GetQueryObject<T>(Dictionary<string, string> queryParams) + { + Type type = typeof(T); + T queryObject = Activator.CreateInstance<T>(); + + foreach (var property in type.GetProperties()) + { + if (queryParams.ContainsKey(property.Name)) + { + object value; + Type propertyType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType; + + if (propertyType == typeof(string) && string.IsNullOrEmpty(queryParams[property.Name])) + { + value = null; + } + else + { + value = Convert.ChangeType(queryParams[property.Name], propertyType); + } + + property.SetValue(queryObject, value); + } + } + + return queryObject; + } + + /// <summary> + /// Make new predicate func + /// </summary> + /// <typeparam name="TDB">Type in DB</typeparam> + /// <typeparam name="TQE">Query type</typeparam> + /// <returns>Predicate func</returns> + public static Expression<Func<TDB, bool>>? GetPredicate<TDB, TQE>(TQE query) + { + var userParameter = Expression.Parameter(typeof(TDB), typeof(TDB).Name.ToLower()); + List<Expression> conditions = new List<Expression>(); + + //Только те свойства которые помечены атрибутами + var T_pr_props = typeof(TQE).GetProperties().Where(prop => prop.GetCustomAttribute<LessOrEqualAttribute>() != null || + prop.GetCustomAttribute<GreaterOrEqualAttribute>() != null || prop.GetCustomAttribute<StartWithAttribute>() != null).ToArray(); + + //Проходимся по всем параметрам класса БД + foreach (var p in typeof(TDB).GetProperties()) + { + var yslovia = T_pr_props.Where(x => x.Name.Contains(p.Name)); + foreach (var y in yslovia) + { + if (y.GetCustomAttribute<StartWithAttribute>() != null) + { + var propertyY = Expression.Property(userParameter, y.Name); + var propertyP = Expression.Property(userParameter, p.Name); + var containsCall = Expression.Call(propertyP, "Contains", null, Expression.Constant(y.GetValue(query), typeof(string))); + var obj = y.GetValue(query); + if (obj != null) + { + conditions.Add(containsCall); + } + } + else if (y.GetCustomAttribute<GreaterOrEqualAttribute>() != null) + { + var propertyName = y.GetCustomAttribute<GreaterOrEqualAttribute>().PropertyName; + var propertyY = Expression.Property(userParameter, propertyName); + var obj = y.GetValue(query); + if (obj != null) + { + var value = Convert.ChangeType(obj, p.PropertyType); + var constantValue = Expression.Constant(value, p.PropertyType); + var condition = Expression.GreaterThanOrEqual(propertyY, constantValue); + conditions.Add(condition); + } + } + else if (y.GetCustomAttribute<LessOrEqualAttribute>() != null) + { + var propertyName = y.GetCustomAttribute<LessOrEqualAttribute>().PropertyName; + var propertyY = Expression.Property(userParameter, propertyName); + var obj = y.GetValue(query); + if (obj != null) + { + var value = Convert.ChangeType(obj, p.PropertyType); + var constantValue = Expression.Constant(value, p.PropertyType); + var condition = Expression.LessThanOrEqual(propertyY, constantValue); + conditions.Add(condition); + } + } + } + } + + var body = conditions.Aggregate(Expression.AndAlso); + var predicate = Expression.Lambda<Func<TDB, bool>>(body, userParameter); + return predicate; + } + } +} diff --git a/QueryExpressionBuilder/QueryExpressionBuilder.csproj b/QueryExpressionBuilder/QueryExpressionBuilder.csproj @@ -0,0 +1,9 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>net8.0</TargetFramework> + <ImplicitUsings>enable</ImplicitUsings> + <Nullable>enable</Nullable> + </PropertyGroup> + +</Project> diff --git a/QueryExpressionBuilder/QueryExpressionBuilder.csproj.user b/QueryExpressionBuilder/QueryExpressionBuilder.csproj.user @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <PropertyGroup> + <_LastSelectedProfileId>P:\MyEcoSpace\Libraries\Helpers\QueryExpressionBuilder\QueryExpressionBuilder\Properties\PublishProfiles\FolderProfile.pubxml</_LastSelectedProfileId> + </PropertyGroup> +</Project> +\ No newline at end of file diff --git a/README.md b/README.md @@ -0,0 +1,114 @@ +##QueryExpressionBuilder +Библиотека для генирации функций-предикатов для фильтрирования запросов в БД. + +#Описание +Данная библиотека помогает создать функцию-предикат для фильтрирования запросов в БД на основе модели. + +#Инструкция +Для работы библиотеки необходимо создать модель с свойствами для фильтрации.<br> +Свойства в модели необходимо пометить атрибутами.<br><br> + +На данный момент существует 3 атрибута:<br><br> + +Пространство имен QueryExpressionBuilder.Attributes.String - атрибуты для свойств типа String<br> +StartWithAttribute - Означает, что дял свойства будет использоватся фильтр с аналогом функции System.String.StartWith()<br><br> + +Пространство имен QueryExpressionBuilder.Attributes.Numbers - атрибут для всех свойств которые ввляются числовыми, в том числе DateTime<br> +GreaterOrEqualAttribute - Означает, что для свойства будет использоваться фильтр с аналогом условного выражения >=<br> +LessOrEqualAttribute - Означает, что для свойства будет использоваться фильтр с аналогом условного выражения <=<br> + +В конструктор атрибута необходимо передать название свойства из класса представляющий сущьность в БД.<br><br> + +#Примеры +Предположим у нас есть сущьность User, которая являеться сущьностью в БД. +```csharp + public class User : IEntity + { + public Guid Id { get; set; } + public string Name { get; set; } + public string Surname { get; set; } + public string Email { get; set; } + public DateTime BirthDate { get; set; } + public DateTime RegistrationDate { get; set; } + public string PasswodHash { get; set; } + public int Age { get; set; } + public float Amount { get; set; } + } +``` +Для создания Query фильтра нам необходимо создать query-модель +```csharp + public class UserQuery + { + [QueryExpressionBuilder.Attributes.String.StartWith("Name")] + public string Name { get; set; } + + [QueryExpressionBuilder.Attributes.String.StartWith("Surname")] + public string Surname { get; set; } + + [QueryExpressionBuilder.Attributes.String.StartWith("Email")] + public string Email { get; set; } + + [QueryExpressionBuilder.Attributes.Numbers.GreaterOrEqual("BirthDate")] + public DateTime? FromBirthDate { get; set; } + + [QueryExpressionBuilder.Attributes.Numbers.LessOrEqual("BirthDate")] + public DateTime? ToBirthDate { get; set; } + + [QueryExpressionBuilder.Attributes.Numbers.GreaterOrEqual("RegistrationDate")] + public DateTime? FromRegistrationDate { get; set; } + + [QueryExpressionBuilder.Attributes.Numbers.LessOrEqual("RegistrationDate")] + public DateTime? ToRegistrationDate { get; set; } + + [QueryExpressionBuilder.Attributes.Numbers.GreaterOrEqual("Age")] + public int? FromAge { get; set; } + + [QueryExpressionBuilder.Attributes.Numbers.LessOrEqual("Age")] + public int? ToAge { get; set; } + + [QueryExpressionBuilder.Attributes.Numbers.GreaterOrEqual("Amount")] + public float? FromAmount { get; set; } + + [QueryExpressionBuilder.Attributes.Numbers.LessOrEqual("Amount")] + public float? ToAmount { get; set; } + } +``` +И теперь в методе контроллера, который представляет конечную точку, нужно просто передать обьект UserQuery в класс ExpressionBuilder, который вернет функцию-предикат. +```csharp + [ApiController] + [Route("[controller]")] + public class WeatherForecastController : ControllerBase + { + private readonly ILogger<WeatherForecastController> _logger; + private readonly IUserService userService; + + public WeatherForecastController(ILogger<WeatherForecastController> logger, IUserService _userService) + { + userService = _userService; + _logger = logger; + } + + /// <summary> + /// Метод получения юзеров + /// </summary> + /// <param name="queryParams">Объект содержащий параметры фильтрации</param> + /// <returns>Возвращает список юзеров</returns> + [HttpGet("[controller]/GetUsers")] + public async Task<IEnumerable<User>> GetUsers([FromQuery] UserQuery queryParams) + { + //Генерируем предикат на основе параметров + var predicate = ExpressionBuilder.GetPredicate<User, UserQuery>(queryParams); + //Передаем предикат в сервис и возвращаем результат + var result = await userService.GetUsers(predicate); + return result; + } + } +``` +Теперь отправляя запрос +https://localhost:7001/GetUsers?Name=S&FromBirthDate=2010-01-01&ToBirthDate=2040-01-01&FromAge=20 +в БД мы получаес SQL запрос +```sql +SELECT "u"."Id", "u"."Age", "u"."Amount", "u"."BirthDate", "u"."Email", "u"."Name", "u"."PasswodHash", "u"."RegistrationDate", "u"."Surname" + FROM "Users" AS "u" + WHERE instr("u"."Name", 'S') > 0 AND "u"."BirthDate" >= '2010-01-01 00:00:00' AND "u"."BirthDate" <= '2040-01-01 00:00:00' AND "u"."Age" >= 20 +``` +\ No newline at end of file