alirezanet/Gridify

Filtering and Ordering validation attribute

sebascomeau opened this issue ยท 13 comments

I needed to add documentations on Filter and OrderBy fields. I've created my own class Query that implements IGridifyQuery interface.

public class Query : IGridifyQuery {

    public Query() { }

    public Query(int page, int pageSize, string filter, string? orderBy = null) {
        Page = page;
        PageSize = pageSize;
        Filter = filter;
        OrderBy = orderBy;
    }

    public int Page { get; set; }

    public int PageSize { get; set; }

    /// <summary>
    /// Filtering syntax<br />
    /// <a href="https://alirezanet.github.io/Gridify/guide/filtering.html">https://alirezanet.github.io/Gridify/guide/filtering.html</a>
    /// </summary>
    public string? Filter { get; set; }

    /// <summary>
    /// Ordering syntax<br />
    /// <a href="https://alirezanet.github.io/Gridify/guide/ordering.html">https://alirezanet.github.io/Gridify/guide/ordering.html</a>
    /// </summary>
    public string? OrderBy { get; set; }

}

This woks great but my problem is that I need to catch GridifyMapperException in my controller enpoint if the Filter or OrderBy value cannot be mapped. I don't want to set IgnoreNotMappedFields to true because I want to return a BadRequest reponse. At the moment if I don't catch the GridifyMapperException my application will returns a 500 error. Maybe there's a way to create an ValidationAttribute for Filtering and Ordering that would validate the value on a type.

[GridifyFiltering(typeof(User))]
public string? Filter { get; set; }

I come up with a different solution for my project. Of course having a way to validate the model before going into the controller methods would be ideal.

Added generic type to my Query class.

public class Query<T> : IGridifyQuery{}

Then created extension methods to validate Filter and OrderBy.

public static class QueryExtensions {

    public static bool IsFilterValid<T>(this Query<T> query, IGridifyMapper<T>? mapper = null) {
        return ((IGridifyFiltering)query).IsValid(mapper);
    }

    public static bool IsOrderByValid<T>(this Query<T> query, IGridifyMapper<T>? mapper = null) {
        return ((IGridifyOrdering)query).IsValid(mapper);
    }

    public static bool IsValid<T>(this Query<T> query, IGridifyMapper<T>? mapper = null) {
        return query.IsFilterValid(mapper) && query.IsOrderByValid(mapper);
    }
}

Then validate if manully in the controller.

public async Task<IActionResult> GetOrganizations([FromQuery] Query<Organization> query) {

    if (!query.IsValid()) {
            return Problem(statusCode: StatusCodes.Status400BadRequest, title: "Invalid filtering and/or ordering");
    }

    // ...   
}

Hi @sebascomeau,
So is the problem solved? You can also use the IsValid methods in a custom attribute if you want.

Also, I suggest wrapping the code in a try/catch to handle other unexpected exceptions if occurs

I've tried to build a ValidationAttribute but generic Type is not supported.

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
public class GridifyFilterAttribute<T> : ValidationAttribute {

    public GridifyFilterAttribute()
        : base("The field {0} must be a valid filtering syntax.") { }

    public override bool IsValid(object? value) {
        if (value == null) {
            return true;
        }

        string valueAsString = (string)value;

        GridifyQuery query = new() {
            Filter = valueAsString
        };

        return query.IsValid<T>();
    }
}

Generic Type seems to be in preview.
image

That's right, now I see the problem. you need a non-generic IsValid method here that is not quite possible because we need to validate the string against a specific type. (unless we use some reflection trick that is not optimal).

Generic attributes seems to be a new feature of C# 10. I will investigate more because I'm using the latest .net core version.

Generic attributes seems to be a new feature of C# 10. I will investigate more because I'm using the latest .net core version.

Yes, it is a .Net6 and C# 10 feature.
if you couldn't use the generic attributes the bellow workaround works too although I don't recommend it (just updated your attribute)

public class GridifyFilterAttribute : ValidationAttribute
{
	private Type _type;

	public GridifyFilterAttribute(Type type)
			: base("The field {0} must be a valid filtering syntax.")
	{
		_type = type;
	}

	public override bool IsValid(object? value)
	{
		if (value == null)
		{
			return true;
		}

		string valueAsString = (string)value;

		GridifyQuery query = new()
		{
			Filter = valueAsString
		};

		var method = ((Func<IGridifyQuery, IGridifyMapper<object>, bool>)GridifyExtensions.IsValid).Method.GetGenericMethodDefinition();
		var genericMethod = method.MakeGenericMethod(_type);
		return (bool)genericMethod.Invoke(null, new[] { query, null });	// last null could be custom mapper
	}
}

Thanks, I've didn't found how to enable the generic attributes feature yet. I was trying to code you example, thanks very appreciate. Why you don't recommend? One thing that doesn't work at the moment is using T as typeOf(T) with the attribute.

public class Query<T> : IGridifyQuery {
    [GridifyFilter(typeof(T))]
    public string? Filter { get; set; }
}

image

In this case, if your Query class is generic you have only the generic attribute option that is currently a preview feature.

I don't recommend this solution because validating the gridifyQuery is not a static process and it needs underlying types and custom mapper information. in another word, we can not validate Query<T> if we don't know what is the mapper configurations, so validating only makes sense in a specific context.

I suggest doing the validation where you want to use the query before execution or simply at the controller. like your second comment

You're right! Thanks for your help and great work btw! ๐Ÿ™‚

I've created a extension method to validate Filter and OrderBy from IGridifyQuery. EntityBase is my abstract class for all my DbContent entities.

public static class GridifyExtensions {
    public static ModelStateDictionary Validate<T>(this IGridifyQuery query, IGridifyMapper<T>? mapper = null) where T : EntityBase {
        ModelStateDictionary modelState = new();

        if (!((IGridifyFiltering)query).IsValid(mapper)) {
            string fieldName = nameof(IGridifyFiltering.Filter);
            modelState.AddModelError(fieldName, $"The field {fieldName} is not valid.");
        }

        if (!((IGridifyOrdering)query).IsValid(mapper)) {
            string fieldName = nameof(IGridifyOrdering.OrderBy);
            modelState.AddModelError(fieldName, $"The field {fieldName} is not valid.");
        }

        return modelState;
    }
}

Usage in my controller

public async Task<IActionResult> GetOrganizations([FromQuery] GridifyQuery query) {

    ModelStateDictionary modelState = query.Validate<Organization>();

    if (!modelState.IsValid) {
        return ValidationProblem(modelState);
    }

    // ...
}

Sorry to revive this old post without supplying code examples for a proposed implementation. Not sure if this is solved or not, but I know that this can help with the issue regarding generic types and validation.

Would it be possible to implement an interface called IGridifyValidator

The contact could look like:

public interface IGridifyValidator
{
 
   // If the methods need this, perhaps something like this might be helpful to.
   string QueryParams { get; set; }


   // Returns true or false if the query parameters are empty.  (this can also be a property instead of a method)
  // Perhaps you will not need this, and can just use the method below
    bool IsValid();

  // Validates the query parameters (this can be implemented in the base class of Gridify, 
  // or the user can override this and implement this themselves.
   bool Validate();

}

Again, not sure if this helps regarding generic types. I am not sure if the library implements something similar (I have to review the code )

Thanks

Hi @jeffward01
Thank you for the suggestion, in the current version of gridify because the validate methods are static having this contract would not help a lot but I'll consider your suggestion for version 3.

@alirezanet - I agree, this makes sense.

When net7.0 comes out, we get interfaces with abstract static methods and properties. I believe this will make this much easier to integrate as it can be added onto the contract.

Just a thought!

Thanks