Pluggable filter value conversions #1284
Closed
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
This PR adds an extensibility point for API developers to add custom converters that turn literal text (a value surrounded by single quotes) in filter query string parameters into a .NET object. The resulting object may or may not be compatible with the property type. In the latter case, a custom expression rewriter can be used to manipulate the filter to match up, potentially adding additional conditions. Such a rewriter is typically invoked from
IResourceDefinition<,>.OnApplyFilter()
. An example is added in this PR to demonstrate that.A converter must implement
IFilterValueConverter
. ItsCanConvert
method takes anAttrAttribute
, which provides access to the attribute name and type, and its containing type in the resource graph. Multiple converters can be registered in the IoC container, which are tried sequentially. TheConvert
method performs the actual conversion and executes whenCanConvert
returnstrue
. It takes the sameAttrAttribute
, the string value to convert from (without the surrounding quotes), and the outer filter function that consumes the value. The return type isobject
, which enables to return something incompatible to be rewritten later. It must never returnnull
, because not all expression types support that. As a workaround, return a sentinel value (for example,Missing.Value
) that is recognized by your rewriter. The consuming filter functions can be Comparison (equals
,greaterThan
,greaterOrEqual
,lessThan
,lessOrEqual
), Any (pick from a set of candidates), or TextMatch (contains
,startsWith
,endsWith
). The last one requires that your rewriter produces astring
. If that's not possible, or the conversion fails for any other invalid input, throw aQueryParseException
. By doing so, it gets translated into an appropriate JSON:API error response. Throwing anything else results in an HTTP 500 error.Alternative designs considered:
TypeConverter
. This requires developers to add[TypeConverter(typeof(YourConverter))]
on resource properties, whereYourConverter
derives fromSystem.ComponentModel.TypeConverter
, overridingCanConvertTo/From
andConvertTo/From
. This requires fine-grained control per property (which may be liked or not), but the downside is that everything is globally wired up, so IoC dependencies cannot be injected into the converter. It is also tricky to get right because ModelState validation triggers your type converter too. This API doesn't fit our needs well: we always convert from string, and to something the converter itself decides, which is unknown upfront.ValueConverter<TModel, TProvider>
from Entity Framework Core. They convert to/from an entity property type to a database type (for example, to store enums as strings). Aside from taking a dependency on EF Core for parsing query strings doesn't sound appealing, these are configured through EF Core's fluent API, which JsonApiDotNetCore doesn't have. In EF Core, they can be applied per entity property, or convention-based. It is also possible to configure a converter instance, whose construction can depend on injected dependencies in theDbContext
. The static typing works against us because we'd like the converter to flexibly choose different return types, based on the incoming text. Using the non-genericValueConverter
base class doesn't solve that. What's also unfortunate is that the conversion must be aSystem.Expression
, which doesn't permit to use modern C# syntax such as the?.
operator. Expressions are required because these converters are weaved into EF Core's projection shapers that turn SQL result sets into .NET objects.Additionally, the first commit in this PR improves validation of filters and eliminates the multi-pass parsing of
null
comparisons against to-one relationships.Fixes #1277.
Fixes #1275.
Fixes #1283.
QUALITY CHECKLIST