Skip to content

Make subclassing DefaultBodyModelValidator more useful #97

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/System.Web.Http/System.Web.Http.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,9 @@
<Compile Include="Tracing\Tracers\FormatterLoggerTraceWrapper.cs" />
<Compile Include="Tracing\Tracers\DefaultHttpControllerTypeResolverTracer.cs" />
<Compile Include="Tracing\Tracers\OverrideFilterTracer.cs" />
<Compile Include="Validation\BodyModelValidatorContext.cs" />
<Compile Include="Validation\IModelValidatorCache.cs" />
<Compile Include="Validation\IBodyModelValidatorKeyBuilder.cs" />
<Compile Include="Validation\ModelValidatorCache.cs" />
<Compile Include="Controllers\ResponseMessageResultConverter.cs" />
<Compile Include="Controllers\ValueResultConverter.cs" />
Expand Down
53 changes: 53 additions & 0 deletions src/System.Web.Http/Validation/BodyModelValidatorContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.Collections.Generic;
using System.Web.Http.Controllers;
using System.Web.Http.Metadata;
using System.Web.Http.ModelBinding;

namespace System.Web.Http.Validation
{
/// <summary>
/// Context passed between <see cref="DefaultBodyModelValidator"/> methods.
/// </summary>
public class BodyModelValidatorContext
{
/// <summary>
/// Gets or sets the <see cref="ModelMetadataProvider"/> used to provide the model metadata.
/// </summary>
public ModelMetadataProvider MetadataProvider { get; set; }

/// <summary>
/// Gets or sets the <see cref="HttpActionContext"/> within which the model is being validated.
/// </summary>
public HttpActionContext ActionContext { get; set; }

/// <summary>
/// Gets or sets the <see cref="IModelValidatorCache"/>.
/// </summary>
public IModelValidatorCache ValidatorCache { get; set; }

/// <summary>
/// Gets or sets the current <see cref="ModelStateDictionary"/>.
/// </summary>
public ModelStateDictionary ModelState { get; set; }

/// <summary>
/// Gets or sets the set of model objects visited in this validation. Includes the model being validated in the
/// current scope.
/// </summary>
public HashSet<object> Visited { get; set; }

/// <summary>
/// Gets or sets the stack of <see cref="IBodyModelValidatorKeyBuilder"/>s used in this validation. Includes
/// the <see cref="IBodyModelValidatorKeyBuilder"/> to generate model state keys for the current scope.
/// </summary>
public Stack<IBodyModelValidatorKeyBuilder> KeyBuilders { get; set; }

/// <summary>
/// Gets or sets the model state prefix for the root scope of this validation.
/// </summary>
public string RootPrefix { get; set; }
}
}
128 changes: 98 additions & 30 deletions src/System.Web.Http/Validation/DefaultBodyModelValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,10 @@
namespace System.Web.Http.Validation
{
/// <summary>
/// Recursively validate an object.
/// Recursively validate an object.
/// </summary>
public class DefaultBodyModelValidator : IBodyModelValidator
{
private interface IKeyBuilder
{
string AppendTo(string prefix);
}

/// <summary>
/// Determines whether the <paramref name="model"/> is valid and adds any validation errors to the <paramref name="actionContext"/>'s <see cref="ModelStateDictionary"/>
/// </summary>
Expand Down Expand Up @@ -64,14 +59,14 @@ public bool Validate(object model, Type type, ModelMetadataProvider metadataProv
}

ModelMetadata metadata = metadataProvider.GetMetadataForType(() => model, type);
ValidationContext validationContext = new ValidationContext()
BodyModelValidatorContext validationContext = new BodyModelValidatorContext
{
MetadataProvider = metadataProvider,
ActionContext = actionContext,
ValidatorCache = actionContext.GetValidatorCache(),
ModelState = actionContext.ModelState,
Visited = new HashSet<object>(ReferenceEqualityComparer.Instance),
KeyBuilders = new Stack<IKeyBuilder>(),
KeyBuilders = new Stack<IBodyModelValidatorKeyBuilder>(),
RootPrefix = keyPrefix
};
return ValidateNodeAndChildren(metadata, validationContext, container: null, validators: null);
Expand All @@ -87,12 +82,36 @@ public virtual bool ShouldValidateType(Type type)
return !MediaTypeFormatterCollection.IsTypeExcludedFromValidation(type);
}

/// <summary>
/// Recursively validate the given <paramref name="metadata"/> and <paramref name="container"/>.
/// </summary>
/// <param name="metadata">The <see cref="ModelMetadata"/> for the object to validate.</param>
/// <param name="validationContext">The <see cref="BodyModelValidatorContext"/>.</param>
/// <param name="container">The object containing the object to validate.</param>
/// <param name="validators">The collection of <see cref="ModelValidator"/>s.</param>
/// <returns>
/// <see langword="true"/> if validation succeeds for the given <paramref name="metadata"/>,
/// <paramref name="container"/>, and child nodes; <see langword="false"/> otherwise.
/// </returns>
[SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "See comment below")]
private bool ValidateNodeAndChildren(ModelMetadata metadata, ValidationContext validationContext, object container, IEnumerable<ModelValidator> validators)
protected virtual bool ValidateNodeAndChildren(
ModelMetadata metadata,
BodyModelValidatorContext validationContext,
object container,
IEnumerable<ModelValidator> validators)
{
// Recursion guard to avoid stack overflows
RuntimeHelpers.EnsureSufficientExecutionStack();

if (metadata == null)
{
throw Error.ArgumentNull("metadata");
}
if (validationContext == null)
{
throw Error.ArgumentNull("validationContext");
}

object model = null;
try
{
Expand Down Expand Up @@ -155,8 +174,26 @@ private bool ValidateNodeAndChildren(ModelMetadata metadata, ValidationContext v
return isValid;
}

private bool ValidateProperties(ModelMetadata metadata, ValidationContext validationContext)
/// <summary>
/// Recursively validate the properties of the given <paramref name="metadata"/>.
/// </summary>
/// <param name="metadata">The <see cref="ModelMetadata"/> for the object to validate.</param>
/// <param name="validationContext">The <see cref="BodyModelValidatorContext"/>.</param>
/// <returns>
/// <see langword="true"/> if validation succeeds for all properties in <paramref name="metadata"/>;
/// <see langword="false"/> otherwise.
/// </returns>
protected virtual bool ValidateProperties(ModelMetadata metadata, BodyModelValidatorContext validationContext)
{
if (metadata == null)
{
throw Error.ArgumentNull("metadata");
}
if (validationContext == null)
{
throw Error.ArgumentNull("validationContext");
}

bool isValid = true;
PropertyScope propertyScope = new PropertyScope();
validationContext.KeyBuilders.Push(propertyScope);
Expand All @@ -172,8 +209,26 @@ private bool ValidateProperties(ModelMetadata metadata, ValidationContext valida
return isValid;
}

private bool ValidateElements(IEnumerable model, ValidationContext validationContext)
/// <summary>
/// Recursively validate the elements of the <paramref name="model"/> collection.
/// </summary>
/// <param name="model">The <see cref="IEnumerable"/> instance containing the elements to validate.</param>
/// <param name="validationContext">The <see cref="BodyModelValidatorContext"/>.</param>
/// <returns>
/// <see langword="true"/> if validation succeeds for all elements of <paramref name="model"/>;
/// <see langword="false"/> otherwise.
/// </returns>
protected virtual bool ValidateElements(IEnumerable model, BodyModelValidatorContext validationContext)
{
if (model == null)
{
throw Error.ArgumentNull("model");
}
if (validationContext == null)
{
throw Error.ArgumentNull("validationContext");
}

bool isValid = true;
Type elementType = GetElementType(model.GetType());
ModelMetadata elementMetadata = validationContext.MetadataProvider.GetMetadataForType(null, elementType);
Expand Down Expand Up @@ -207,15 +262,39 @@ private bool ValidateElements(IEnumerable model, ValidationContext validationCon
return isValid;
}

// Validates a single node (not including children)
// Returns true if validation passes successfully
private static bool ShallowValidate(ModelMetadata metadata, ValidationContext validationContext, object container, IEnumerable<ModelValidator> validators)
/// <summary>
/// Validate a single node, not including its children.
/// </summary>
/// <param name="metadata">The <see cref="ModelMetadata"/>.</param>
/// <param name="validationContext">The <see cref="BodyModelValidatorContext"/>.</param>
/// <param name="container">The object to validate.</param>
/// <param name="validators">The collection of <see cref="ModelValidator"/>s.</param>
/// <returns>
/// <see langword="true"/> if validation succeeds for the given <paramref name="metadata"/> and
/// <paramref name="container"/>; <see langword="false"/> otherwise.
/// </returns>
protected virtual bool ShallowValidate(
ModelMetadata metadata,
BodyModelValidatorContext validationContext,
object container,
IEnumerable<ModelValidator> validators)
{
if (metadata == null)
{
throw Error.ArgumentNull("metadata");
}
if (validationContext == null)
{
throw Error.ArgumentNull("validationContext");
}
if (validators == null)
{
throw Error.ArgumentNull("validators");
}

bool isValid = true;
string modelKey = null;

Contract.Assert(validators != null);

// When the are no validators we bail quickly. This saves a GetEnumerator allocation.
// In a large array (tens of thousands or more) scenario it's very significant.
ICollection validatorsAsCollection = validators as ICollection;
Expand All @@ -231,7 +310,7 @@ private static bool ShallowValidate(ModelMetadata metadata, ValidationContext va
if (modelKey == null)
{
modelKey = validationContext.RootPrefix;
foreach (IKeyBuilder keyBuilder in validationContext.KeyBuilders.Reverse())
foreach (IBodyModelValidatorKeyBuilder keyBuilder in validationContext.KeyBuilders.Reverse())
{
modelKey = keyBuilder.AppendTo(modelKey);
}
Expand Down Expand Up @@ -263,7 +342,7 @@ private static Type GetElementType(Type type)
return typeof(object);
}

private class PropertyScope : IKeyBuilder
private class PropertyScope : IBodyModelValidatorKeyBuilder
{
public string PropertyName { get; set; }

Expand All @@ -273,7 +352,7 @@ public string AppendTo(string prefix)
}
}

private class ElementScope : IKeyBuilder
private class ElementScope : IBodyModelValidatorKeyBuilder
{
public int Index { get; set; }

Expand All @@ -282,16 +361,5 @@ public string AppendTo(string prefix)
return ModelBindingHelper.CreateIndexModelName(prefix, Index);
}
}

private class ValidationContext
{
public ModelMetadataProvider MetadataProvider { get; set; }
public HttpActionContext ActionContext { get; set; }
public IModelValidatorCache ValidatorCache { get; set; }
public ModelStateDictionary ModelState { get; set; }
public HashSet<object> Visited { get; set; }
public Stack<IKeyBuilder> KeyBuilders { get; set; }
public string RootPrefix { get; set; }
}
}
}
19 changes: 19 additions & 0 deletions src/System.Web.Http/Validation/IBodyModelValidatorKeyBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

namespace System.Web.Http.Validation
{
/// <summary>
/// Abstraction for creating keys used in nested validation scopes. Intended for use in
/// <see cref="IBodyModelValidator"/> implementations, especially <see cref="DefaultBodyModelValidator"/>.
/// </summary>
public interface IBodyModelValidatorKeyBuilder
{
/// <summary>
/// Returns the key for a nested scope within the <paramref name="prefix"/> scope.
/// </summary>
/// <param name="prefix">Key for the current scope.</param>
/// <returns>Key for a nested scope. Usually appends a property name to <paramref name="prefix"/>.</returns>
string AppendTo(string prefix);
}
}
11 changes: 8 additions & 3 deletions src/System.Web.Http/Validation/IModelValidatorCache.cs
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.Collections.Generic;
using System.Web.Http.Metadata;

namespace System.Web.Http.Validation
{
/// <summary>
/// Defines a cache for <see cref="ModelValidator"/>s. This cache is keyed on the type or property that the metadata is associated with.
/// Defines a cache for <see cref="ModelValidator"/>s. This cache is keyed on the type or property that the
/// metadata is associated with.
/// </summary>
internal interface IModelValidatorCache
public interface IModelValidatorCache
{
/// <summary>
/// Returns the <see cref="ModelValidator"/>s for the given <paramref name="metadata"/>.
/// </summary>
/// <param name="metadata">The <see cref="ModelMetadata"/>.</param>
/// <returns>An array of <see cref="ModelValidator"/>s for the given <paramref name="metadata"/>.</returns>
ModelValidator[] GetValidators(ModelMetadata metadata);
}
}