-
Notifications
You must be signed in to change notification settings - Fork 7
Expand file tree
/
Copy pathCollectionRule.cs
More file actions
317 lines (275 loc) · 14.2 KB
/
Copy pathCollectionRule.cs
File metadata and controls
317 lines (275 loc) · 14.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
namespace CoreEx.Validation.Rules;
/// <summary>
/// Provides a collection (<see cref="IEnumerable{T}"/>) validation including item-based validation and duplicate checking.
/// </summary>
/// <typeparam name="TEntity">The entity <see cref="Type"/>.</typeparam>
/// <typeparam name="TProperty">The property <see cref="Type"/> (<see cref="IEnumerable{T}"/>).</typeparam>
/// <typeparam name="TItem">The item <see cref="Type"/>.</typeparam>
public sealed class CollectionRule<TEntity, TProperty, TItem> : PropertyRuleBase<TEntity, TProperty> where TEntity : class where TProperty : IEnumerable<TItem?>
{
private readonly Func<PropertyContext<TEntity, TProperty>, int>? _minCount;
private readonly Func<PropertyContext<TEntity, TProperty>, int?>? _maxCount;
private readonly With _with;
/// <summary>
/// Initializes a new instance of the <see cref="CollectionRule{TEntity, TProperty, TItem}"/> class.
/// </summary>
/// <param name="minCount">The minimum count.</param>
/// <param name="maxCount">The maximum count.</param>
/// <param name="with">Extends configuration <see cref="With"/>.</param>
public CollectionRule(Func<PropertyContext<TEntity, TProperty>, int>? minCount, Func<PropertyContext<TEntity, TProperty>, int?>? maxCount, Func<With, With>? with)
{
_minCount = minCount;
_maxCount = maxCount;
var w = new With(this);
_with = with?.Invoke(w) ?? w;
}
/// <inheritdoc/>
protected async override Task OnValidateAsync(PropertyContext<TEntity, TProperty> context, CancellationToken cancellationToken)
{
var minCount = _minCount?.Invoke(context) ?? 0;
var maxCount = _maxCount?.Invoke(context);
if (minCount < 0)
throw new InvalidOperationException("Minimum count must not be negative.");
if (maxCount.HasValue)
{
if (maxCount.Value < 0)
throw new InvalidOperationException("Maximum count must not be negative.");
if (maxCount.Value < minCount)
throw new InvalidOperationException("Maximum count must not be less than minimum count.");
}
await _with.ValidateAsync(context, minCount, maxCount, cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Provides additional configuration options for the <see cref="CollectionRule{TEntity, TProperty, TItem}"/>.
/// </summary>
public sealed class With
{
private readonly CollectionRule<TEntity, TProperty, TItem> _rule;
private Func<ValidationArgs, IValidatorEx<TItem>>? _getValidator;
private bool _hasAllowNullItems;
#pragma warning disable CA1859 // Use concrete types when possible for improved performance; not applicable here as interface is needed.
private IItemDuplicateCheck? _itemDuplicateCheck;
#pragma warning restore CA1859 // Use concrete types when possible for improved performance
/// <summary>
/// Initializes a new instance of the <see cref="With"/> class.
/// </summary>
internal With(CollectionRule<TEntity, TProperty, TItem> rule) => _rule = rule;
/// <summary>
/// Indicates that one or more items can be <see langword="null"/>.
/// </summary>
/// <returns>The <see cref="With"/> to support fluent-style method-chaining.</returns>
public With AllowNullItems()
{
_hasAllowNullItems = true;
return this;
}
/// <summary>
/// Sets the specified <b>Item</b> <paramref name="configure"/>.
/// </summary>
/// <returns>The <see cref="With"/> to support fluent-style method-chaining.</returns>
public With WithItemValidator(Action<InlineValidator<TItem>.Validator>? configure) => WithItemValidator(new ValidatingInlineValidator<TItem>(configure));
/// <summary>
/// Sets the specified <b>Item</b> <paramref name="validator"/>.
/// </summary>
/// <returns>The <see cref="With"/> to support fluent-style method-chaining.</returns>
public With WithItemValidator(IValidatorEx<TItem> validator)
{
_getValidator = _getValidator is not null ? throw new InvalidOperationException("The collection rule can only have one validator.") : _ => validator.ThrowIfNull();
return this;
}
/// <summary>
/// Sets the specified <b>Item</b> <typeparamref name="TValidator"/> service (resolved at validation runtime).
/// </summary>
/// <typeparam name="TValidator">The property validator <see cref="Type"/>.</typeparam>
/// <returns>The <see cref="With"/> to support fluent-style method-chaining.</returns>
public With WithItemValidator<TValidator>() where TValidator : IValidatorEx<TItem>
{
_getValidator = _getValidator is not null ? throw new InvalidOperationException("The collection rule can only have one validator.") : args => CoreEx.Validation.Validator.Get<TValidator>(args.ServiceProvider);
return this;
}
/// <summary>
/// Sets the specified keyed <b>Item</b> <typeparamref name="TValidator"/> service (resolved at validation runtime).
/// </summary>
/// <typeparam name="TValidator">The property validator <see cref="Type"/>.</typeparam>
/// <param name="serviceKey">The service key.</param>
/// <returns>The <see cref="With"/> to support fluent-style method-chaining.</returns>
public With WithItemKeyedValidator<TValidator>(object? serviceKey) where TValidator : IValidatorEx<TItem>
{
_getValidator = _getValidator is not null ? throw new InvalidOperationException("The collection rule can only have one validator.") : args => CoreEx.Validation.Validator.GetKeyed<TValidator>(serviceKey, args.ServiceProvider);
return this;
}
/// <summary>
/// Sets the generic duplicate checking logic.
/// </summary>
/// <typeparam name="TKey">The key <see cref="Type"/>.</typeparam>
/// <param name="keySelector">The key selector.</param>
/// <param name="comparer">The equality comparer.</param>
/// <param name="duplicateText">The duplicate <see cref="LText"/> to be used in the error message.</param>
/// <returns>The <see cref="With"/> to support fluent-style method-chaining.</returns>
internal With WithDuplicateCheckingInternal<TKey>(Func<TItem, TKey> keySelector, IEqualityComparer<TKey>? comparer, Func<LText> duplicateText)
{
_itemDuplicateCheck = _itemDuplicateCheck is not null ? throw new InvalidOperationException("The collection rule can only have one duplicate checker.") : new ItemDuplicateCheck<TKey>(keySelector, comparer, duplicateText);
return this;
}
/// <summary>
/// Validates each item within the collection.
/// </summary>
/// <param name="context"></param>
/// <param name="minCount">The minimum count.</param>
/// <param name="maxCount">The maximum count.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param>
internal async Task ValidateAsync(PropertyContext<TEntity, TProperty> context, int minCount, int? maxCount, CancellationToken cancellationToken)
{
// Fast path where only checking for count.
if (_hasAllowNullItems && _getValidator is null && _itemDuplicateCheck is null && context.Value is ICollection coll)
{
PostEnumerationValidation(context, false, minCount, maxCount, coll.Count);
return;
}
// Enumerate and validate each item.
var index = 0;
var hasNullItem = false;
var hasItemError = false;
var hasDuplicate = false;
var duplicateChecker = _itemDuplicateCheck?.CreateDuplicateChecker();
foreach (var item in context.Value!)
{
// Handle null item(s).
if (item is null)
{
if (!_hasAllowNullItems)
hasNullItem = true;
index++;
continue;
}
// Validate the item.
var hasError = false;
if (_getValidator is not null)
{
// Create the context args.
var args = CreateValidationArgs(context, index);
var last = context.GetCollectionIndexSafe();
context.SetCollectionIndex(index);
// Validate the item and merge the result.
try
{
var vi = _getValidator.Invoke(args);
var r = vi is ValidatingInlineValidator<TItem> vilv
? await vilv.ValidateEntityAsync(item, args, cancellationToken).ConfigureAwait(false)
: await vi.ValidateAsync(item, args, cancellationToken).ConfigureAwait(false);
context.MergeResult(r);
if (r.HasErrors)
hasItemError = hasError = true;
}
finally
{
context.SetCollectionIndex(last);
}
}
// Check for duplicates where applicable.
if (!hasError && !hasDuplicate && duplicateChecker?.IsDuplicate(item) is true)
hasDuplicate = true;
index++;
}
// Check for duplicates and error accordingly.
if (!hasItemError && hasDuplicate)
context.AddError(_rule.ErrorText ?? ValidatorStrings.DuplicateValueFormat, _itemDuplicateCheck!.DuplicateText());
// Perform the standard post enumeration validation.
PostEnumerationValidation(context, hasNullItem, minCount, maxCount, index);
}
/// <summary>
/// Creates the <see cref="ValidationArgs"/> for the specified <paramref name="index"/>.
/// </summary>
private static ValidationArgs CreateValidationArgs(PropertyContext<TEntity, TProperty> context, int index)
{
var args = context.CreateValidationArgs();
var indexer = $"[{index}]";
args.FullyQualifiedEntityName += indexer;
args.FullyQualifiedJsonEntityName += indexer;
return args;
}
/// <summary>
/// Performs the standatd post enumeration validation.
/// </summary>
private void PostEnumerationValidation(PropertyContext<TEntity, TProperty> context, bool hasNullItem, int minCount, int? maxCount, int count)
{
// Emit the null item error.
if (hasNullItem)
context.AddError(_rule.ErrorText ?? ValidatorStrings.CollectionNullItemFormat);
// Check the length/count.
if (count < minCount)
context.AddError(_rule.ErrorText ?? ValidatorStrings.MinCountFormat, minCount);
else if (maxCount.HasValue && count > maxCount.Value)
context.AddError(_rule.ErrorText ?? ValidatorStrings.MaxCountFormat, maxCount);
}
}
/// <summary>
/// Enables the duplicate checking configuration for items within a collection.
/// </summary>
internal interface IItemDuplicateCheck
{
/// <summary>
/// Gets the duplicate <see cref="LText"/> to be used in the error message.
/// </summary>
Func<LText> DuplicateText { get; }
/// <summary>
/// Create the runtime <see cref="IItemDuplicateChecker"/>.
/// </summary>
/// <returns>The <see cref="IItemDuplicateChecker"/>.</returns>
IItemDuplicateChecker CreateDuplicateChecker();
}
/// <summary>
/// Enables the runtime duplicate checking for items within a collection.
/// </summary>
internal interface IItemDuplicateChecker
{
/// <summary>
/// Indicates whether the specified <paramref name="item"/> is a duplicate.
/// </summary>
/// <param name="item">The item.</param>
/// <returns><see langword="true"/> indicates a duplicate; otherwise, <see langword="false"/>.</returns>
bool IsDuplicate(TItem item);
}
/// <summary>
/// Provides duplicate checking configuration for items within a collection.
/// </summary>
/// <typeparam name="TKey">The key <see cref="Type"/>.</typeparam>
/// <param name="keySelector">The key selector.</param>
/// <param name="comparer">The equality comparer.</param>
/// <param name="duplicateText">The duplicate <see cref="LText"/> function.</param>
internal sealed class ItemDuplicateCheck<TKey>(Func<TItem, TKey> keySelector, IEqualityComparer<TKey>? comparer, Func<LText> duplicateText) : IItemDuplicateCheck
{
/// <summary>
/// Gets the key selector.
/// </summary>
public Func<TItem, TKey> KeySelector { get; } = keySelector.ThrowIfNull();
/// <summary>
/// Gets the equality comparer.
/// </summary>
public IEqualityComparer<TKey>? Comparer { get; } = comparer;
/// <inheritdoc/>
public Func<LText> DuplicateText { get; } = duplicateText.ThrowIfNull();
/// <inheritdoc/>
public IItemDuplicateChecker CreateDuplicateChecker() => new ItemDuplicateChecker<TKey>(this);
}
/// <summary>
/// Provides runtime duplicate checking for items within a collection.
/// </summary>
/// <typeparam name="TKey">The key <see cref="Type"/>.</typeparam>
internal sealed class ItemDuplicateChecker<TKey> : IItemDuplicateChecker
{
private readonly ItemDuplicateCheck<TKey> _config;
private readonly HashSet<TKey> _keys;
/// <summary>
/// Initializes a new instance of the <see cref="ItemDuplicateChecker{TKey}"/> class.
/// </summary>
/// <param name="config">The <see cref="ItemDuplicateCheck{TKey}"/>.</param>
public ItemDuplicateChecker(ItemDuplicateCheck<TKey> config)
{
_config = config.ThrowIfNull();
_keys = new HashSet<TKey>(_config.Comparer);
}
/// <inheritdoc/>
public bool IsDuplicate(TItem item) => !_keys.Add(_config.KeySelector(item));
}
}