Skip to content

Commit c58e3af

Browse files
authored
feat: add support AddNestedMapper to QueryBuilder for reusable nested object mappings (#319)
1 parent 8aad12d commit c58e3af

File tree

4 files changed

+670
-1
lines changed

4 files changed

+670
-1
lines changed

docs/pages/guide/queryBuilder.md

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ The `QueryBuilder` class is useful when you want to manually build your query or
1111
| UseCustomMapper | Accepts a GridifyMapper to use in build methods |
1212
| UseEmptyMapper | Sets up an empty GridifyMapper without auto-generated mappings |
1313
| AddMap | Adds a single map to the existing mapper |
14+
| AddCompositeMap | Adds a composite map that searches across multiple properties with OR logic |
15+
| AddNestedMapper | Adds a nested mapper to reuse mappings from nested object types |
1416
| RemoveMap | Removes a single map from the existing mapper |
1517
| ConfigureDefaultMapper | Configures the default mapper when UseCustomMapper method is not used |
1618
| IsValid | Validates Condition, OrderBy, Query, and Mapper; returns a boolean |
@@ -31,3 +33,97 @@ var builder = new QueryBuilder<Person>()
3133

3234
var query = builder.Build(persons);
3335
```
36+
37+
## AddNestedMapper
38+
39+
The `AddNestedMapper` method in `QueryBuilder<T>` allows you to reuse mapper configurations for nested objects when building queries dynamically. This mirrors the functionality available in `GridifyMapper<T>`, enabling DRY (Don't Repeat Yourself) mapper composition in query builders.
40+
41+
::: tip Related Documentation
42+
For detailed information about nested mappers and their benefits, see the [AddNestedMapper documentation in GridifyMapper guide](./gridifyMapper.md#addnestedmapper).
43+
:::
44+
45+
### Basic Usage Examples
46+
47+
#### Example 1: Reusing Address Mapper Without Prefix
48+
49+
```csharp
50+
// Define a reusable address mapper
51+
var addressMapper = new GridifyMapper<Address>()
52+
.AddMap("city", x => x.City)
53+
.AddMap("country", x => x.Country);
54+
55+
// Use in QueryBuilder - merges directly
56+
var builder = new QueryBuilder<User>()
57+
.AddMap("email", x => x.Email)
58+
.AddNestedMapper(x => x.Address, addressMapper)
59+
.AddCondition("city=London")
60+
.AddOrderBy("email");
61+
62+
var result = builder.Build(users.AsQueryable());
63+
// Supports: "city=London", "country=UK"
64+
```
65+
66+
#### Example 2: Reusing Address Mapper With Prefix
67+
68+
```csharp
69+
// Define a reusable address mapper
70+
var addressMapper = new GridifyMapper<Address>()
71+
.AddMap("city", x => x.City)
72+
.AddMap("country", x => x.Country);
73+
74+
// Use in QueryBuilder - with custom prefix
75+
var builder = new QueryBuilder<Company>()
76+
.AddMap("name", x => x.Name)
77+
.AddNestedMapper("location", x => x.Address, addressMapper)
78+
.AddCondition("location.city=Berlin")
79+
.ConfigurePaging(0, 10);
80+
81+
var result = builder.Build(companies.AsQueryable());
82+
// Supports: "location.city=Berlin", "location.country=Germany"
83+
```
84+
85+
#### Example 3: Custom Mapper Classes
86+
87+
```csharp
88+
// Define a custom mapper class
89+
public class AddressMapper : GridifyMapper<Address>
90+
{
91+
public AddressMapper()
92+
{
93+
AddMap("city", q => q.City);
94+
AddMap("country", q => q.Country);
95+
}
96+
}
97+
98+
// Use with QueryBuilder - without prefix
99+
var builder = new QueryBuilder<User>()
100+
.AddMap("email", x => x.Email)
101+
.AddNestedMapper<AddressMapper>(x => x.Address);
102+
103+
// Use with QueryBuilder - with prefix
104+
var builder = new QueryBuilder<Company>()
105+
.AddMap("name", x => x.Name)
106+
.AddNestedMapper<AddressMapper>("location", x => x.Address);
107+
```
108+
109+
#### Example 4: Multiple Nested Mappers
110+
111+
```csharp
112+
var addressMapper = new GridifyMapper<Address>()
113+
.AddMap("city", x => x.City)
114+
.AddMap("country", x => x.Country);
115+
116+
var contactMapper = new GridifyMapper<Contact>()
117+
.AddMap("phone", x => x.Phone)
118+
.AddMap("email", x => x.Email);
119+
120+
var builder = new QueryBuilder<User>()
121+
.AddMap("name", x => x.Name)
122+
.AddNestedMapper("addr", x => x.Address, addressMapper)
123+
.AddNestedMapper("contact", x => x.Contact, contactMapper)
124+
.AddCondition("addr.city=London,contact.phone=1234567890")
125+
.AddOrderBy("name");
126+
127+
var result = builder.Build(users.AsQueryable());
128+
```
129+

src/Gridify/IQueryBuilder.cs

Lines changed: 115 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,123 @@ public interface IQueryBuilder<T>
7878
/// <param name="from">The field name to use in filters</param>
7979
/// <param name="convertor">Optional shared value converter function</param>
8080
/// <param name="expressions">One or more property expressions to search across</param>
81-
/// <returns>returns IQueryBuilder</returns>
81+
/// <inheritdoc cref="AddCompositeMap(string, Func{string, object}, Expression{Func{T, object}}[])" />
8282
IQueryBuilder<T> AddCompositeMap(string from, Func<string, object>? convertor, params Expression<Func<T, object?>>[] expressions);
8383

84+
/// <summary>
85+
/// Reuses mappings from a nested object's mapper by composing expressions.
86+
/// Merges nested mappings directly without a prefix.
87+
/// </summary>
88+
/// <typeparam name="TProperty">The type of the nested property</typeparam>
89+
/// <param name="propertyExpression">Expression pointing to the nested property (e.g., x => x.Address)</param>
90+
/// <param name="nestedMapper">The mapper containing mappings for the nested type</param>
91+
/// <param name="overrideIfExists">Whether to override existing mappings with the same key</param>
92+
/// <returns>returns IQueryBuilder</returns>
93+
/// <example>
94+
/// <code>
95+
/// var addressMapper = new GridifyMapper&lt;Address&gt;()
96+
/// .AddMap("city", x => x.City)
97+
/// .AddMap("country", x => x.Country);
98+
///
99+
/// var builder = new QueryBuilder&lt;User&gt;()
100+
/// .AddMap("email", x => x.Email)
101+
/// .AddNestedMapper(x => x.Address, addressMapper);
102+
/// // Now supports: "city=London", "country=UK"
103+
/// </code>
104+
/// </example>
105+
IQueryBuilder<T> AddNestedMapper<TProperty>(
106+
Expression<Func<T, TProperty>> propertyExpression,
107+
IGridifyMapper<TProperty> nestedMapper,
108+
bool overrideIfExists = true);
109+
110+
/// <summary>
111+
/// Reuses mappings from a nested object's mapper by composing expressions with a prefix.
112+
/// </summary>
113+
/// <typeparam name="TProperty">The type of the nested property</typeparam>
114+
/// <param name="prefix">Prefix to prepend to nested mapping keys (e.g., "location" creates "location.city")</param>
115+
/// <param name="propertyExpression">Expression pointing to the nested property (e.g., x => x.Address)</param>
116+
/// <param name="nestedMapper">The mapper containing mappings for the nested type</param>
117+
/// <param name="overrideIfExists">Whether to override existing mappings with the same key</param>
118+
/// <returns>returns IQueryBuilder</returns>
119+
/// <example>
120+
/// <code>
121+
/// var addressMapper = new GridifyMapper&lt;Address&gt;()
122+
/// .AddMap("city", x => x.City)
123+
/// .AddMap("country", x => x.Country);
124+
///
125+
/// var builder = new QueryBuilder&lt;Company&gt;()
126+
/// .AddMap("name", x => x.Name)
127+
/// .AddNestedMapper("location", x => x.Address, addressMapper);
128+
/// // Now supports: "location.city=London", "location.country=UK"
129+
/// </code>
130+
/// </example>
131+
IQueryBuilder<T> AddNestedMapper<TProperty>(
132+
string prefix,
133+
Expression<Func<T, TProperty>> propertyExpression,
134+
IGridifyMapper<TProperty> nestedMapper,
135+
bool overrideIfExists = true);
136+
137+
/// <summary>
138+
/// Reuses mappings from a custom mapper class for a nested object by composing expressions.
139+
/// Merges mappings directly without a prefix.
140+
/// </summary>
141+
/// <typeparam name="TMapper">The mapper class type that implements IGridifyMapper&lt;TProperty&gt;</typeparam>
142+
/// <param name="propertyExpression">Expression pointing to the nested property (e.g., x => x.Address)</param>
143+
/// <param name="overrideIfExists">Whether to override existing mappings with the same key</param>
144+
/// <returns>returns IQueryBuilder</returns>
145+
/// <example>
146+
/// <code>
147+
/// public class AddressMapper : GridifyMapper&lt;Address&gt;
148+
/// {
149+
/// public AddressMapper()
150+
/// {
151+
/// AddMap("city", q => q.City);
152+
/// AddMap("country", q => q.Country);
153+
/// }
154+
/// }
155+
///
156+
/// var builder = new QueryBuilder&lt;User&gt;()
157+
/// .AddMap("email", x => x.Email)
158+
/// .AddNestedMapper&lt;AddressMapper&gt;(x => x.Address);
159+
/// // Uses AddressMapper and merges mappings: "city=London", "country=UK"
160+
/// </code>
161+
/// </example>
162+
IQueryBuilder<T> AddNestedMapper<TMapper>(
163+
Expression<Func<T, object>> propertyExpression,
164+
bool overrideIfExists = true)
165+
where TMapper : new();
166+
167+
/// <summary>
168+
/// Reuses mappings from a custom mapper class for a nested object by composing expressions with a prefix.
169+
/// </summary>
170+
/// <typeparam name="TMapper">The mapper class type that implements IGridifyMapper&lt;TProperty&gt;</typeparam>
171+
/// <param name="prefix">Prefix to prepend to nested mapping keys (e.g., "location" creates "location.city")</param>
172+
/// <param name="propertyExpression">Expression pointing to the nested property (e.g., x => x.Address)</param>
173+
/// <param name="overrideIfExists">Whether to override existing mappings with the same key</param>
174+
/// <returns>returns IQueryBuilder</returns>
175+
/// <example>
176+
/// <code>
177+
/// public class AddressMapper : GridifyMapper&lt;Address&gt;
178+
/// {
179+
/// public AddressMapper()
180+
/// {
181+
/// AddMap("city", q => q.City);
182+
/// AddMap("country", q => q.Country);
183+
/// }
184+
/// }
185+
///
186+
/// var builder = new QueryBuilder&lt;Company&gt;()
187+
/// .AddMap("name", x => x.Name)
188+
/// .AddNestedMapper&lt;AddressMapper&gt;("location", x => x.Address);
189+
/// // Uses AddressMapper with prefix: "location.city=London", "location.country=UK"
190+
/// </code>
191+
/// </example>
192+
IQueryBuilder<T> AddNestedMapper<TMapper>(
193+
string prefix,
194+
Expression<Func<T, object>> propertyExpression,
195+
bool overrideIfExists = true)
196+
where TMapper : new();
197+
84198
IQueryBuilder<T> RemoveMap(IGMap<T> map);
85199

86200
/// <summary>

src/Gridify/QueryBuilder.cs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,51 @@ public IQueryBuilder<T> AddCompositeMap(string from, Func<string, object>? conve
159159
return this;
160160
}
161161

162+
/// <inheritdoc />
163+
public IQueryBuilder<T> AddNestedMapper<TProperty>(
164+
Expression<Func<T, TProperty>> propertyExpression,
165+
IGridifyMapper<TProperty> nestedMapper,
166+
bool overrideIfExists = true)
167+
{
168+
_mapper ??= new GridifyMapper<T>(true);
169+
_mapper.AddNestedMapper(propertyExpression, nestedMapper, overrideIfExists);
170+
return this;
171+
}
172+
173+
/// <inheritdoc />
174+
public IQueryBuilder<T> AddNestedMapper<TProperty>(
175+
string prefix,
176+
Expression<Func<T, TProperty>> propertyExpression,
177+
IGridifyMapper<TProperty> nestedMapper,
178+
bool overrideIfExists = true)
179+
{
180+
_mapper ??= new GridifyMapper<T>(true);
181+
_mapper.AddNestedMapper(prefix, propertyExpression, nestedMapper, overrideIfExists);
182+
return this;
183+
}
184+
185+
/// <inheritdoc />
186+
public IQueryBuilder<T> AddNestedMapper<TMapper>(
187+
Expression<Func<T, object>> propertyExpression,
188+
bool overrideIfExists = true)
189+
where TMapper : new()
190+
{
191+
_mapper ??= new GridifyMapper<T>(true);
192+
_mapper.AddNestedMapper<TMapper>(propertyExpression, overrideIfExists);
193+
return this;
194+
}
195+
196+
/// <inheritdoc />
197+
public IQueryBuilder<T> AddNestedMapper<TMapper>(
198+
string prefix,
199+
Expression<Func<T, object>> propertyExpression,
200+
bool overrideIfExists = true)
201+
where TMapper : new()
202+
{
203+
_mapper ??= new GridifyMapper<T>(true);
204+
_mapper.AddNestedMapper<TMapper>(prefix, propertyExpression, overrideIfExists);
205+
return this;
206+
}
162207

163208
/// <inheritdoc />
164209
public IQueryBuilder<T> RemoveMap(IGMap<T> map)

0 commit comments

Comments
 (0)