Skip to content

Instantly share code, notes, and snippets.

@roqueneo
Last active March 24, 2023 23:26
Show Gist options
  • Save roqueneo/465165986ba93e8ca3231b62c91defc3 to your computer and use it in GitHub Desktop.
Save roqueneo/465165986ba93e8ca3231b62c91defc3 to your computer and use it in GitHub Desktop.
Test

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. Conexión en 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: Agregar DynamicFilters

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

Modificación namespace

Habrán archivos que tendrán errores. Algunos de estos consistirán en directivas, o referencias, faltantes, como se muestra en la siguiente imagen:

Errores

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:

Errores(1)

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:

Menu contextual

Esto agregará la directiva faltante, y corregirá el error, como se ve en la siguiente imagen:

Directiva faltante

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.

Errores(2)

En el caso del error mostrado arriba, se agregaran los siguientes archivos en la carpeta de Exceptions del proyecto de dominio:

Exzceptions

A estos archivos tambien se les modificará el namespace, y se agregarán directivas faltantes, en caso de haberlas:

namespace apsys.management.timesheets.Exeptions

Modificación namespace(1)

Una vez hecho esto, se puede corregir el error del archivo faltante agregando la referencia faltante, como se mostro con el error anterior:

Error de archivo faltante

Hay que agregar el archivo StringExtensions.cs dentro de la carpeta Extenders, siguiendo el mismo proceso de modificar el namespace y agregar referencias faltantes.

Carpeta Extenders

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.

StringExtender

De tal manera que la carpeta DynamicFilters contendra los siguientes archivos:

Carpeta DynamicFilters

Dentro del archivo FilterExpressionParser se debe de modificar el DomainBase

static public Expression<Func<T, bool>> ParsePredicate<T>(IEnumerable<FilterOperator> operands) where T : DomainBase

FilterExpressionParser

Y para este proyecto debera quedar como se muestra a continuación:

DomainBase

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.

Errores(4))

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:

Ajuste

@Isaprez
Copy link

Isaprez commented Mar 24, 2023

:)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment