Esta documentación nos muestra como preparar el backend para generar filtros de manera dinámica para conectarlo con el componente de tabla del frontend(ver articulo link pendiente). En este punto considere que ya tiene configurado su proyecto de backend con al menos un controlador. Tendra que ser como se muestra a continuación.
{
/// <summary>
/// Budgets controller
/// </summary>
[IdentityUserInfo]
[RoutePrefix("budgets")]
public class BudgetsController : BaseApiController
{
/// <summary>
/// Constructor
/// </summary>
/// <param name="logger"></param>
/// <param name="mediator"></param>
public BudgetsController(ILog logger, IMediator mediator)
: base(logger, mediator)
{ }
/// <summary>
/// Get a list of budgets with filters
/// </summary>
/// <returns></returns>
[HttpGet, Route("")]
public IHttpActionResult Search()
{
throw new System.NotImplementedException();
}
}
}
Para verificar el funcionamiento del endpoint se modifico la función search de la siguiente manera.
/// <summary>
/// Get a list of budgets with filters
/// </summary>
/// <returns></returns>
[HttpGet, Route("")]
public IHttpActionResult Search()
{
return Ok();
}
Posteriormente se verifica la conexión del endpoint con el programa POSTMAN.
Ahora para la configuración base de los filtros, se crea una carpeta llamada DynamicFilters dentro del proyecto dominio de la solución en que se este trabajando, como se muestra a continuación:
Dicha carpeta contiene los archivos que se encargaran del modelado y generación de los filtros. A continuación creamos las siguientes clases:
Interfaz ISearchQuery
namespace apsys.DynamicFilters
{
public interface ISearchQuery
{
string QueryString { get; set; }
}
}
Interfaz ISearchResult
namespace apsys.DynamicFilters
{
public interface ISearchResult<T>
{
int Total { get; set; }
int PageNumber { get; set; }
int PageSize { get; set; }
Sorting Sort { get; set; }
IEnumerable<T> Items { get; set; }
}
}
Clase SearchQuery
namespace apsys.DynamicFilters
{
public class SearchQuery : ISearchQuery
{
}
}
Clase SearchResult
using System.Dynamic;
namespace apsys.DynamicFilters
{
public class SearchResult<T> : ISearchResult<T>
{
public SearchResult()
{
}
public SearchResult(int total, int pageNumber, int pageSize, Sorting sort, IEnumerable<T> items)
{
Total = total;
PageNumber = pageNumber;
PageSize = pageSize;
Sort = sort;
Items = items;
}
public int Total { get; set; }
public int PageNumber { get; set; }
public int PageSize { get; set; }
public Sorting Sort { get; set; } = new Sorting();
public IEnumerable<T> Items { get; set; } = new List<T>();
}
}
Clase RelationalOperator
namespace apsys.DynamicFilters
{
public class RelationalOperator
{
public const string Equal = "equal";
public const string NotEqual = "not_equal";
public const string Contains = "contains";
public const string StartsWith = "starts_with";
public const string EndsWith = "ends_with";
public const string Between = "between";
public const string GreaterThan = "greater_than";
public const string GreaterThanOrEqual = "greater_or_equal_than";
public const string LessThan = "less_than";
public const string LessThanOrEqual = "less_or_equal_than";
}
}
Clase FilterOperator
using System.Dynamic;
namespace apsys.DynamicFilters
{
public class FilterOperator
{
public FilterOperator()
{
}
public FilterOperator(string fileldName, string value)
{
FieldName = fileldName;
RelationalOperatorType = RelationalOperator.Equal;
Values.Add(value);
}
public FilterOperator(string fileldName, string value, string relationalOperatorType)
{
FieldName = fileldName;
RelationalOperatorType = relationalOperatorType;
Values.Add(value);
}
public FilterOperator(string fileldName, IEnumerable<string> values, string relationalOperatorType)
{
FieldName = fileldName;
RelationalOperatorType = relationalOperatorType;
Values = values.ToList();
}
public string FieldName { get; set; }
public string RelationalOperatorType { get; set; }
public IList<string> Values { get; set; } = new List<string>();
}
}
Clase Sorting
namespace apsys.DynamicFilters
{
public class Sorting
{
public Sorting() { }
public Sorting(string by, string direction)
{
By = by;
Direction = direction;
}
private string _by;
public string By
{
get { return string.IsNullOrEmpty(_by) ? _by : _by.ToPascalCase(); }
set { _by = value; }
}
public string Direction { get; set; }
public bool IsValid()
{
return !string.IsNullOrEmpty(By) && !string.IsNullOrEmpty(Direction);
}
public bool IsAscending()
=> Direction?.ToLower() == "asc";
}
}
Clase QueryStringParser
using apsys.DynamicFilters;
using apsys.Exeptions;
using apsys.Extenders;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Web;
namespace apsys.Parsers
{
public class QueryStringParser
{
private const string _pageNumber = "pageNumber";
private const string _pageSize = "pageSize";
private const string _sortBy = "sortBy";
private const string _sortDirection = "sortDirection";
private const string _query = "query";
private const string _query_ColumnsToSearch = "query_ColumnsToSearch";
private readonly string _queryString;
private readonly string _descending = "desc";
private readonly string _ascending = "asc";
private readonly string[] _excludedKeys = new string[] { _pageNumber, _pageSize, _sortBy, _sortDirection, _query };
public QueryStringParser(string queryString)
{
_queryString = HttpUtility.UrlDecode(queryString);
}
public int ParsePageNumber()
{
int pageNumber = 0;
if (string.IsNullOrEmpty(_queryString))
return pageNumber;
QueryStringArgs parameters = new QueryStringArgs(_queryString);
if (parameters.ContainsKey(_pageNumber))
{
if (!int.TryParse(parameters[_pageNumber], out pageNumber))
throw new InvalidQueryStringArgumentException(_pageNumber);
}
if (pageNumber < 0)
throw new InvalidQueryStringArgumentException(_pageNumber);
return pageNumber;
}
public int ParsePageSize()
{
int pageSize = 25;
if (string.IsNullOrEmpty(_queryString))
return pageSize;
QueryStringArgs parameters = new QueryStringArgs(_queryString);
if (parameters.ContainsKey(_pageSize))
{
if (!int.TryParse(parameters[_pageSize], out pageSize))
throw new InvalidQueryStringArgumentException(_pageSize);
}
if (pageSize <= 0 )
throw new InvalidQueryStringArgumentException(_pageSize);
return pageSize;
}
public Sorting ParseSorting<T>(string defaultFieldName)
{
string sortByField = defaultFieldName;
string sortDirection = _descending;
QueryStringArgs parameters = new QueryStringArgs(_queryString);
if (parameters.ContainsKey(_sortBy))
{
sortByField = parameters[_sortBy];
PropertyInfo[] properties = typeof(T).GetProperties();
if (!properties.Any(p => p.Name.ToLower() == sortByField.ToLower()))
throw new InvalidQueryStringArgumentException(_sortBy);
}
if (parameters.ContainsKey(_sortDirection))
{
sortDirection = parameters[_sortDirection];
if(sortDirection != _descending && sortDirection != _ascending)
throw new InvalidQueryStringArgumentException(_sortDirection);
}
return new Sorting(sortByField, sortDirection);
}
public IList<FilterOperator> ParseFilterOperators<T>()
{
IList<FilterOperator> filterOperatorsResult = new List<FilterOperator>();
QueryStringArgs parameters = new QueryStringArgs(_queryString);
IEnumerable<KeyValuePair<string, string>> allFilters = parameters.Where(parameter => !_excludedKeys.Contains(parameter.Key));
foreach(var filter in allFilters)
{
string[] filterData = filter.Value.Split("||");
string[] filterValues = filterData[0].Split("|");
var fileterOperator = filterData[1];
var operatorFieldName = filter.Key.ToPascalCase();
filterOperatorsResult.Add(new FilterOperator(operatorFieldName, filterValues, fileterOperator));
}
return filterOperatorsResult;
}
public QuickSearch ParseQuery<T>()
{
/// - Declare variables
string? query = string.Empty;
string? fieldsString = string.Empty;
IList<string> fields = new List<string>();
QuickSearch quickSearch = new QuickSearch();
/// - Verify _queryString has a value
if (string.IsNullOrEmpty(_queryString))
throw new InvalidQueryStringArgumentException(_query);
/// - Parse _queryString into parameters
QueryStringArgs parameters = new QueryStringArgs(_queryString);
/// - Check that parameters contains a value for query string
if (!parameters.ContainsKey(_query))
throw new InvalidQueryStringArgumentException(_query);
/// - Check that the value for query is not null or a white space
if (string.IsNullOrWhiteSpace(parameters[_query]))
throw new InvalidQueryStringArgumentException(_query);
/// - Get the value to use in the quick search
query = parameters[_query].Split("||").FirstOrDefault();
/// - Verify the value to use in the quick search is not null or empty
if(string.IsNullOrEmpty(query))
throw new InvalidQueryStringArgumentException(_query);
/// - Get the properties of the inherited class
PropertyInfo[] properties = typeof(T).GetProperties();
/// - Case where there are no columns to search
if (parameters[_query].Split("||").Count() <= 1)
{
/// - Define a collection of strings that will contain all fields that are
/// - of string type
ICollection<string> stringFields = new List<string>();
/// - Set the quick search value
quickSearch.Value = parameters[_query];
/// - Iterate over the inherited class's properties
foreach (PropertyInfo property in properties)
/// - Check if the current property is of string type, and isn't the Id property
if (property.PropertyType == typeof(string) && property.Name != "Id")
/// - Add value to stringFields
stringFields.Add(property.Name);
/// - Set the stringFields as the colums to use in quick search
quickSearch.FieldNames = stringFields.ToList();
/// - Return the quick search
return quickSearch;
}
/// - Set query as the quick search value
quickSearch.Value = query;
/// - Verify that the colums to search has a value
if(string.IsNullOrWhiteSpace(parameters[_query].Split("||")[1]))
throw new InvalidQueryStringArgumentException(_query_ColumnsToSearch);
/// - Get the colums to search into an array
fields = parameters[_query].Split("||")[1].Split("|");
/// - Verify if there is a value that is not a property of the inherited class
foreach (string field in fields)
if (!properties.Any(p => p.Name.ToLower() == field.ToLower()))
throw new InvalidQueryStringArgumentException(_query_ColumnsToSearch);
/// - Set fields as the colums to search in quick search
quickSearch.FieldNames = fields;
/// - Return the quick search
return quickSearch;
}
}
internal class QueryStringArgs : Dictionary<string, string>
{
private const string Pattern = @"(?<argName>\w+)=(?<argValue>.+)";
private readonly Regex _regex = new Regex(Pattern, RegexOptions.IgnoreCase | RegexOptions.Compiled);
/// <summary>
/// Determine if the user pass at least one valid parameter
/// </summary>
/// <returns></returns>
public bool ContainsValidArguments()
{
return (this.ContainsKey("cnn"));
}
/// <summary>
/// Constructor
/// </summary>
public QueryStringArgs(string query)
{
var args = query.Split('&');
foreach (var match in args.Select(arg => _regex.Match(arg)).Where(m => m.Success))
{
try
{
this.Add(match.Groups["argName"].Value, match.Groups["argValue"].Value);
}
catch
{
// Continues execution
}
}
}
}
}
Clase CatalogOption
using System.Dynamic;
namespace apsys.DynamicFilters
{
public class CatalogOption
{
/// <summary>
/// Constructor
/// </summary>
public CatalogOption()
{ }
/// <summary>
/// Constructor
/// </summary>
/// <param name="code"></param>
/// <param name="description"></param>
public CatalogOption(string code, string description)
{
Code = code;
Description = description;
}
/// <summary>
/// Gets or sets the key of the option
/// </summary>
public string Code { get; set; }
/// <summary>
/// Gets or sets the name of the option
/// </summary>
public string Description { get; set; }
}
}
Es necesario modificar el namespace dentro de cada archivo de la carpeta, para que se ajuste al proyecto en que se está trabajando, como se muestra en la siguiente imagen con el archivo CatalogOption.cs.
namespace apsys.management.timesheets.DynamicFilters
Habrán archivos que tendrán errores. Algunos de estos consistirán en directivas, o referencias, faltantes, como se muestra en la siguiente imagen:
Estos errores pueden corregirse al dejar el cursor sobre el error o (Ctrl + .)
, lo cual permitirá hacer click sobre el icono de "Acciones rápidas y refactorizaciones", marcado con el recuadro rojo en la siguiente imagen:
Al hacer click sobre el icono, se abrirá un menú contextual. De este menú, usualmente se selecciona la primera opción, resaltado con el recuadro rojo en la siguiente imagen:
Esto agregará la directiva faltante, y corregirá el error, como se ve en la siguiente imagen:
Este proceso se repetira para cada uno de los errores con la finalidad de incluir las referencias faltantes.
Otros errores consisten en archivos faltantes que también deberán agregarse al proyecto, los cuales estarán también dentro del repositorio.
En el caso del error mostrado arriba, se agregaran los siguientes archivos en la carpeta de Exceptions del proyecto de dominio:
A estos archivos tambien se les modificará el namespace, y se agregarán directivas faltantes, en caso de haberlas:
namespace apsys.management.timesheets.Exeptions
Una vez hecho esto, se puede corregir el error del archivo faltante agregando la referencia faltante, como se mostro con el error anterior:
Hay que agregar el archivo StringExtensions.cs dentro de la carpeta Extenders, siguiendo el mismo proceso de modificar el namespace y agregar referencias faltantes.
Si ya existe un archivo en la carpeta mencionada y que también sea un extender de strings, quedara a consideración de quien sigue esta guia el copiar el contenido del archivo "StringExtensions" (el archivo del repositorio) dentro del archivo StringExtender (o similar), o crear el archivo StringExtensions, tal que existan ambos archivos.
De tal manera que la carpeta DynamicFilters contendra los siguientes archivos:
Dentro del archivo FilterExpressionParser se debe de modificar el DomainBase
static public Expression<Func<T, bool>> ParsePredicate<T>(IEnumerable<FilterOperator> operands) where T : DomainBase
Y para este proyecto debera quedar como se muestra a continuación:
Es posible que nos encontremos con errores como los que se muestran, para resolver los conflictos presentados debemos modificar la referencia al objeto de dominio base en esas funciones.
Quedando como:
private static Expression CallToStringMethod<T>(Expression propertyExpression, string propertyName) where T : DomainBase
{
PropertyInfo propertyInfo = typeof(T).GetProperty(propertyName);
if (!propertyInfo.PropertyType.Equals(typeof(string)))
return Expression.Call(propertyExpression, "ToString", Type.EmptyTypes);
return propertyExpression;
}
private static Expression CreateConstantExpression<T>(string propertyName, string constatValue) where T : DomainBase
{
PropertyInfo propertyInfo = typeof(T).GetProperty(propertyName);
if (propertyInfo.PropertyType.Equals(typeof(string)))
return Expression.Constant(constatValue);
object convertedValue = propertyInfo.PropertyType.IsEnum
? Enum.Parse(propertyInfo.PropertyType, constatValue)
: Convert.ChangeType(constatValue, propertyInfo.PropertyType);
return Expression.Constant(convertedValue);
}
Dependiendo de la versión de lenguaje de programación que se este usando posiblemente sea necesario realizar algunos ajustes, como se muestra en la siguiente imagen:
:)