Skip to content
This repository has been archived by the owner on Dec 14, 2018. It is now read-only.

Commit

Permalink
Shortcircuit validation when using default validator providers and no…
Browse files Browse the repository at this point in the history
… validation metadata is discovered

Fixes #5887
  • Loading branch information
pranavkm committed Oct 12, 2018
1 parent a40c1f2 commit fb57810
Show file tree
Hide file tree
Showing 35 changed files with 1,992 additions and 39 deletions.
7 changes: 7 additions & 0 deletions benchmarkapps/BasicApi/Controllers/PetController.cs
Expand Up @@ -132,6 +132,13 @@ public async Task<IActionResult> AddPet([FromBody] Pet pet)
return new CreatedAtRouteResult("FindPetById", new { id = pet.Id }, pet);
}

[Authorize("pet-store-writer")]
[HttpPost("add-pet")]
public ActionResult<Pet> AddPetWithoutDb(Pet pet)
{
return pet;
}

[Authorize("pet-store-writer")]
[HttpPut]
public IActionResult EditPet(Pet pet)
Expand Down
6 changes: 6 additions & 0 deletions benchmarkapps/BasicApi/benchmarks.json
Expand Up @@ -44,5 +44,11 @@
"Scripts": "https://raw.githubusercontent.com/aspnet/Mvc/release/2.2/benchmarkapps/BasicApi/postJsonWithToken.lua"
},
"Path": "/pet"
},
"BasicApi.PostWithoutDb": {
"Path": "/pet/add-pet",
"ClientProperties": {
"Scripts": "https://raw.githubusercontent.com/aspnet/Mvc/release/2.2/benchmarkapps/BasicApi/postJsonWithToken.lua"
}
}
}
@@ -0,0 +1,75 @@
// 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 BenchmarkDotNet.Attributes;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.DataAnnotations;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Options;

namespace Microsoft.AspNetCore.Mvc.Performance
{
public abstract class ValidationVisitorBenchmarkBase
{
protected const int Iterations = 4;

protected static readonly IModelValidatorProvider[] ValidatorProviders = new IModelValidatorProvider[]
{
new DefaultModelValidatorProvider(),
new DataAnnotationsModelValidatorProvider(
new ValidationAttributeAdapterProvider(),
Options.Create(new MvcDataAnnotationsLocalizationOptions()),
null),
};

protected static readonly CompositeModelValidatorProvider CompositeModelValidatorProvider = new CompositeModelValidatorProvider(ValidatorProviders);

public abstract object Model { get; }

public ModelMetadataProvider BaselineModelMetadataProvider { get; private set; }
public ModelMetadataProvider ModelMetadataProvider { get; private set; }
public ModelMetadata BaselineModelMetadata { get; private set; }
public ModelMetadata ModelMetadata { get; private set; }
public ActionContext ActionContext { get; private set; }
public ValidatorCache ValidatorCache { get; private set; }

[GlobalSetup]
public void Setup()
{
BaselineModelMetadataProvider = CreateModelMetadataProvider(addHasValidatorsProvider: false);
ModelMetadataProvider = CreateModelMetadataProvider(addHasValidatorsProvider: true);

BaselineModelMetadata = BaselineModelMetadataProvider.GetMetadataForType(Model.GetType());
ModelMetadata = ModelMetadataProvider.GetMetadataForType(Model.GetType());
ActionContext = GetActionContext();
ValidatorCache = new ValidatorCache();
}

protected static ModelMetadataProvider CreateModelMetadataProvider(bool addHasValidatorsProvider)
{
var detailsProviders = new List<IMetadataDetailsProvider>
{
new DefaultValidationMetadataProvider(),
};

if (addHasValidatorsProvider)
{
detailsProviders.Add(new HasValidatorsValidationMetadataProvider(ValidatorProviders));
}

var compositeDetailsProvider = new DefaultCompositeMetadataDetailsProvider(detailsProviders);
return new DefaultModelMetadataProvider(compositeDetailsProvider, Options.Create(new MvcOptions()));
}

protected static ActionContext GetActionContext()
{
return new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor());
}
}
}
@@ -0,0 +1,42 @@
// 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 BenchmarkDotNet.Attributes;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;

namespace Microsoft.AspNetCore.Mvc.Performance
{
public class ValidationVisitorByteArrayBenchmark : ValidationVisitorBenchmarkBase
{
public override object Model { get; } = new byte[30];

[Benchmark(Baseline = true, Description = "validation for byte arrays baseline", OperationsPerInvoke = Iterations)]
public void Baseline()
{
// Baseline for validating a byte array of size 30, without the ModelMetadata.HasValidators optimization.
// This is the behavior as of 2.1.
var validationVisitor = new ValidationVisitor(
ActionContext,
CompositeModelValidatorProvider,
ValidatorCache,
BaselineModelMetadataProvider,
new ValidationStateDictionary());

validationVisitor.Validate(BaselineModelMetadata, "key", Model);
}

[Benchmark(Description = "validation for byte arrays", OperationsPerInvoke = Iterations)]
public void HasValidators()
{
// Validating a byte array of size 30, with the ModelMetadata.HasValidators optimization.
var validationVisitor = new ValidationVisitor(
ActionContext,
CompositeModelValidatorProvider,
ValidatorCache,
ModelMetadataProvider,
new ValidationStateDictionary());

validationVisitor.Validate(ModelMetadata, "key", Model);
}
}
}
@@ -0,0 +1,92 @@
// 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.ComponentModel.DataAnnotations;
using BenchmarkDotNet.Attributes;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;

namespace Microsoft.AspNetCore.Mvc.Performance
{
public class ValidationVisitorModelWithValidatedProperties : ValidationVisitorBenchmarkBase
{
public class Person
{
[Required]
public int Id { get; set; }

[Required]
[StringLength(20)]
public string Name { get; set; }

public string Description { get; set; }

public IList<Address> Address { get; set; }
}

public class Address
{
[Required]
public string Street { get; set; }

public string Street2 { get; set; }

public string Type { get; set; }

[Required]
public string Zip { get; set; }
}

public override object Model { get; } = new Person
{
Id = 10,
Name = "Test",
Address = new List<Address>
{
new Address
{
Street = "1 Microsoft Way",
Type = "Work",
Zip = "98056",
},
new Address
{
Street = "15701 NE 39th St",
Type = "Home",
Zip = "98052",
}
},
};

[Benchmark(Baseline = true, Description = "validation for a model with some validated properties - baseline", OperationsPerInvoke = Iterations)]
public void Visit_TypeWithSomeValidatedProperties_Baseline()
{
// Baseline for validating a typical model with some properties that require validation.
// This executes without the ModelMetadata.HasValidators optimization.

var validationVisitor = new ValidationVisitor(
ActionContext,
CompositeModelValidatorProvider,
ValidatorCache,
BaselineModelMetadataProvider,
new ValidationStateDictionary());

validationVisitor.Validate(BaselineModelMetadata, "key", Model);
}

[Benchmark(Description = "validation for a model with some validated properties", OperationsPerInvoke = Iterations)]
public void Visit_TypeWithSomeValidatedProperties()
{
// Validating a typical model with some properties that require validation.
// This executes with the ModelMetadata.HasValidators optimization.
var validationVisitor = new ValidationVisitor(
ActionContext,
CompositeModelValidatorProvider,
ValidatorCache,
ModelMetadataProvider,
new ValidationStateDictionary());

validationVisitor.Validate(ModelMetadata, "key", Model);
}
}
}
Expand Up @@ -325,6 +325,15 @@ public virtual ModelMetadata ContainerMetadata
/// </summary>
public abstract bool ValidateChildren { get; }

/// <summary>
/// Gets a value that indicates if the model, or one of it's properties, or elements has associatated validators.
/// </summary>
/// <remarks>
/// When <see langword="false"/>, validation can be assume that the model is valid (<see cref="ModelValidationState.Valid"/>) without
/// inspecting the object graph.
/// </remarks>
public virtual bool? HasValidators { get; }

/// <summary>
/// Gets a collection of metadata items for validators.
/// </summary>
Expand Down
Expand Up @@ -146,6 +146,8 @@ internal static void AddMvcCoreServices(IServiceCollection services)
ServiceDescriptor.Transient<IConfigureOptions<MvcOptions>, MvcCoreMvcOptionsSetup>());
services.TryAddEnumerable(
ServiceDescriptor.Transient<IPostConfigureOptions<MvcOptions>, MvcOptionsConfigureCompatibilityOptions>());
services.TryAddEnumerable(
ServiceDescriptor.Transient<IPostConfigureOptions<MvcOptions>, MvcCoreMvcOptionsSetup>());
services.TryAddEnumerable(
ServiceDescriptor.Transient<IConfigureOptions<ApiBehaviorOptions>, ApiBehaviorOptionsSetup>());
services.TryAddEnumerable(
Expand Down
Expand Up @@ -8,24 +8,25 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;

namespace Microsoft.AspNetCore.Mvc.Internal
namespace Microsoft.AspNetCore.Mvc
{
/// <summary>
/// Sets up default options for <see cref="MvcOptions"/>.
/// </summary>
public class MvcCoreMvcOptionsSetup : IConfigureOptions<MvcOptions>
internal class MvcCoreMvcOptionsSetup : IConfigureOptions<MvcOptions>, IPostConfigureOptions<MvcOptions>
{
private readonly IHttpRequestStreamReaderFactory _readerFactory;
private readonly ILoggerFactory _loggerFactory;

// Used in tests
public MvcCoreMvcOptionsSetup(IHttpRequestStreamReaderFactory readerFactory)
: this(readerFactory, NullLoggerFactory.Instance)
{
Expand Down Expand Up @@ -83,6 +84,15 @@ public void Configure(MvcOptions options)
options.ModelValidatorProviders.Add(new DefaultModelValidatorProvider());
}

public void PostConfigure(string name, MvcOptions options)
{
// HasValidatorsValidationMetadataProvider uses the results of other ValidationMetadataProvider to determine if a model requires
// validation. It is imperative that this executes later than all other metadata provider. We'll register it as part of PostConfigure.
// This should ensure it appears later than all of the details provider registered by MVC and user configured details provider registered
// as part of ConfigureOptions.
options.ModelMetadataDetailsProviders.Add(new HasValidatorsValidationMetadataProvider(options.ModelValidatorProviders));
}

internal static void ConfigureAdditionalModelMetadataDetailsProviders(IList<IMetadataDetailsProvider> modelMetadataDetailsProviders)
{
// Don't bind the Type class by default as it's expensive. A user can override this behavior
Expand Down

0 comments on commit fb57810

Please sign in to comment.