Skip to content

Commit

Permalink
Fix: Recursive validation replaced by attribute. Fixes stackoverflow …
Browse files Browse the repository at this point in the history
…issue
  • Loading branch information
iambtshft committed Jun 9, 2020
1 parent 2d74c3c commit f22b49e
Show file tree
Hide file tree
Showing 12 changed files with 82 additions and 80 deletions.
7 changes: 4 additions & 3 deletions src/Zeus/Features/Alerting/AlertingFeatureOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,19 @@
using Zeus.Features.Alerting.Channels;
using Zeus.Features.Alerting.Subscriptions;
using Zeus.Features.Alerting.Templates;
using Zeus.Shared.Validation;

namespace Zeus.Features.Alerting
{
public class AlertingFeatureOptions
{
[Required]
[Required, ValidateObject]
public TemplatesOptions Templates { get; set; }

[Required]
[Required, ValidateObject]
public ChannelsOptions Channels { get; set; }

[Required]
[Required, ValidateObject]
public SubscriptionsOptions Subscriptions { get; set; }
}
}
3 changes: 2 additions & 1 deletion src/Zeus/Features/Alerting/Channels/ChannelsOptions.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
using System.ComponentModel.DataAnnotations;
using Zeus.Shared.Validation;

namespace Zeus.Features.Alerting.Channels
{
public class ChannelsOptions
{
[Required]
[Required, ValidateObject]
public ChannelsStoreOptions Store { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
using System.ComponentModel.DataAnnotations;
using Zeus.Shared.Validation;

namespace Zeus.Features.Alerting.Subscriptions
{
public class SubscriptionsOptions
{
[Required]
[Required, ValidateObject]
public SubscriptionsStoreOptions Store { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
namespace Zeus.Features.Alerting.Subscriptions
using Zeus.Shared.Validation;

namespace Zeus.Features.Alerting.Subscriptions
{
public class SubscriptionsStoreOptions
{
public bool UseInMemoryStore { get; set; }

[ValidateObject]
public SubscriptionsConsulStoreOptions Consul { get; set; }
}
}
3 changes: 2 additions & 1 deletion src/Zeus/Features/Alerting/Templates/TemplatesOptions.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
using System.ComponentModel.DataAnnotations;
using Zeus.Shared.Validation;

namespace Zeus.Features.Alerting.Templates
{
public class TemplatesOptions
{
[Required]
[Required, ValidateObject]
public TemplatesStoreOptions Store { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
using System.ComponentModel.DataAnnotations;
using Zeus.Shared.Validation;

namespace Zeus.Features.Alerting.Templates
{
public class TemplatesStoreOptions
{
[Required]
[Required, ValidateObject]
public TemplatesFileSystemStoreOptions FileSystem { get; set; }
}
}
3 changes: 2 additions & 1 deletion src/Zeus/Features/Clients/ClientsFeatureOptions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
using Zeus.Shared.Validation;

namespace Zeus.Features.Clients
{
Expand All @@ -7,7 +8,7 @@ public class ClientsFeatureOptions
/// <summary>
/// Callback client options.
/// </summary>
[Required]
[Required, ValidateObject]
public ClientOptions Callback { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
using System;
using System.Threading;
using System.Threading;
using System.Threading.Tasks;
using MediatR;
using Microsoft.AspNetCore.Http;
using Telegram.Bot;
using Telegram.Bot.Types;
using Zeus.Handlers.Bot.Abstractions;

namespace Zeus.Handlers.Bot.Context
Expand Down
1 change: 0 additions & 1 deletion src/Zeus/Handlers/Bot/Updates/BotUpdateRequestHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
using Microsoft.Extensions.Localization;
using Telegram.Bot;
using Telegram.Bot.Types;
using Telegram.Bot.Types.Enums;
using Zeus.Handlers.Bot.Abstractions;
using Zeus.Handlers.Bot.Actions;
using Zeus.Handlers.Bot.Actions.Echo;
Expand Down
20 changes: 20 additions & 0 deletions src/Zeus/Shared/Validation/CompositeValidationResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace Zeus.Shared.Validation
{
internal class CompositeValidationResult : ValidationResult
{
public CompositeValidationResult(string errorMessage)
: base(errorMessage) { }

public CompositeValidationResult(string errorMessage, IEnumerable<string> memberNames)
: base(errorMessage, memberNames) { }

protected CompositeValidationResult(ValidationResult validationResult)
: base(validationResult) { }

public List<ValidationResult> InnerResults { get; }
= new List<ValidationResult>();
}
}
81 changes: 15 additions & 66 deletions src/Zeus/Shared/Validation/DataAnnotationsValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Runtime.CompilerServices;
using Zeus.Shared.Exceptions;

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

var results = new List<ValidationResult>();
if (TryValidateRecursive(instance, ref results))
var context = new ValidationContext(instance);
var validationResults = new List<ValidationResult>();

if (Validator.TryValidateObject(instance, context, validationResults, validateAllProperties: true))
return;

var errors = results.Where(s => !string.IsNullOrEmpty(s.ErrorMessage))
var flatResults = new List<ValidationResult>();
Flatten(validationResults, ref flatResults);

var errors = flatResults.Where(s => !string.IsNullOrEmpty(s.ErrorMessage))
.Select(s => s.ErrorMessage);

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

private static bool TryValidateRecursive(object instance, ref List<ValidationResult> results)
private static void Flatten(IEnumerable<ValidationResult> results, ref List<ValidationResult> output)
{
if (instance == null)
throw new ArgumentNullException(nameof(instance));

if (results == null)
results = new List<ValidationResult>();

var validationContext = new ValidationContext(instance);
var isValid = Validator.TryValidateObject(instance, validationContext, results, validateAllProperties: true);
if (output == null)
output = new List<ValidationResult>();

var properties = instance.GetType()
.GetProperties()
.Where(prop => prop.CanRead && prop.GetIndexParameters().Length == 0)
.Where(prop => CanValidate(prop.PropertyType))
.ToArray();

foreach (var property in properties)
foreach (var result in results)
{
var value = property.GetValue(instance);
if (value == null)
continue;

var enumerable = value as IEnumerable<object> ?? new[] { value };
output.Add(result);

foreach (var toValidate in enumerable)
if (result is CompositeValidationResult composite)
{
var nestedResults = new List<ValidationResult>();
if (TryValidateRecursive(toValidate, ref nestedResults))
{
continue;
}

isValid = false;

results.AddRange(nestedResults
.Select(result => new ValidationResult(
result.ErrorMessage, result.MemberNames
.Select(x => property.Name + '.' + x))));
Flatten(composite.InnerResults, ref output);
}
}


return isValid;
}

/// <summary>
/// Returns whether the given <paramref name="type"/> can be validated
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool CanValidate(Type type)
{
while (true)
{
if (type == null)
return false;

if (type == typeof(string))
return false;

if (type.IsValueType)
return false;

if (!type.IsArray || !type.HasElementType)
return true;

var elementType = type.GetElementType();
type = elementType;
}
}
}
}
28 changes: 28 additions & 0 deletions src/Zeus/Shared/Validation/ValidateObjectAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace Zeus.Shared.Validation
{
public class ValidateObjectAttribute : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if (value == null)
return ValidationResult.Success;

var results = new List<ValidationResult>();
var context = new ValidationContext(value, null, null);

var isValid = Validator.TryValidateObject(value, context, results, true);
if (isValid)
return ValidationResult.Success;

var compositeResults =
new CompositeValidationResult($"Validation for {validationContext.DisplayName} failed");

compositeResults.InnerResults.AddRange(results);

return compositeResults;
}
}
}

0 comments on commit f22b49e

Please sign in to comment.