Skip to content

Commit f22b49e

Browse files
committed
Fix: Recursive validation replaced by attribute. Fixes stackoverflow issue
1 parent 2d74c3c commit f22b49e

12 files changed

+82
-80
lines changed

src/Zeus/Features/Alerting/AlertingFeatureOptions.cs

+4-3
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,19 @@
22
using Zeus.Features.Alerting.Channels;
33
using Zeus.Features.Alerting.Subscriptions;
44
using Zeus.Features.Alerting.Templates;
5+
using Zeus.Shared.Validation;
56

67
namespace Zeus.Features.Alerting
78
{
89
public class AlertingFeatureOptions
910
{
10-
[Required]
11+
[Required, ValidateObject]
1112
public TemplatesOptions Templates { get; set; }
1213

13-
[Required]
14+
[Required, ValidateObject]
1415
public ChannelsOptions Channels { get; set; }
1516

16-
[Required]
17+
[Required, ValidateObject]
1718
public SubscriptionsOptions Subscriptions { get; set; }
1819
}
1920
}
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
using System.ComponentModel.DataAnnotations;
2+
using Zeus.Shared.Validation;
23

34
namespace Zeus.Features.Alerting.Channels
45
{
56
public class ChannelsOptions
67
{
7-
[Required]
8+
[Required, ValidateObject]
89
public ChannelsStoreOptions Store { get; set; }
910
}
1011
}
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
using System.ComponentModel.DataAnnotations;
2+
using Zeus.Shared.Validation;
23

34
namespace Zeus.Features.Alerting.Subscriptions
45
{
56
public class SubscriptionsOptions
67
{
7-
[Required]
8+
[Required, ValidateObject]
89
public SubscriptionsStoreOptions Store { get; set; }
910
}
1011
}
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1-
namespace Zeus.Features.Alerting.Subscriptions
1+
using Zeus.Shared.Validation;
2+
3+
namespace Zeus.Features.Alerting.Subscriptions
24
{
35
public class SubscriptionsStoreOptions
46
{
57
public bool UseInMemoryStore { get; set; }
68

9+
[ValidateObject]
710
public SubscriptionsConsulStoreOptions Consul { get; set; }
811
}
912
}
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
using System.ComponentModel.DataAnnotations;
2+
using Zeus.Shared.Validation;
23

34
namespace Zeus.Features.Alerting.Templates
45
{
56
public class TemplatesOptions
67
{
7-
[Required]
8+
[Required, ValidateObject]
89
public TemplatesStoreOptions Store { get; set; }
910
}
1011
}
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
using System.ComponentModel.DataAnnotations;
2+
using Zeus.Shared.Validation;
23

34
namespace Zeus.Features.Alerting.Templates
45
{
56
public class TemplatesStoreOptions
67
{
7-
[Required]
8+
[Required, ValidateObject]
89
public TemplatesFileSystemStoreOptions FileSystem { get; set; }
910
}
1011
}
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.ComponentModel.DataAnnotations;
2+
using Zeus.Shared.Validation;
23

34
namespace Zeus.Features.Clients
45
{
@@ -7,7 +8,7 @@ public class ClientsFeatureOptions
78
/// <summary>
89
/// Callback client options.
910
/// </summary>
10-
[Required]
11+
[Required, ValidateObject]
1112
public ClientOptions Callback { get; set; }
1213
}
1314
}

src/Zeus/Handlers/Bot/Context/BotActionContextInitBehavior.cs

+1-4
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
1-
using System;
2-
using System.Threading;
1+
using System.Threading;
32
using System.Threading.Tasks;
43
using MediatR;
54
using Microsoft.AspNetCore.Http;
6-
using Telegram.Bot;
7-
using Telegram.Bot.Types;
85
using Zeus.Handlers.Bot.Abstractions;
96

107
namespace Zeus.Handlers.Bot.Context

src/Zeus/Handlers/Bot/Updates/BotUpdateRequestHandler.cs

-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
using Microsoft.Extensions.Localization;
55
using Telegram.Bot;
66
using Telegram.Bot.Types;
7-
using Telegram.Bot.Types.Enums;
87
using Zeus.Handlers.Bot.Abstractions;
98
using Zeus.Handlers.Bot.Actions;
109
using Zeus.Handlers.Bot.Actions.Echo;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
using System.Collections.Generic;
2+
using System.ComponentModel.DataAnnotations;
3+
4+
namespace Zeus.Shared.Validation
5+
{
6+
internal class CompositeValidationResult : ValidationResult
7+
{
8+
public CompositeValidationResult(string errorMessage)
9+
: base(errorMessage) { }
10+
11+
public CompositeValidationResult(string errorMessage, IEnumerable<string> memberNames)
12+
: base(errorMessage, memberNames) { }
13+
14+
protected CompositeValidationResult(ValidationResult validationResult)
15+
: base(validationResult) { }
16+
17+
public List<ValidationResult> InnerResults { get; }
18+
= new List<ValidationResult>();
19+
}
20+
}

src/Zeus/Shared/Validation/DataAnnotationsValidator.cs

+15-66
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
using System.Collections.Generic;
33
using System.ComponentModel.DataAnnotations;
44
using System.Linq;
5-
using System.Runtime.CompilerServices;
65
using Zeus.Shared.Exceptions;
76

87
namespace Zeus.Shared.Validation
@@ -14,86 +13,36 @@ public static void EnsureValid(object instance)
1413
if (instance == null)
1514
throw new ArgumentNullException(nameof(instance));
1615

17-
var results = new List<ValidationResult>();
18-
if (TryValidateRecursive(instance, ref results))
16+
var context = new ValidationContext(instance);
17+
var validationResults = new List<ValidationResult>();
18+
19+
if (Validator.TryValidateObject(instance, context, validationResults, validateAllProperties: true))
1920
return;
2021

21-
var errors = results.Where(s => !string.IsNullOrEmpty(s.ErrorMessage))
22+
var flatResults = new List<ValidationResult>();
23+
Flatten(validationResults, ref flatResults);
24+
25+
var errors = flatResults.Where(s => !string.IsNullOrEmpty(s.ErrorMessage))
2226
.Select(s => s.ErrorMessage);
2327

2428
var errorMessage = string.Join(';', errors);
2529
throw new ConfigurationException($"Errors occured while validating object '{instance.GetType().Name}': {errorMessage}");
2630
}
2731

28-
private static bool TryValidateRecursive(object instance, ref List<ValidationResult> results)
32+
private static void Flatten(IEnumerable<ValidationResult> results, ref List<ValidationResult> output)
2933
{
30-
if (instance == null)
31-
throw new ArgumentNullException(nameof(instance));
32-
33-
if (results == null)
34-
results = new List<ValidationResult>();
35-
36-
var validationContext = new ValidationContext(instance);
37-
var isValid = Validator.TryValidateObject(instance, validationContext, results, validateAllProperties: true);
34+
if (output == null)
35+
output = new List<ValidationResult>();
3836

39-
var properties = instance.GetType()
40-
.GetProperties()
41-
.Where(prop => prop.CanRead && prop.GetIndexParameters().Length == 0)
42-
.Where(prop => CanValidate(prop.PropertyType))
43-
.ToArray();
44-
45-
foreach (var property in properties)
37+
foreach (var result in results)
4638
{
47-
var value = property.GetValue(instance);
48-
if (value == null)
49-
continue;
50-
51-
var enumerable = value as IEnumerable<object> ?? new[] { value };
39+
output.Add(result);
5240

53-
foreach (var toValidate in enumerable)
41+
if (result is CompositeValidationResult composite)
5442
{
55-
var nestedResults = new List<ValidationResult>();
56-
if (TryValidateRecursive(toValidate, ref nestedResults))
57-
{
58-
continue;
59-
}
60-
61-
isValid = false;
62-
63-
results.AddRange(nestedResults
64-
.Select(result => new ValidationResult(
65-
result.ErrorMessage, result.MemberNames
66-
.Select(x => property.Name + '.' + x))));
43+
Flatten(composite.InnerResults, ref output);
6744
}
6845
}
69-
70-
71-
return isValid;
72-
}
73-
74-
/// <summary>
75-
/// Returns whether the given <paramref name="type"/> can be validated
76-
/// </summary>
77-
[MethodImpl(MethodImplOptions.AggressiveInlining)]
78-
private static bool CanValidate(Type type)
79-
{
80-
while (true)
81-
{
82-
if (type == null)
83-
return false;
84-
85-
if (type == typeof(string))
86-
return false;
87-
88-
if (type.IsValueType)
89-
return false;
90-
91-
if (!type.IsArray || !type.HasElementType)
92-
return true;
93-
94-
var elementType = type.GetElementType();
95-
type = elementType;
96-
}
9746
}
9847
}
9948
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
using System.Collections.Generic;
2+
using System.ComponentModel.DataAnnotations;
3+
4+
namespace Zeus.Shared.Validation
5+
{
6+
public class ValidateObjectAttribute : ValidationAttribute
7+
{
8+
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
9+
{
10+
if (value == null)
11+
return ValidationResult.Success;
12+
13+
var results = new List<ValidationResult>();
14+
var context = new ValidationContext(value, null, null);
15+
16+
var isValid = Validator.TryValidateObject(value, context, results, true);
17+
if (isValid)
18+
return ValidationResult.Success;
19+
20+
var compositeResults =
21+
new CompositeValidationResult($"Validation for {validationContext.DisplayName} failed");
22+
23+
compositeResults.InnerResults.AddRange(results);
24+
25+
return compositeResults;
26+
}
27+
}
28+
}

0 commit comments

Comments
 (0)