generated from Avanade/avanade-template
-
Notifications
You must be signed in to change notification settings - Fork 7
/
Copy pathValidatorT.cs
234 lines (204 loc) · 13.1 KB
/
ValidatorT.cs
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
// Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/CoreEx
using CoreEx.Abstractions.Reflection;
using CoreEx.Entities;
using CoreEx.Localization;
using CoreEx.Results;
using System;
using System.Linq.Expressions;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
namespace CoreEx.Validation
{
/// <summary>
/// Provides entity validation.
/// </summary>
/// <typeparam name="TEntity">The entity <see cref="Type"/>.</typeparam>
public class Validator<TEntity> : ValidatorBase<TEntity> where TEntity : class
{
private RuleSet<TEntity>? _currentRuleSet;
private Func<ValidationContext<TEntity>, CancellationToken, Task<Result>>? _additionalAsync;
/// <inheritdoc/>
public override Task<ValidationContext<TEntity>> ValidateAsync(TEntity value, ValidationArgs? args = null, CancellationToken cancellationToken = default)
{
return ValidationInvoker.Current.InvokeAsync(this, async (_, cancellationToken) =>
{
var context = new ValidationContext<TEntity>(value, args ?? new ValidationArgs());
if (value is null)
{
context.AddMessage(nameof(value), nameof(value), MessageType.Error, ValidatorStrings.MandatoryFormat, Validation.ValueTextDefault);
return context;
}
// Validate each of the property rules.
foreach (var rule in Rules)
{
await rule.ValidateAsync(context, cancellationToken).ConfigureAwait(false);
// Where in a failure state no further validation should be performed.
if (context.FailureResult.HasValue)
return context;
}
var result = await OnValidateAsync(context, cancellationToken).ConfigureAwait(false);
if (result.IsSuccess && _additionalAsync != null)
result = await _additionalAsync(context, cancellationToken).ConfigureAwait(false);
context.SetFailureResult(result);
return context;
}, cancellationToken);
}
/// <summary>
/// Validate the entity value (post all configured property rules) enabling additional validation logic to be added by the inheriting classes.
/// </summary>
/// <param name="context">The <see cref="ValidationContext{TEntity}"/>.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param>
/// <returns>The corresponding <see cref="Result"/>.</returns>
/// <remarks>The <paramref name="context"/> (see <see cref="ValidationContext{TEntity}"/> 'AddError' and related methods should be used for specific validation messages. Any <see cref="Result.IsFailure"/> <see cref="Result"/> will
/// override any existing validations and no further validations will occur.</remarks>
protected virtual Task<Result> OnValidateAsync(ValidationContext<TEntity> context, CancellationToken cancellationToken) => Task.FromResult(Result.Success);
/// <summary>
/// Adds a <see cref="PropertyRule{TEntity, TProperty}"/> to the validator.
/// </summary>
/// <typeparam name="TProperty">The property <see cref="Type"/>.</typeparam>
/// <param name="propertyExpression">The <see cref="Expression"/> to reference the entity property.</param>
/// <returns>The <see cref="PropertyRule{TEntity, TProperty}"/>.</returns>
public override IPropertyRule<TEntity, TProperty> Property<TProperty>(Expression<Func<TEntity, TProperty>> propertyExpression)
{
// Depending on the the state update either the ruleset rules or the underlying rules.
if (_currentRuleSet == null)
return base.Property(propertyExpression);
return _currentRuleSet.Property(propertyExpression);
}
/// <summary>
/// Adds the <see cref="PropertyRule{TEntity, TProperty}"/> to the validator enabling additional configuration via the specified <paramref name="property"/> action.
/// </summary>
/// <typeparam name="TProperty">The property <see cref="Type"/>.</typeparam>
/// <param name="propertyExpression">The <see cref="Expression"/> to reference the entity property.</param>
/// <param name="property">The action to act on the created <see cref="PropertyRule{TEntity, TProperty}"/>.</param>
/// <returns>The <see cref="Validator{TEntity}"/>.</returns>
public Validator<TEntity> HasProperty<TProperty>(Expression<Func<TEntity, TProperty>> propertyExpression, Action<IPropertyRule<TEntity, TProperty>>? property = null)
{
var p = Property(propertyExpression);
property?.Invoke(p);
return this;
}
/// <summary>
/// Adds the <see cref="PropertyRule{TEntity, TProperty}"/> to the validator enabling additional configuration via the specified <paramref name="property"/> action.
/// </summary>
/// <typeparam name="TProperty">The property <see cref="Type"/>.</typeparam>
/// <param name="propertyExpression">The <see cref="Expression"/> to reference the entity property.</param>
/// <param name="property">The action to act on the created <see cref="PropertyRule{TEntity, TProperty}"/>.</param>
/// <returns>The <see cref="Validator{TEntity}"/>.</returns>
/// <remarks>This is a synonym for <see cref="HasProperty{TProperty}(Expression{Func{TEntity, TProperty}}, Action{IPropertyRule{TEntity, TProperty}}?)"/>.</remarks>
public Validator<TEntity> HasRuleFor<TProperty>(Expression<Func<TEntity, TProperty>> propertyExpression, Action<IPropertyRule<TEntity, TProperty>>? property = null) => HasProperty(propertyExpression, property);
/// <summary>
/// Adds a <see cref="IncludeBaseRule{TEntity, TInclude}"/> to the validator to enable a same typed validator to be included within the validator rule set.
/// </summary>
/// <param name="include">The <see cref="IValidatorEx{TInclude}"/> to include (add).</param>
/// <returns>The <see cref="Validator{TEntity}"/>.</returns>
public Validator<TEntity> Include(IValidatorEx<TEntity> include)
{
include.ThrowIfNull(nameof(include));
if (_currentRuleSet == null)
Rules.Add(new IncludeBaseRule<TEntity, TEntity>(include));
else
_currentRuleSet.Rules.Add(new IncludeBaseRule<TEntity, TEntity>(include));
return this;
}
/// <summary>
/// Adds a <see cref="IncludeBaseRule{TEntity, TInclude}"/> to the validator to enable a base validator to be included within the validator rule set.
/// </summary>
/// <typeparam name="TInclude">The include <see cref="Type"/> in which <typeparamref name="TEntity"/> inherits from.</typeparam>
/// <param name="include">The <see cref="IValidatorEx{TInclude}"/> to include (add).</param>
/// <returns>The <see cref="Validator{TEntity}"/>.</returns>
public Validator<TEntity> IncludeBase<TInclude>(IValidatorEx<TInclude> include) where TInclude : class
{
include.ThrowIfNull(nameof(include));
if (!typeof(TEntity).GetTypeInfo().IsSubclassOf(typeof(TInclude)))
throw new ArgumentException($"Type {typeof(TEntity).Name} must inherit from {typeof(TInclude).Name}.", nameof(include));
if (_currentRuleSet == null)
Rules.Add(new IncludeBaseRule<TEntity, TInclude>(include));
else
_currentRuleSet.Rules.Add(new IncludeBaseRule<TEntity, TInclude>(include));
return this;
}
/// <summary>
/// Validate the entity value (post all configured property rules) enabling additional validation logic to be added.
/// </summary>
/// <param name="additionalAsync">The asynchronous function to invoke.</param>
/// <returns>The <see cref="Validator{TEntity}"/>.</returns>
public Validator<TEntity> AdditionalAsync(Func<ValidationContext<TEntity>, CancellationToken, Task<Result>> additionalAsync)
{
if (_additionalAsync != null)
throw new InvalidOperationException("Additional can only be defined once for a Validator.");
_additionalAsync = additionalAsync.ThrowIfNull(nameof(additionalAsync));
return this;
}
/// <summary>
/// Adds a <see cref="RuleSet(Predicate{ValidationContext{TEntity}}, Action)"/> that is conditionally invoked where the <paramref name="predicate"/> is true.
/// </summary>
/// <param name="predicate">The predicate.</param>
/// <param name="action">The action to invoke where the <see cref="Property"/> method will update the corresponding <see cref="RuleSet(Predicate{ValidationContext{TEntity}}, Action)"/> <see cref="ValidatorBase{TEntity}.Rules">rules</see>.</param>
/// <returns>The <see cref="RuleSet{TEntity}"/>.</returns>
public RuleSet<TEntity> RuleSet(Predicate<ValidationContext<TEntity>> predicate, Action action)
{
predicate.ThrowIfNull(nameof(predicate));
action.ThrowIfNull(nameof(action));
return SetRuleSet(new RuleSet<TEntity>(predicate), (v) => action());
}
/// <summary>
/// Adds a <see cref="RuleSet(Predicate{ValidationContext{TEntity}}, Action)"/> that is conditionally invoked where the <paramref name="predicate"/> is true.
/// </summary>
/// <param name="predicate">The predicate.</param>
/// <param name="action">The action to invoke where the passed <see cref="Validator{TEntity}"/> enables the <see cref="RuleSet(Predicate{ValidationContext{TEntity}}, Action)"/> <see cref="ValidatorBase{TEntity}.Rules">rules</see> to be updated.</param>
/// <returns>The <see cref="Validator{TEntity}"/>.</returns>
public Validator<TEntity> HasRuleSet(Predicate<ValidationContext<TEntity>> predicate, Action<Validator<TEntity>> action)
{
predicate.ThrowIfNull(nameof(predicate));
action.ThrowIfNull(nameof(action));
SetRuleSet(new RuleSet<TEntity>(predicate), action);
return this;
}
/// <summary>
/// Sets the rule set and invokes the action.
/// </summary>
private RuleSet<TEntity> SetRuleSet(RuleSet<TEntity> ruleSet, Action<Validator<TEntity>> action)
{
if (_currentRuleSet != null)
throw new InvalidOperationException("RuleSets only support a single level of nesting.");
// Invoke the action that will add the entries to the ruleset not the underlying rules.
if (action != null)
{
_currentRuleSet = ruleSet;
action(this);
_currentRuleSet = null;
}
// Add the ruleset to the rules.
Rules.Add(ruleSet);
return ruleSet;
}
/// <summary>
/// Throws a <see cref="ValidationException"/> where the <see cref="MessageItem"/> <see cref="MessageItem.Property"/> is set based on the <paramref name="propertyExpression"/>.
/// </summary>
/// <typeparam name="TProperty">The property <see cref="Type"/>.</typeparam>
/// <param name="propertyExpression">The <see cref="Expression"/> to reference the entity property.</param>
/// <param name="text">The message text.</param>
public void ThrowValidationException<TProperty>(Expression<Func<TEntity, TProperty>> propertyExpression, LText text)
{
var p = PropertyExpression.Create(propertyExpression);
throw new ValidationException(MessageItem.CreateErrorMessage(ValidationArgs.DefaultUseJsonNames ? p.JsonName : p.Name, text));
}
/// <summary>
/// Throws a <see cref="ValidationException"/> where the <see cref="MessageItem"/> <see cref="MessageItem.Property"/> is set based on the <paramref name="propertyExpression"/>. The property
/// friendly text and <paramref name="propertyValue"/> are automatically passed as the first two arguments to the string formatter.
/// </summary>
/// <typeparam name="TProperty">The property <see cref="Type"/>.</typeparam>
/// <param name="propertyExpression">The <see cref="Expression"/> to reference the entity property.</param>
/// <param name="format">The composite format string.</param>
/// <param name="propertyValue">The property values (to be used as part of the format).</param>
/// <param name="values"></param>
public void ThrowValidationException<TProperty>(Expression<Func<TEntity, TProperty>> propertyExpression, LText format, TProperty propertyValue, params object[] values)
{
var p = PropertyExpression.Create(propertyExpression);
throw new ValidationException(MessageItem.CreateErrorMessage(ValidationArgs.DefaultUseJsonNames ? p.JsonName : p.Name,
string.Format(System.Globalization.CultureInfo.CurrentCulture, format, [p.Text, propertyValue!, .. values])));
}
}
}