Internally, Gridify uses an auto-generated mapper that maps your string-based field names to actual properties in your entities. However, sometimes you may want to control which fields support filtering or sorting. You can create a custom mapper to define exactly what field names map to which properties.
To better understand how this works, consider the following example:
// sample Entities
public class Person
{
public string UserName { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Password { get; set; }
public Contact Contact { get; set; }
}
public class Contact
{
public string Address { get; set; }
public int PhoneNumber { get; set; }
}In this example we want to:
- Generate default mappings
- Ignore the
Passwordproperty - Map the
addressandmobileto the Contact property - Make sure the userName value is always lowercase in the search
var mapper = new GridifyMapper<Person>()
.GenerateMappings()
.RemoveMap(nameof(Person.Password))
.AddMap("address", p => p.Contact.Address)
.AddMap("mobile", p => p.Contact.PhoneNumber)
.AddMap("userName", p => p.UserName, v => v.ToLower());In the following, we will become more familiar with the above methods
This method generates mappings for the properties of the entity, including top-level public properties and properties of nested classes up to the specified nesting depth.
- To generate mappings for top-level public properties only, you can call this method without passing any arguments:
var mapper = new GridifyMapper<Person>()
.GenerateMappings();- To generate mappings with control over nesting depth, you can specify the
maxNestingDepthparameter. This parameter limits how deep the mappings will be generated for nested classes and navigation properties(added in v2.15.0). Set it to 0 for no nesting or a positive value to control the depth(added in v2.11.0):
var mapper = new GridifyMapper<Person>()
// Generates mappings for top-level properties and properties of nested classes up to 2 levels deep.
.GenerateMappings(2);::: tip Another alternative to generate default mappings for top-level public properties is by passing true to the GridifyMapper constructor. This generates mappings without considering nesting depth.
var mapper = new GridifyMapper<Person>(true);
var mapperWithDepth = new GridifyMapper<Person>(true, 2);:::
This method removes a mapping from the mapper. You will typically use this method after generating mappings to ignore properties that you don't want to support with Gridify filtering or ordering actions.
This method adds a mapping to the mapper.
- the first parameter is the name of the field you want to use in the string query
- the second parameter is a property selector expression
- the third parameter is an optional value convertor expression that you can use to convert user inputs to anything you want
If you need to modify search values before the filtering operation, you can use this feature. The third parameter of the GridifyMapper AddMap method accepts a function to convert input values.
In the example above, we convert the userName value to lowercase before filtering:
mapper = mapper.AddMap("userName", p => p.UserName, value => value.ToLower());The AddCompositeMap method allows you to search across multiple properties with a single filter reference, automatically combining them with OR logic. This eliminates the need to construct complex OR filter strings on the frontend.
var mapper = new GridifyMapper<Person>()
.AddCompositeMap("search",
x => x.FirstName,
x => x.LastName,
x => x.UserName);
// Frontend sends: search=John
// Generates: WHERE FirstName = 'John' OR LastName = 'John' OR UserName = 'John'You can apply a shared value converter function that transforms filter values before comparison:
var mapper = new GridifyMapper<Product>()
.AddCompositeMap("search",
value => value.ToUpper(), // Shared convertor
x => x.Name,
x => x.Description);
// Filter: search=phone
// Converts "phone" to "PHONE" before searching// For in-memory collections
var mapper = new GridifyMapper<Product>()
.AddCompositeMap("search",
x => x.Name,
x => x.Description,
x => (object)x.Id); // Cast to object for non-string types
// For Entity Framework (recommended)
var mapper = new GridifyMapper<Product>()
.AddCompositeMap("search",
x => x.Name,
x => x.Description,
x => x.Id.ToString()); // Convert to string for EF compatibility// Without convertor
IGridifyMapper<T> AddCompositeMap(
string from,
params Expression<Func<T, object?>>[] expressions)
// With convertor
IGridifyMapper<T> AddCompositeMap(
string from,
Func<string, object>? convertor,
params Expression<Func<T, object?>>[] expressions)Parameters:
from: The field name to use in filtersconvertor: Optional shared value converter functionexpressions: One or more property expressions to search across
Returns: The mapper instance for method chaining
Composite maps support all Gridify operators: =, !=, >, <, >=, <=, =*, !*, ^, $, !^, !$
- Cleaner Frontend Code - Send
search=valueinstead ofname=value|email=value|phone=value - Backend Control - Change searchable fields without frontend changes
- Type Safety - Compile-time checking of property expressions
::: warning Entity Framework Users When using composite maps with Entity Framework, especially with PostgreSQL, follow the Entity Framework compatibility guidelines for proper type handling. :::
The AddNestedMapper method allows you to reuse mapper configurations for nested objects across multiple entities. This is particularly useful when you have the same nested type (like Address) used in multiple parent entities (like User and Company), and you want to define the nested mappings once and reuse them everywhere.
// Define a reusable mapper for Address
var addressMapper = new GridifyMapper<Address>()
.AddMap("city", x => x.City)
.AddMap("country", x => x.Country);
// Note: Secret is intentionally not mapped
// Without prefix - merges directly
var userMapper = new GridifyMapper<User>()
.AddMap("email", x => x.Email)
.AddNestedMapper(x => x.Address, addressMapper);
// Supports: "city=London", "country=UK"
// With prefix
var companyMapper = new GridifyMapper<Company>()
.AddMap("name", x => x.Name)
.AddNestedMapper("location", x => x.Address, addressMapper);
// Supports: "location.city=London", "location.country=UK"You can define custom mapper classes and use them with the generic overloads:
// Define a custom mapper class
public class AddressMapper : GridifyMapper<Address>
{
public AddressMapper()
{
AddMap("city", q => q.City);
AddMap("country", q => q.Country);
// Secret field is not mapped - not exposed for filtering
}
}
// Without prefix - uses custom mapper class
var userMapper = new GridifyMapper<User>()
.AddMap("email", x => x.Email)
.AddNestedMapper<AddressMapper>(x => x.Address);
// Supports: "city=London", "country=UK" (Secret is hidden)
// With prefix - uses custom mapper class
var companyMapper = new GridifyMapper<Company>()
.AddMap("name", x => x.Name)
.AddNestedMapper<AddressMapper>("location", x => x.Address);
// Supports: "location.city=London", "location.country=UK" (Secret is hidden)Benefits: Custom mapper classes allow you to define mappings once, control field exposure, and reuse them across multiple entities with compile-time safety.
- Reusability - Define nested mappings once, reuse across multiple entities
- Type Safety - Compile-time checking of property expressions
- Convertor Support - Nested mappings preserve their value convertors
- Composite Map Support - Works with composite maps defined in the nested mapper
- Security - Only expose fields you explicitly map; unmapped fields remain hidden
- Flexible Prefixing - Use custom prefixes or merge directly without prefix
public class Address
{
public string City { get; set; }
public string Country { get; set; }
public string Secret { get; set; } // Sensitive data
}
// Create a secure address mapper that excludes Secret
var addressMapper = new GridifyMapper<Address>()
.AddMap("city", x => x.City)
.AddMap("country", x => x.Country);
// Secret is intentionally not mapped
// Apply to multiple entities with prefix
var userMapper = new GridifyMapper<User>()
.AddNestedMapper("address", x => x.Address, addressMapper);
var companyMapper = new GridifyMapper<Company>()
.AddNestedMapper("location", x => x.Address, addressMapper);
// Secret is not exposed in any of these mappers
Assert.False(userMapper.HasMap("address.secret"));
Assert.False(companyMapper.HasMap("location.secret"));- DRY Principle - Don't repeat yourself; define nested mappings once
- Consistency - Ensure the same fields are exposed/hidden across all entities
- Maintainability - Change nested mappings in one place, apply everywhere
- Similar to AutoMapper - Works like embedded DTO mappings in AutoMapper
This method checks if the mapper has a mapping for the given field name.
This method clears the list of mappings.
This method returns the list of current mappings.
This method returns the list of current mappings for the given type.
var mapperConfig = new GridifyMapperConfiguration()
{
CaseSensitive = false,
AllowNullSearch = true,
IgnoreNotMappedFields = false
};
var mapper = new GridifyMapper<Person>(mapperConfig);By default, the mapper is case-insensitive, but you can enable case-sensitive mappings if needed.
- Type:
bool - Default:
false
var mapper = new GridifyMapper<Person>(q => q.CaseSensitive = true);By setting this to true, Gridify won't throw an exception when a field name is not mapped. For instance, in the example above, searching for password will not throw an exception.
- Type:
bool - Default:
false
var mapper = new GridifyMapper<Person>(q => q.IgnoreNotMappedFields = true);By setting this to false, Gridify won't allow searching for null values using the null keyword.
- Type:
bool - Default:
true
var mapper = new GridifyMapper<Person>(q => q.AllowNullSearch = false);If true, string comparison operations are case insensitive by default.
- type:
bool - default:
false
var mapper = new GridifyMapper<Person>(q => q.CaseInsensitiveFiltering = true);By setting this property to a DateTimeKind value, you can change the default DateTimeKind used when parsing dates.
- type:
DateTimeKind - default:
null
var mapper = new GridifyMapper<Person>(q => q.DefaultDateTimeKind = DateTimeKind.Utc);Here's the addition for EntityFrameworkCompatibilityLayer with slight improvements for clarity:
This setting is similar to DisableNullChecks in the global configuration, but it allows you to enable this setting on a per-query basis instead of globally. When set to true, Gridify won't check for null values in collections during filtering operations.
- Type:
bool - Default:
false
var mapper = new GridifyMapper<Person>(q => q.DisableCollectionNullChecks = true);This setting is similar to AvoidNullReference in the global configuration, but it allows you to enable this setting on a per-query basis instead of globally. When set to true, Gridify won't check for null values in collections during filtering operations.
- Type:
bool - Default:
false
var mapper = new GridifyMapper<Person>(q => q.AvoidNullReference = true);This setting is the same as EntityFrameworkCompatibilityLayer in the global configuration, but it allows you to enable this setting on a per-query basis instead of globally. When set to true, the EntityFramework Compatibility layer is enabled, making the generated expressions compatible with Entity Framework.
- Type:
bool - Default:
false
var mapper = new GridifyMapper<Person>(q => q.EntityFrameworkCompatibilityLayer = true);This setting is the same as AllowFieldToFieldComparison in the global configuration, but it allows you to enable this setting on a per-mapper basis. When set to true, filter expressions can reference another mapped field on the right-hand side using the (fieldName) syntax.
- Type:
bool - Default:
false
var mapper = new GridifyMapper<Order>(q => q.AllowFieldToFieldComparison = true)
.AddMap("price", o => o.Price)
.AddMap("discount", o => o.Discount);
// Find orders where price is greater than discount
orders.ApplyFiltering("price>(discount)", mapper);For more information, see Field-to-Field Comparison.
You can use LINQ Select and SelectMany methods to filter your data using its nested collections.
In this example, we have 3 nested collections, but filtering will apply to the Property1 of the third level.
var mapper = new GridifyMapper<Level1>()
.AddMap("prop1", l1 => l1.Level2List
.SelectMany(l2 => l2.Level3List)
.Select(l3 => l3.Property1));
// ...
var query = level1List.ApplyFiltering("prop1 = 123", mapper);if you have only two-level nesting, you don't need to use SelectMany.
Starting from version v2.15.0, GridifyMapper's AddMap method supports filtering on properties that are indexable, such as sub-collections, arrays, and dictionaries. This allows you to create dynamic queries by defining mappings to specific indexes or dictionary keys using square brackets [ ].
You can define a mapping to a specific index in an array or sub-collection by specifying the index within square brackets [ ].`
var gm = new GridifyMapper<TargetType>()
.AddMap("arrayProp", (target, index) => target.MyArray[index].Prop);
var gq = new GridifyQuery
{
// Filters on the 8th element of an array property
Filter = "arrayProp[8] > 10"
};Similarly, you can define a mapping to a specific key in a dictionary or in a navigation property.
var gm = new GridifyMapper<TargetType>()
.AddMap("dictProp", (target, key) => target.MyDictionary[key]);
var gm2 = new GridifyMapper<TargetType>()
.AddMap("navProp", (target, key) => target.NavigationProperty.Where(n => n.Key == key).Select(n => n.Value));
var gq = new GridifyQuery
{
// Filters on the value associated with the 'name' key in a dictionary
Filter = "dictProp[name] = John"
};If your dictionary key is not a string, you can use the generic overload of the AddMap<T> method to specify the key type.
var gm = new GridifyMapper<TargetType>()
.AddMap<Guid>("dictProp", (target, key) => target.MyDictionary[key]);For more information on filtering using these mappings, refer to the Using Indexers.
This method returns the selector expression that you can use it in LINQ queries.
Expression<Func<Person, object>> selector = mapper.GetExpression(nameof(Person.Name));This method returns the selector expression that you can use it in LINQ queries.
LambdaExpression selector = mapper.GetLambdaExpression(nameof(Person.Name));