feat: add property path support
This commit is contained in:
parent
de2d0e2693
commit
8f54ff4e31
8 changed files with 276 additions and 49 deletions
76
src/request.validation.tests/PropertyPathTests.cs
Normal file
76
src/request.validation.tests/PropertyPathTests.cs
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
// Copyright (c) The Geekeey Authors
|
||||||
|
// SPDX-License-Identifier: EUPL-1.2
|
||||||
|
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace Geekeey.Request.Validation.Tests;
|
||||||
|
|
||||||
|
internal sealed class PropertyPathTests
|
||||||
|
{
|
||||||
|
private static readonly JsonSerializerOptions CamelCaseJsonOptions = new()
|
||||||
|
{
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||||
|
};
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task I_serialize_property_paths_using_the_json_naming_policy()
|
||||||
|
{
|
||||||
|
var problem = new Problem
|
||||||
|
{
|
||||||
|
PropertyPath = "Address.Street",
|
||||||
|
Message = "Street is required.",
|
||||||
|
};
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(problem, CamelCaseJsonOptions);
|
||||||
|
|
||||||
|
await Assert.That(json).IsEqualTo(/*lang=json,strict*/ """{"propertyPath":"address.street","severity":0,"message":"Street is required.","code":null,"attemptedValue":null}""");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task I_serialize_indexed_property_paths_using_the_json_naming_policy()
|
||||||
|
{
|
||||||
|
var problem = new Problem
|
||||||
|
{
|
||||||
|
PropertyPath = "Members[1].Name",
|
||||||
|
Message = "Member name is required.",
|
||||||
|
};
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(problem, CamelCaseJsonOptions);
|
||||||
|
|
||||||
|
await Assert.That(json).IsEqualTo(/*lang=json,strict*/ """{"propertyPath":"members[1].name","severity":0,"message":"Member name is required.","code":null,"attemptedValue":null}""");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task I_can_combine_property_paths_with_the_plus_operator()
|
||||||
|
{
|
||||||
|
var propertyPath = "Address" + (PropertyPath)"Street";
|
||||||
|
|
||||||
|
await Assert.That(propertyPath).IsEqualTo("Address.Street");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task I_can_combine_property_paths_with_the_plus_operator_when_one_side_is_empty()
|
||||||
|
{
|
||||||
|
var leftEmpty = "" + (PropertyPath)"Street";
|
||||||
|
var rightEmpty = (PropertyPath)"Address" + "";
|
||||||
|
|
||||||
|
await Assert.That(leftEmpty).IsEqualTo("Street");
|
||||||
|
await Assert.That(rightEmpty).IsEqualTo("Address");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task I_can_append_a_collection_index_with_the_plus_operator()
|
||||||
|
{
|
||||||
|
var propertyPath = (PropertyPath)"Members" + 1;
|
||||||
|
|
||||||
|
await Assert.That(propertyPath).IsEqualTo("Members[1]");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task I_can_append_a_collection_index_with_the_plus_operator_when_the_property_name_is_empty()
|
||||||
|
{
|
||||||
|
var propertyPath = (PropertyPath)"" + 1;
|
||||||
|
|
||||||
|
await Assert.That(propertyPath).IsEqualTo("[1]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -311,7 +311,7 @@ internal sealed class RuleBuilderExtensionsTests
|
||||||
.Throws<ArgumentOutOfRangeException>();
|
.Throws<ArgumentOutOfRangeException>();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task AssertSingleProblem(Validation validation, string propertyName, string message)
|
private static async Task AssertSingleProblem(Validation validation, string propertyPath, string message)
|
||||||
{
|
{
|
||||||
await Assert.That(validation.Problems).Count().IsEqualTo(1);
|
await Assert.That(validation.Problems).Count().IsEqualTo(1);
|
||||||
|
|
||||||
|
|
@ -319,7 +319,7 @@ internal sealed class RuleBuilderExtensionsTests
|
||||||
|
|
||||||
using (Assert.Multiple())
|
using (Assert.Multiple())
|
||||||
{
|
{
|
||||||
await Assert.That(problem.PropertyName).IsEqualTo(propertyName);
|
await Assert.That(problem.PropertyPath).IsEqualTo(propertyPath);
|
||||||
await Assert.That(problem.Message).IsEqualTo(message);
|
await Assert.That(problem.Message).IsEqualTo(message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ internal sealed class ValidatorTests
|
||||||
|
|
||||||
using (Assert.Multiple())
|
using (Assert.Multiple())
|
||||||
{
|
{
|
||||||
await Assert.That(problem.PropertyName).IsEqualTo(nameof(Person.Name));
|
await Assert.That(problem.PropertyPath).IsEqualTo(nameof(Person.Name));
|
||||||
await Assert.That(problem.Message).IsEqualTo("Name is required.");
|
await Assert.That(problem.Message).IsEqualTo("Name is required.");
|
||||||
await Assert.That(problem.Code).IsEqualTo("NAME_REQUIRED");
|
await Assert.That(problem.Code).IsEqualTo("NAME_REQUIRED");
|
||||||
await Assert.That(problem.Severity).IsEqualTo(Severity.Warning);
|
await Assert.That(problem.Severity).IsEqualTo(Severity.Warning);
|
||||||
|
|
@ -58,7 +58,7 @@ internal sealed class ValidatorTests
|
||||||
});
|
});
|
||||||
|
|
||||||
await Assert.That(result.Problems).Count().IsEqualTo(1);
|
await Assert.That(result.Problems).Count().IsEqualTo(1);
|
||||||
await Assert.That(result.Problems.Single().PropertyName).IsEqualTo("Address.Street");
|
await Assert.That(result.Problems.Single().PropertyPath).IsEqualTo("Address.Street");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
|
@ -87,7 +87,7 @@ internal sealed class ValidatorTests
|
||||||
});
|
});
|
||||||
|
|
||||||
await Assert.That(result.Problems).Count().IsEqualTo(1);
|
await Assert.That(result.Problems).Count().IsEqualTo(1);
|
||||||
await Assert.That(result.Problems.Single().PropertyName).IsEqualTo("Members[1].Name");
|
await Assert.That(result.Problems.Single().PropertyPath).IsEqualTo("Members[1].Name");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
|
@ -105,7 +105,7 @@ internal sealed class ValidatorTests
|
||||||
|
|
||||||
using (Assert.Multiple())
|
using (Assert.Multiple())
|
||||||
{
|
{
|
||||||
await Assert.That(problem.PropertyName).IsEqualTo("Address.Street");
|
await Assert.That(problem.PropertyPath).IsEqualTo("Address.Street");
|
||||||
await Assert.That(problem.Message).IsEqualTo("Value is required.");
|
await Assert.That(problem.Message).IsEqualTo("Value is required.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -124,7 +124,7 @@ internal sealed class ValidatorTests
|
||||||
|
|
||||||
using (Assert.Multiple())
|
using (Assert.Multiple())
|
||||||
{
|
{
|
||||||
await Assert.That(problem.PropertyName).IsEqualTo(nameof(Person.Name));
|
await Assert.That(problem.PropertyPath).IsEqualTo(nameof(Person.Name));
|
||||||
await Assert.That(problem.Message).IsEqualTo("Value is required.");
|
await Assert.That(problem.Message).IsEqualTo("Value is required.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -208,8 +208,8 @@ internal sealed class ValidatorTests
|
||||||
});
|
});
|
||||||
|
|
||||||
await Assert.That(result.Problems).Count().IsEqualTo(2);
|
await Assert.That(result.Problems).Count().IsEqualTo(2);
|
||||||
await Assert.That(result.Problems[0].PropertyName).IsEqualTo("Members[0].Name");
|
await Assert.That(result.Problems[0].PropertyPath).IsEqualTo("Members[0].Name");
|
||||||
await Assert.That(result.Problems[1].PropertyName).IsEqualTo("Members[2].Name");
|
await Assert.That(result.Problems[1].PropertyPath).IsEqualTo("Members[2].Name");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
|
@ -230,7 +230,7 @@ internal sealed class ValidatorTests
|
||||||
var result = validator.Validate(context);
|
var result = validator.Validate(context);
|
||||||
|
|
||||||
await Assert.That(result.Problems).Count().IsEqualTo(1);
|
await Assert.That(result.Problems).Count().IsEqualTo(1);
|
||||||
await Assert.That(result.Problems.Single().PropertyName).IsEqualTo("Address.Street");
|
await Assert.That(result.Problems.Single().PropertyPath).IsEqualTo("Address.Street");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ public record Problem
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The name of the property.
|
/// The name of the property.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public required string PropertyName { get; init; }
|
public required PropertyPath PropertyPath { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Custom severity level associated with the failure.
|
/// Custom severity level associated with the failure.
|
||||||
|
|
|
||||||
169
src/request.validation/PropertyPath.cs
Normal file
169
src/request.validation/PropertyPath.cs
Normal file
|
|
@ -0,0 +1,169 @@
|
||||||
|
// Copyright (c) The Geekeey Authors
|
||||||
|
// SPDX-License-Identifier: EUPL-1.2
|
||||||
|
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Geekeey.Request.Validation;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a property path used by validation problems.
|
||||||
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(PropertyPathJsonConverter))]
|
||||||
|
public readonly struct PropertyPath : IEquatable<PropertyPath>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new property path.
|
||||||
|
/// </summary>
|
||||||
|
public PropertyPath(string value)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(value);
|
||||||
|
Value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the raw property path.
|
||||||
|
/// </summary>
|
||||||
|
public string Value { get; } = string.Empty;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts a string into a property path.
|
||||||
|
/// </summary>
|
||||||
|
public static implicit operator PropertyPath(string value)
|
||||||
|
{
|
||||||
|
return new PropertyPath(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts a property path into its string representation.
|
||||||
|
/// </summary>
|
||||||
|
public static implicit operator string(PropertyPath propertyPath)
|
||||||
|
{
|
||||||
|
return propertyPath.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool Equals(PropertyPath other)
|
||||||
|
{
|
||||||
|
return string.Equals(Value, other.Value, StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override bool Equals(object? obj)
|
||||||
|
{
|
||||||
|
return obj is PropertyPath other && Equals(other);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override int GetHashCode()
|
||||||
|
{
|
||||||
|
return StringComparer.Ordinal.GetHashCode(Value ?? string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Compares two property paths for ordinal equality.
|
||||||
|
/// </summary>
|
||||||
|
public static bool operator ==(PropertyPath left, PropertyPath right)
|
||||||
|
{
|
||||||
|
return left.Equals(right);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Compares two property paths for ordinal inequality.
|
||||||
|
/// </summary>
|
||||||
|
public static bool operator !=(PropertyPath left, PropertyPath right)
|
||||||
|
{
|
||||||
|
return !left.Equals(right);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Combines two property paths using the same rules as nested validation paths.
|
||||||
|
/// </summary>
|
||||||
|
public static PropertyPath operator +(PropertyPath prefix, PropertyPath suffix)
|
||||||
|
{
|
||||||
|
if (prefix.Value is not { Length: > 0 })
|
||||||
|
{
|
||||||
|
return suffix;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (suffix.Value is not { Length: > 0 })
|
||||||
|
{
|
||||||
|
return prefix;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $"{prefix}.{suffix}";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Appends a collection index to a property path.
|
||||||
|
/// </summary>
|
||||||
|
public static PropertyPath operator +(PropertyPath propertyPath, int index)
|
||||||
|
{
|
||||||
|
return propertyPath.Value is { Length: > 0 } ? $"{propertyPath}[{index}]" : $"[{index}]";
|
||||||
|
}
|
||||||
|
|
||||||
|
internal string ToJsonName(JsonNamingPolicy? namingPolicy)
|
||||||
|
{
|
||||||
|
var value = Value ?? string.Empty;
|
||||||
|
if (namingPolicy is null || value.Length is 0)
|
||||||
|
{
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
var builder = new StringBuilder(value.Length);
|
||||||
|
var first = true;
|
||||||
|
|
||||||
|
foreach (var range in value.AsSpan().Split('.'))
|
||||||
|
{
|
||||||
|
if (!first)
|
||||||
|
{
|
||||||
|
builder.Append('.');
|
||||||
|
}
|
||||||
|
|
||||||
|
var segment = value.AsSpan()[range];
|
||||||
|
if (segment.Length is not 0)
|
||||||
|
{
|
||||||
|
if (segment.IndexOf('[') is >= 0 and var x)
|
||||||
|
{
|
||||||
|
builder.Append(namingPolicy.ConvertName(segment[..x].ToString()));
|
||||||
|
builder.Append(segment[x..]);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
builder.Append(namingPolicy.ConvertName(segment.ToString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class PropertyPathJsonConverter : JsonConverter<PropertyPath>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override PropertyPath Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
if (reader.TokenType is JsonTokenType.String)
|
||||||
|
{
|
||||||
|
return new PropertyPath(reader.GetString()!);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new JsonException($"Expected {nameof(JsonTokenType.String)} but got {reader.TokenType}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override void Write(Utf8JsonWriter writer, PropertyPath value, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
writer.WriteStringValue(value.ToJsonName(options.PropertyNamingPolicy));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -18,14 +18,14 @@ internal abstract record Rule<T, TProperty> : Rule
|
||||||
{
|
{
|
||||||
protected Rule(Expression expression)
|
protected Rule(Expression expression)
|
||||||
{
|
{
|
||||||
PropertyName = GetPropertyPath(expression);
|
PropertyPath = GetPropertyPath(expression);
|
||||||
}
|
}
|
||||||
|
|
||||||
public string PropertyName { get; }
|
public PropertyPath PropertyPath { get; }
|
||||||
|
|
||||||
public IReadOnlyList<IRuleStep<TProperty>> Steps { get; init; } = [];
|
public IReadOnlyList<IRuleStep<TProperty>> Steps { get; init; } = [];
|
||||||
|
|
||||||
private static string GetPropertyPath(Expression expression)
|
private static PropertyPath GetPropertyPath(Expression expression)
|
||||||
{
|
{
|
||||||
var members = new Stack<string>();
|
var members = new Stack<string>();
|
||||||
|
|
||||||
|
|
@ -34,18 +34,15 @@ internal abstract record Rule<T, TProperty> : Rule
|
||||||
{
|
{
|
||||||
switch (current)
|
switch (current)
|
||||||
{
|
{
|
||||||
case LambdaExpression lambdaExpression:
|
case LambdaExpression expr:
|
||||||
current = lambdaExpression.Body;
|
current = expr.Body;
|
||||||
break;
|
break;
|
||||||
case MemberExpression memberExpression:
|
case MemberExpression expr:
|
||||||
members.Push(memberExpression.Member.Name);
|
members.Push(expr.Member.Name);
|
||||||
current = memberExpression.Expression;
|
current = expr.Expression;
|
||||||
break;
|
break;
|
||||||
case UnaryExpression
|
case UnaryExpression { NodeType: ExpressionType.Convert or ExpressionType.ConvertChecked } expr:
|
||||||
{
|
current = expr.Operand;
|
||||||
NodeType: ExpressionType.Convert or ExpressionType.ConvertChecked
|
|
||||||
} unaryExpression:
|
|
||||||
current = unaryExpression.Operand;
|
|
||||||
break;
|
break;
|
||||||
case ParameterExpression:
|
case ParameterExpression:
|
||||||
current = null;
|
current = null;
|
||||||
|
|
@ -55,7 +52,7 @@ internal abstract record Rule<T, TProperty> : Rule
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return string.Join(".", members);
|
return string.Join('.', members);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override IEnumerable<Problem> Validate(ValidationContext context)
|
public override IEnumerable<Problem> Validate(ValidationContext context)
|
||||||
|
|
@ -95,7 +92,7 @@ internal sealed record PropertyRule<T, TProperty> : Rule<T, TProperty>
|
||||||
}
|
}
|
||||||
|
|
||||||
var value = _accessor(instance);
|
var value = _accessor(instance);
|
||||||
return Steps.SelectMany(step => step.Validate(value, context, PropertyName, Code, Severity));
|
return Steps.SelectMany(step => step.Validate(value, context, PropertyPath, Code, Severity));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -123,11 +120,11 @@ internal sealed record CollectionRule<T, TElement> : Rule<T, TElement>
|
||||||
var index = 0;
|
var index = 0;
|
||||||
foreach (var element in collection)
|
foreach (var element in collection)
|
||||||
{
|
{
|
||||||
var propertyName = string.IsNullOrEmpty(PropertyName) ? $"[{index}]" : $"{PropertyName}[{index}]";
|
var propertyPath = PropertyPath + index;
|
||||||
|
|
||||||
foreach (var step in Steps)
|
foreach (var step in Steps)
|
||||||
{
|
{
|
||||||
foreach (var problem in step.Validate(element, context, propertyName, Code, Severity))
|
foreach (var problem in step.Validate(element, context, propertyPath, Code, Severity))
|
||||||
{
|
{
|
||||||
yield return problem;
|
yield return problem;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ namespace Geekeey.Request.Validation;
|
||||||
|
|
||||||
internal interface IRuleStep<in TValue>
|
internal interface IRuleStep<in TValue>
|
||||||
{
|
{
|
||||||
IEnumerable<Problem> Validate(TValue value, ValidationContext context, string propertyPath, string? code, Severity severity);
|
IEnumerable<Problem> Validate(TValue value, ValidationContext context, PropertyPath propertyPath, string? code, Severity severity);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal sealed record PredicateRuleStep<TValue> : IRuleStep<TValue>
|
internal sealed record PredicateRuleStep<TValue> : IRuleStep<TValue>
|
||||||
|
|
@ -19,7 +19,7 @@ internal sealed record PredicateRuleStep<TValue> : IRuleStep<TValue>
|
||||||
_message = message;
|
_message = message;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<Problem> Validate(TValue value, ValidationContext context, string propertyPath, string? code, Severity severity)
|
public IEnumerable<Problem> Validate(TValue value, ValidationContext context, PropertyPath propertyPath, string? code, Severity severity)
|
||||||
{
|
{
|
||||||
if (_predicate(value))
|
if (_predicate(value))
|
||||||
{
|
{
|
||||||
|
|
@ -28,7 +28,7 @@ internal sealed record PredicateRuleStep<TValue> : IRuleStep<TValue>
|
||||||
|
|
||||||
yield return new Problem
|
yield return new Problem
|
||||||
{
|
{
|
||||||
PropertyName = propertyPath,
|
PropertyPath = propertyPath,
|
||||||
Message = _message,
|
Message = _message,
|
||||||
Code = code,
|
Code = code,
|
||||||
Severity = severity,
|
Severity = severity,
|
||||||
|
|
@ -46,7 +46,7 @@ internal sealed record ValidatorRuleStep<TValue> : IRuleStep<TValue>
|
||||||
_resolver = resolver;
|
_resolver = resolver;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<Problem> Validate(TValue value, ValidationContext context, string propertyPath, string? code, Severity severity)
|
public IEnumerable<Problem> Validate(TValue value, ValidationContext context, PropertyPath propertyPath, string? code, Severity severity)
|
||||||
{
|
{
|
||||||
if (value is null)
|
if (value is null)
|
||||||
{
|
{
|
||||||
|
|
@ -58,22 +58,7 @@ internal sealed record ValidatorRuleStep<TValue> : IRuleStep<TValue>
|
||||||
|
|
||||||
foreach (var problem in validation.Problems)
|
foreach (var problem in validation.Problems)
|
||||||
{
|
{
|
||||||
yield return problem with { PropertyName = AppendPropertyPath(propertyPath, problem.PropertyName) };
|
yield return problem with { PropertyPath = propertyPath + problem.PropertyPath };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string AppendPropertyPath(string prefix, string suffix)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(prefix))
|
|
||||||
{
|
|
||||||
return suffix;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(suffix))
|
|
||||||
{
|
|
||||||
return prefix;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $"{prefix}.{suffix}";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -77,7 +77,7 @@ var validation = validator.Validate(new CreateUserRequest(
|
||||||
|
|
||||||
foreach (var problem in validation.Problems)
|
foreach (var problem in validation.Problems)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"{problem.PropertyName}: {problem.Message}");
|
Console.WriteLine($"{problem.PropertyPath}: {problem.Message}");
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue