Get `[Remote]` and supporting classes building

- #439 (2 of 3)
- correct namespaces
- correct `Resources` class and member names; add new resources
- add `RequestServices` property to `ClientModelValidationContext`
- adjust to modern `IUrlHelper` API
- add `IClientModelValidator` support in `DataAnnotationsModelValidator`
- move previously-unused `StringSplit()` to `RemoteAttribute` and rename
- rewrite `RemoteAttributeTest`
 - improve test method names

Reduce number of `[Remote]` constructor overloads
- remove `AreaReference` enum and related `[Remote]` constructor overload
 - use `null` or empty `string` as explicit reference to the root area
- generally reduce parameter validation; match `UrlHelper`

Cleanup
- correct Engineering Guidelines violations
 - especially: add doc comments
- correct spelling error in `_additonalFieldsSplit`

nits:
- minimize `null` checks in `AdditionalFields`
- make `GetClientValidationRules` `virtual`; some subclasses use `new` today
- add tests of `DataAnnotationsModelValidator.GetClientValidationRules()`
- remove `builder.ToString()` calls since it appears
  https://roslyn.codeplex.com/workitem/246 has been resolved or Moq has
  worked around that issue.
This commit is contained in:
Doug Bunting 2015-01-25 22:14:45 -08:00
parent 1af1583302
commit 80b004678d
18 changed files with 896 additions and 473 deletions

View File

@ -1,20 +0,0 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
namespace System.Web.Mvc
{
/// <summary>
/// Controls interpretation of a controller name when constructing a <see cref="RemoteAttribute"/>.
/// </summary>
public enum AreaReference
{
/// <summary>
/// Find the controller in the current area.
/// </summary>
UseCurrent = 0,
/// <summary>
/// Find the controller in the root area.
/// </summary>
UseRoot = 1,
}
}

View File

@ -1,26 +1,47 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System;
using Microsoft.AspNet.Mvc.ModelBinding;
namespace System.Web.Mvc
namespace Microsoft.AspNet.Mvc.Internal
{
[TypeForwardedFrom("System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35")]
/// <summary>
/// <see cref="ModelClientValidationRule"/> containing information for HTML attribute generation in fields a
/// <see cref="RemoteAttribute"/> targets.
/// </summary>
public class ModelClientValidationRemoteRule : ModelClientValidationRule
{
[SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", Justification = "The value is a not a regular URL since it may contain ~/ ASP.NET-specific characters")]
public ModelClientValidationRemoteRule(string errorMessage, string url, string httpMethod, string additionalFields)
{
ErrorMessage = errorMessage;
ValidationType = "remote";
ValidationParameters["url"] = url;
private const string RemoteValidationType = "remote";
private const string AdditionalFieldsValidationParameter = "additionalfields";
private const string TypeValidationParameter = "type";
private const string UrlValidationParameter = "url";
if (!String.IsNullOrEmpty(httpMethod))
/// <summary>
/// Initializes a new instance of the <see cref="ModelClientValidationRemoteRule"/> class.
/// </summary>
/// <param name="errorMessage">Error message client should display when validation fails.</param>
/// <param name="url">URL where client should send a validation request.</param>
/// <param name="httpMethod">
/// HTTP method (<c>"GET"</c> or <c>"POST"</c>) client should use when sending a validation request.
/// </param>
/// <param name="additionalFields">
/// Comma-separated names of fields the client should include in a validation request.
/// </param>
public ModelClientValidationRemoteRule(
string errorMessage,
string url,
string httpMethod,
string additionalFields)
: base(validationType: RemoteValidationType, errorMessage: errorMessage)
{
ValidationParameters[UrlValidationParameter] = url;
if (!string.IsNullOrEmpty(httpMethod))
{
ValidationParameters["type"] = httpMethod;
ValidationParameters[TypeValidationParameter] = httpMethod;
}
ValidationParameters["additionalfields"] = additionalFields;
ValidationParameters[AdditionalFieldsValidationParameter] = additionalFields;
}
}
}

View File

@ -1706,6 +1706,38 @@ namespace Microsoft.AspNet.Mvc.Core
return string.Format(CultureInfo.CurrentCulture, GetString("ApiExplorer_UnsupportedAction"), p0);
}
/// <summary>
/// No URL for remote validation could be found.
/// </summary>
internal static string RemoteAttribute_NoUrlFound
{
get { return GetString("RemoteAttribute_NoUrlFound"); }
}
/// <summary>
/// No URL for remote validation could be found.
/// </summary>
internal static string FormatRemoteAttribute_NoUrlFound()
{
return GetString("RemoteAttribute_NoUrlFound");
}
/// <summary>
/// '{0}' is invalid.
/// </summary>
internal static string RemoteAttribute_RemoteValidationFailed
{
get { return GetString("RemoteAttribute_RemoteValidationFailed"); }
}
/// <summary>
/// '{0}' is invalid.
/// </summary>
internal static string FormatRemoteAttribute_RemoteValidationFailed(object p0)
{
return string.Format(CultureInfo.CurrentCulture, GetString("RemoteAttribute_RemoteValidationFailed"), p0);
}
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);

View File

@ -1,158 +1,251 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Web.Mvc.Properties;
using System.Web.Routing;
using System.Linq;
using Microsoft.AspNet.Mvc.Core;
using Microsoft.AspNet.Mvc.Internal;
using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.AspNet.Routing;
using Microsoft.Framework.DependencyInjection;
namespace System.Web.Mvc
namespace Microsoft.AspNet.Mvc
{
[AttributeUsage(AttributeTargets.Property)]
[SuppressMessage("Microsoft.Design", "CA1019:DefineAccessorsForAttributeArguments", Justification = "The constructor parameters are used to feed RouteData, which is public.")]
[SuppressMessage("Microsoft.Performance", "CA1813:AvoidUnsealedAttributes", Justification = "This attribute is designed to be a base class for other attributes.")]
public class RemoteAttribute : ValidationAttribute, IClientValidatable
/// <summary>
/// A <see cref="ValidationAttribute"/> which configures Unobtrusive validation to send an Ajax request to the
/// web site. The invoked action should return JSON indicating whether the value is valid.
/// </summary>
/// <remarks>Does no server-side validation of the final form submission.</remarks>
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class RemoteAttribute : ValidationAttribute, IClientModelValidator
{
private string _additionalFields;
private string[] _additonalFieldsSplit = new string[0];
private string _additionalFields = string.Empty;
private string[] _additionalFieldsSplit = new string[0];
/// <summary>
/// Initializes a new instance of the <see cref="RemoteAttribute"/> class.
/// </summary>
/// <remarks>
/// Intended for subclasses that support URL generation with no route, action, or controller names.
/// </remarks>
protected RemoteAttribute()
: base(MvcResources.RemoteAttribute_RemoteValidationFailed)
: base(Resources.RemoteAttribute_RemoteValidationFailed)
{
RouteData = new RouteValueDictionary();
}
/// <summary>
/// Initializes a new instance of the <see cref="RemoteAttribute"/> class.
/// </summary>
/// <param name="routeName">
/// The route name used when generating the URL where client should send a validation request.
/// </param>
/// <remarks>
/// Finds the <paramref name="routeName"/> in any area of the application.
/// </remarks>
public RemoteAttribute(string routeName)
: this()
{
if (String.IsNullOrWhiteSpace(routeName))
{
throw new ArgumentException(MvcResources.Common_NullOrEmpty, "routeName");
}
RouteName = routeName;
}
/// <summary>
/// Initializes a new instance of the <see cref="RemoteAttribute"/> class.
/// </summary>
/// <param name="action">
/// The action name used when generating the URL where client should send a validation request.
/// </param>
/// <param name="controller">
/// The controller name used when generating the URL where client should send a validation request.
/// </param>
/// <remarks>
/// <para>
/// If either <paramref name="action"/> or <paramref name="controller"/> is <c>null</c>, uses the corresponding
/// ambient value.
/// </para>
/// <para>Finds the <paramref name="controller"/> in the current area.</para>
/// </remarks>
public RemoteAttribute(string action, string controller)
:
this(action, controller, null /* areaName */)
{
}
public RemoteAttribute(string action, string controller, string areaName)
: this()
{
if (String.IsNullOrWhiteSpace(action))
if (action != null)
{
throw new ArgumentException(MvcResources.Common_NullOrEmpty, "action");
}
if (String.IsNullOrWhiteSpace(controller))
{
throw new ArgumentException(MvcResources.Common_NullOrEmpty, "controller");
RouteData["action"] = action;
}
RouteData["controller"] = controller;
RouteData["action"] = action;
if (!String.IsNullOrWhiteSpace(areaName))
if (controller != null)
{
RouteData["area"] = areaName;
RouteData["controller"] = controller;
}
}
/// <summary>
/// Initializes a new instance of the <see cref="RemoteAttribute"/> class.
/// </summary>
/// <param name="action">The route name.</param>
/// <param name="controller">The name of the controller.</param>
/// <param name="areaReference">
/// Find the controller in the root if <see cref="AreaReference.UseRoot"/>. Otherwise look in the current area.
/// <param name="action">
/// The action name used when generating the URL where client should send a validation request.
/// </param>
public RemoteAttribute(string action, string controller, AreaReference areaReference)
/// <param name="controller">
/// The controller name used when generating the URL where client should send a validation request.
/// </param>
/// <param name="areaName">The name of the area containing the <paramref name="controller"/>.</param>
/// <remarks>
/// <para>
/// If either <paramref name="action"/> or <paramref name="controller"/> is <c>null</c>, uses the corresponding
/// ambient value.
/// </para>
/// If <paramref name="areaName"/> is <c>null</c>, finds the <paramref name="controller"/> in the root area.
/// Use the <see cref="RemoteAttribute(string, string)"/> overload find the <paramref name="controller"/> in
/// the current area. Or explicitly pass the current area's name as the <paramref name="areaName"/> argument to
/// this overload.
/// </remarks>
public RemoteAttribute(string action, string controller, string areaName)
: this(action, controller)
{
if (areaReference == AreaReference.UseRoot)
{
RouteData["area"] = null;
}
RouteData["area"] = areaName;
}
/// <summary>
/// Gets or sets the HTTP method (<c>"Get"</c> or <c>"Post"</c>) client should use when sending a validation
/// request.
/// </summary>
public string HttpMethod { get; set; }
/// <summary>
/// Gets or sets the comma-separated names of fields the client should include in a validation request.
/// </summary>
public string AdditionalFields
{
get { return _additionalFields ?? String.Empty; }
get { return _additionalFields; }
set
{
_additionalFields = value;
_additonalFieldsSplit = AuthorizeAttribute.SplitString(value);
_additionalFields = value ?? string.Empty;
_additionalFieldsSplit = SplitAndTrimPropertyNames(value)
.Select(field => FormatPropertyForClientValidation(field))
.ToArray();
}
}
protected RouteValueDictionary RouteData { get; private set; }
/// <summary>
/// Gets the <see cref="RouteValueDictionary"/> used when generating the URL where client should send a
/// validation request.
/// </summary>
protected RouteValueDictionary RouteData { get; }
/// <summary>
/// Gets or sets the route name used when generating the URL where client should send a validation request.
/// </summary>
protected string RouteName { get; set; }
protected virtual RouteCollection Routes
{
get { return RouteTable.Routes; }
}
/// <summary>
/// Formats <paramref name="property"/> and <see cref="AdditionalFields"/> for use in generated HTML.
/// </summary>
/// <param name="property">
/// Name of the property associated with this <see cref="RemoteAttribute"/> instance.
/// </param>
/// <returns>Comma-separated names of fields the client should include in a validation request.</returns>
/// <remarks>
/// Excludes any whitespace from <see cref="AdditionalFields"/> in the return value.
/// Prefixes each field name in the return value with <c>"*."</c>.
/// </remarks>
public string FormatAdditionalFieldsForClientValidation(string property)
{
if (String.IsNullOrEmpty(property))
if (string.IsNullOrEmpty(property))
{
throw new ArgumentException(MvcResources.Common_NullOrEmpty, "property");
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, "property");
}
string delimitedAdditionalFields = FormatPropertyForClientValidation(property);
foreach (string field in _additonalFieldsSplit)
var delimitedAdditionalFields = string.Join(",", _additionalFieldsSplit);
if (!string.IsNullOrEmpty(delimitedAdditionalFields))
{
delimitedAdditionalFields += "," + FormatPropertyForClientValidation(field);
delimitedAdditionalFields = "," + delimitedAdditionalFields;
}
return delimitedAdditionalFields;
var formattedString = FormatPropertyForClientValidation(property) + delimitedAdditionalFields;
return formattedString;
}
/// <summary>
/// Formats <paramref name="property"/> for use in generated HTML.
/// </summary>
/// <param name="property">One field name the client should include in a validation request.</param>
/// <returns>Name of a field the client should include in a validation request.</returns>
/// <remarks>Returns <paramref name="property"/> with a <c>"*."</c> prefix.</remarks>
public static string FormatPropertyForClientValidation(string property)
{
if (String.IsNullOrEmpty(property))
if (string.IsNullOrEmpty(property))
{
throw new ArgumentException(MvcResources.Common_NullOrEmpty, "property");
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, "property");
}
return "*." + property;
}
[SuppressMessage("Microsoft.Design", "CA1055:UriReturnValuesShouldNotBeStrings", Justification = "The value is a not a regular URL since it may contain ~/ ASP.NET-specific characters")]
protected virtual string GetUrl(ControllerContext controllerContext)
/// <summary>
/// Returns the URL where the client should send a validation request.
/// </summary>
/// <param name="context">The <see cref="ClientModelValidationContext"/> used to generate the URL.</param>
/// <returns>The URL where the client should send a validation request.</returns>
protected virtual string GetUrl([NotNull] ClientModelValidationContext context)
{
var pathData = Routes.GetVirtualPathForArea(controllerContext.RequestContext,
RouteName,
RouteData);
if (pathData == null)
var urlHelper = context.RequestServices.GetRequiredService<IUrlHelper>();
var url = urlHelper.RouteUrl(RouteName, values: RouteData, protocol: null, host: null, fragment: null);
if (url == null)
{
throw new InvalidOperationException(MvcResources.RemoteAttribute_NoUrlFound);
throw new InvalidOperationException(Resources.RemoteAttribute_NoUrlFound);
}
return pathData.VirtualPath;
return url;
}
/// <inheritdoc />
public override string FormatErrorMessage(string name)
{
return String.Format(CultureInfo.CurrentCulture, ErrorMessageString, name);
return string.Format(CultureInfo.CurrentCulture, ErrorMessageString, name);
}
/// <inheritdoc />
/// <remarks>
/// Always returns <c>true</c> since this <see cref="ValidationAttribute"/> does no validation itself.
/// Related validations occur only when the client sends a validation request.
/// </remarks>
public override bool IsValid(object value)
{
return true;
}
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
/// <inheritdoc />
/// <exception cref="InvalidOperationException">
/// Thrown if unable to generate a target URL for a validation request.
/// </exception>
public virtual IEnumerable<ModelClientValidationRule> GetClientValidationRules(
[NotNull] ClientModelValidationContext context)
{
yield return new ModelClientValidationRemoteRule(FormatErrorMessage(metadata.GetDisplayName()), GetUrl(context), HttpMethod, FormatAdditionalFieldsForClientValidation(metadata.PropertyName));
var metadata = context.ModelMetadata;
var rule = new ModelClientValidationRemoteRule(
FormatErrorMessage(metadata.GetDisplayName()),
GetUrl(context),
HttpMethod,
FormatAdditionalFieldsForClientValidation(metadata.PropertyName));
return new[] { rule };
}
private static IEnumerable<string> SplitAndTrimPropertyNames(string original)
{
if (string.IsNullOrEmpty(original))
{
return new string[0];
}
var split = original.Split(',')
.Select(piece => piece.Trim())
.Where(trimmed => !string.IsNullOrEmpty(trimmed));
return split;
}
}
}

View File

@ -724,16 +724,15 @@ namespace Microsoft.AspNet.Mvc.Rendering
string name)
{
var validatorProvider = _bindingContextAccessor.Value.ValidatorProvider;
metadata = metadata ??
ExpressionMetadataProvider.FromStringExpression(name, viewContext.ViewData, _metadataProvider);
var validationContext =
new ClientModelValidationContext(metadata, _metadataProvider, viewContext.HttpContext.RequestServices);
return
validatorProvider
return validatorProvider
.GetValidators(metadata)
.OfType<IClientModelValidator>()
.SelectMany(v => v.GetClientValidationRules(
new ClientModelValidationContext(metadata, _metadataProvider)));
.SelectMany(v => v.GetClientValidationRules(validationContext));
}
internal static string EvalString(ViewContext viewContext, string key, string format)

View File

@ -445,4 +445,10 @@
<data name="ApiExplorer_UnsupportedAction" xml:space="preserve">
<value>The action '{0}' has ApiExplorer enabled, but is using conventional routing. Only actions which use attribute routing support ApiExplorer.</value>
</data>
<data name="RemoteAttribute_NoUrlFound" xml:space="preserve">
<value>No URL for remote validation could be found.</value>
</data>
<data name="RemoteAttribute_RemoteValidationFailed" xml:space="preserve">
<value>'{0}' is invalid.</value>
</data>
</root>

View File

@ -351,19 +351,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
}
}
private static IEnumerable<string> SplitString(string original)
{
if (string.IsNullOrEmpty(original))
{
return new string[0];
}
var split = original.Split(',')
.Select(piece => piece.Trim())
.Where(trimmed => !string.IsNullOrEmpty(trimmed));
return split;
}
private class CompositePredicateProvider : IPropertyBindingPredicateProvider
{
private readonly IPropertyBindingPredicateProvider[] _providers;

View File

@ -1,19 +1,25 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
namespace Microsoft.AspNet.Mvc.ModelBinding
{
public class ClientModelValidationContext
{
public ClientModelValidationContext([NotNull] ModelMetadata metadata,
[NotNull] IModelMetadataProvider metadataProvider)
[NotNull] IModelMetadataProvider metadataProvider,
[NotNull] IServiceProvider requestServices)
{
ModelMetadata = metadata;
MetadataProvider = metadataProvider;
RequestServices = requestServices;
}
public ModelMetadata ModelMetadata { get; private set; }
public ModelMetadata ModelMetadata { get; }
public IModelMetadataProvider MetadataProvider { get; private set; }
public IModelMetadataProvider MetadataProvider { get; }
public IServiceProvider RequestServices { get; }
}
}

View File

@ -61,6 +61,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
public virtual IEnumerable<ModelClientValidationRule> GetClientValidationRules(
[NotNull] ClientModelValidationContext context)
{
var customValidator = Attribute as IClientModelValidator;
if (customValidator != null)
{
return customValidator.GetClientValidationRules(context);
}
return Enumerable.Empty<ModelClientValidationRule>();
}

View File

@ -1,37 +1,54 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Routing;
using Microsoft.TestCommon;
using Microsoft.AspNet.Builder;
using Microsoft.AspNet.Http.Core;
using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.AspNet.Routing;
using Microsoft.Framework.DependencyInjection;
using Microsoft.Framework.DependencyInjection.Fallback;
using Microsoft.Framework.Logging;
using Microsoft.Framework.OptionsModel;
using Moq;
using Xunit;
namespace System.Web.Mvc.Test
namespace Microsoft.AspNet.Mvc
{
public class RemoteAttributeTest
{
// Good route name, bad route name
// Controller + Action
private static readonly IModelMetadataProvider _metadataProvider = new EmptyModelMetadataProvider();
private static readonly ModelMetadata _metadata = _metadataProvider.GetMetadataForProperty(
modelAccessor: null,
containerType: typeof(string),
propertyName: "Length");
[Fact]
public void GuardClauses()
public static TheoryData<string> SomeNames
{
// Act & Assert
Assert.ThrowsArgumentNullOrEmpty(
() => new RemoteAttribute(null, "controller"),
"action");
Assert.ThrowsArgumentNullOrEmpty(
() => new RemoteAttribute("action", null),
"controller");
Assert.ThrowsArgumentNullOrEmpty(
() => new RemoteAttribute(null),
"routeName");
Assert.ThrowsArgumentNullOrEmpty(
() => RemoteAttribute.FormatPropertyForClientValidation(String.Empty),
"property");
Assert.ThrowsArgumentNullOrEmpty(
() => new RemoteAttribute("foo").FormatAdditionalFieldsForClientValidation(String.Empty),
"property");
get
{
return new TheoryData<string>
{
string.Empty,
"Action",
"In a controller",
" slightly\t odd\t whitespace\t\r\n",
};
}
}
// Null or empty property names are invalid. (Those containing just whitespace are legal.)
public static TheoryData<string> NullOrEmptyNames
{
get
{
return new TheoryData<string>
{
null,
string.Empty,
};
}
}
[Fact]
@ -43,408 +60,583 @@ namespace System.Web.Mvc.Test
}
[Fact]
public void BadRouteNameThrows()
public void Constructor_WithNullAction_IgnoresArgument()
{
// Arrange
ControllerContext context = new ControllerContext();
ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForType(null, typeof(object));
TestableRemoteAttribute attribute = new TestableRemoteAttribute("RouteName");
// Act & Assert
Assert.Throws<ArgumentException>(
() => new List<ModelClientValidationRule>(attribute.GetClientValidationRules(metadata, context)),
"A route named 'RouteName' could not be found in the route collection.\r\nParameter name: name");
}
[Fact]
public void NoRouteWithActionControllerThrows()
{
// Arrange
ControllerContext context = new ControllerContext();
ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForProperty(null, typeof(string), "Length");
TestableRemoteAttribute attribute = new TestableRemoteAttribute("Action", "Controller");
// Act & Assert
Assert.Throws<InvalidOperationException>(
() => new List<ModelClientValidationRule>(attribute.GetClientValidationRules(metadata, context)),
"No url for remote validation could be found.");
}
[Fact]
public void GoodRouteNameReturnsCorrectClientData()
{
// Arrange
string url = null;
ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForProperty(null, typeof(string), "Length");
TestableRemoteAttribute attribute = new TestableRemoteAttribute("RouteName");
attribute.RouteTable.Add("RouteName", new Route("my/url", new MvcRouteHandler()));
// Act
ModelClientValidationRule rule = attribute.GetClientValidationRules(metadata, GetMockControllerContext(url)).Single();
// Arrange & Act
var attribute = new TestableRemoteAttribute(action: null, controller: "AController");
// Assert
Assert.Equal("remote", rule.ValidationType);
Assert.Equal("'Length' is invalid.", rule.ErrorMessage);
Assert.Equal(2, rule.ValidationParameters.Count);
Assert.Equal("/my/url", rule.ValidationParameters["url"]);
var keyValuePair = Assert.Single(attribute.RouteData);
Assert.Equal(keyValuePair.Key, "controller");
}
[Fact]
public void ActionControllerReturnsCorrectClientDataWithoutNamedParameters()
public void Constructor_WithNullController_IgnoresArgument()
{
// Arrange
string url = null;
ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForProperty(null, typeof(string), "Length");
TestableRemoteAttribute attribute = new TestableRemoteAttribute("Action", "Controller");
attribute.RouteTable.Add(new Route("{controller}/{action}", new MvcRouteHandler()));
// Act
ModelClientValidationRule rule = attribute.GetClientValidationRules(metadata, GetMockControllerContext(url)).Single();
// Arrange & Act
var attribute = new TestableRemoteAttribute("AnAction", controller: null);
// Assert
var keyValuePair = Assert.Single(attribute.RouteData);
Assert.Equal(keyValuePair.Key, "action");
Assert.Null(attribute.RouteName);
}
[Theory]
[InlineData(null)]
[MemberData(nameof(SomeNames))]
public void Constructor_WithRouteName_UpdatesProperty(string routeName)
{
// Arrange & Act
var attribute = new TestableRemoteAttribute(routeName);
// Assert
Assert.Empty(attribute.RouteData);
Assert.Equal(routeName, attribute.RouteName);
}
[Theory]
[MemberData(nameof(SomeNames))]
public void Constructor_WithActionController_UpdatesActionRouteData(string action)
{
// Arrange & Act
var attribute = new TestableRemoteAttribute(action, "AController");
// Assert
Assert.Equal(2, attribute.RouteData.Count);
Assert.Contains("controller", attribute.RouteData.Keys);
var resultName = Assert.Single(
attribute.RouteData,
keyValuePair => string.Equals(keyValuePair.Key, "action", StringComparison.Ordinal))
.Value;
Assert.Equal(action, resultName);
Assert.Null(attribute.RouteName);
}
[Theory]
[MemberData(nameof(SomeNames))]
public void Constructor_WithActionController_UpdatesControllerRouteData(string controller)
{
// Arrange & Act
var attribute = new TestableRemoteAttribute("AnAction", controller);
// Assert
Assert.Equal(2, attribute.RouteData.Count);
Assert.Contains("action", attribute.RouteData.Keys);
var resultName = Assert.Single(
attribute.RouteData,
keyValuePair => string.Equals(keyValuePair.Key, "controller", StringComparison.Ordinal))
.Value;
Assert.Equal(controller, resultName);
Assert.Null(attribute.RouteName);
}
[Theory]
[InlineData(null)]
[MemberData(nameof(SomeNames))]
public void Constructor_WithActionControllerAreaName_UpdatesAreaRouteData(string areaName)
{
// Arrange & Act
var attribute = new TestableRemoteAttribute("AnAction", "AController", areaName: areaName);
// Assert
Assert.Equal(3, attribute.RouteData.Count);
Assert.Contains("action", attribute.RouteData.Keys);
Assert.Contains("controller", attribute.RouteData.Keys);
var resultName = Assert.Single(
attribute.RouteData,
keyValuePair => string.Equals(keyValuePair.Key, "area", StringComparison.Ordinal))
.Value;
Assert.Equal(areaName, resultName);
Assert.Null(attribute.RouteName);
}
[Theory]
[MemberData(nameof(NullOrEmptyNames))]
public void FormatAdditionalFieldsForClientValidation_WithInvalidPropertyName_Throws(string property)
{
// Arrange
var attribute = new RemoteAttribute(routeName: "default");
var expected = "Value cannot be null or empty." + Environment.NewLine + "Parameter name: property";
// Act & Assert
var exception = Assert.Throws<ArgumentException>(
"property",
() => attribute.FormatAdditionalFieldsForClientValidation(property));
Assert.Equal(expected, exception.Message);
}
[Theory]
[MemberData(nameof(NullOrEmptyNames))]
public void FormatPropertyForClientValidation_WithInvalidPropertyName_Throws(string property)
{
// Arrange
var expected = "Value cannot be null or empty." + Environment.NewLine + "Parameter name: property";
// Act & Assert
var exception = Assert.Throws<ArgumentException>(
"property",
() => RemoteAttribute.FormatPropertyForClientValidation(property));
Assert.Equal(expected, exception.Message);
}
[Fact]
public void GetClientValidationRules_WithBadRouteName_Throws()
{
// Arrange
var attribute = new RemoteAttribute("nonexistentRoute");
var context = GetValidationContextWithArea(currentArea: null);
// Act & Assert
var exception = Assert.Throws<InvalidOperationException>(() => attribute.GetClientValidationRules(context));
Assert.Equal("No URL for remote validation could be found.", exception.Message);
}
[Fact]
public void GetClientValidationRules_WithActionController_NoController_Throws()
{
// Arrange
var attribute = new RemoteAttribute("Action", "Controller");
var context = GetValidationContextWithNoController();
// Act & Assert
var exception = Assert.Throws<InvalidOperationException>(() => attribute.GetClientValidationRules(context));
Assert.Equal("No URL for remote validation could be found.", exception.Message);
}
[Fact]
public void GetClientValidationRules_WithRoute_CallsUrlHelperWithExpectedValues()
{
// Arrange
var routeName = "RouteName";
var attribute = new RemoteAttribute(routeName);
var url = "/my/URL";
var urlHelper = new MockUrlHelper(url, routeName);
var context = GetValidationContext(urlHelper);
// Act & Assert
var rule = Assert.Single(attribute.GetClientValidationRules(context));
Assert.Equal("remote", rule.ValidationType);
Assert.Equal("'Length' is invalid.", rule.ErrorMessage);
Assert.Equal(2, rule.ValidationParameters.Count);
Assert.Equal("/Controller/Action", rule.ValidationParameters["url"]);
Assert.Equal("*.Length", rule.ValidationParameters["additionalfields"]);
Assert.Throws<KeyNotFoundException>(
() => rule.ValidationParameters["type"],
"The given key was not present in the dictionary.");
Assert.Equal(url, rule.ValidationParameters["url"]);
var routeDictionary = Assert.IsType<RouteValueDictionary>(urlHelper.RouteValues);
Assert.Empty(routeDictionary);
}
[Fact]
public void ActionControllerReturnsCorrectClientDataWithNamedParameters()
public void GetClientValidationRules_WithActionController_CallsUrlHelperWithExpectedValues()
{
// Arrange
string url = null;
var attribute = new RemoteAttribute("Action", "Controller");
var url = "/Controller/Action";
var urlHelper = new MockUrlHelper(url, routeName: null);
var context = GetValidationContext(urlHelper);
ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForProperty(null, typeof(string), "Length");
TestableRemoteAttribute attribute = new TestableRemoteAttribute("Action", "Controller");
attribute.HttpMethod = "POST";
attribute.AdditionalFields = "Password,ConfirmPassword";
attribute.RouteTable.Add(new Route("{controller}/{action}", new MvcRouteHandler()));
// Act
ModelClientValidationRule rule = attribute.GetClientValidationRules(metadata, GetMockControllerContext(url)).Single();
// Assert
// Act & Assert
var rule = Assert.Single(attribute.GetClientValidationRules(context));
Assert.Equal("remote", rule.ValidationType);
Assert.Equal("'Length' is invalid.", rule.ErrorMessage);
Assert.Equal(2, rule.ValidationParameters.Count);
Assert.Equal("*.Length", rule.ValidationParameters["additionalfields"]);
Assert.Equal(url, rule.ValidationParameters["url"]);
var routeDictionary = Assert.IsType<RouteValueDictionary>(urlHelper.RouteValues);
Assert.Equal(2, routeDictionary.Count);
Assert.Equal("Action", routeDictionary["action"] as string);
Assert.Equal("Controller", routeDictionary["controller"] as string);
}
[Fact]
public void GetClientValidationRules_WithActionController_PropertiesSet_CallsUrlHelperWithExpectedValues()
{
// Arrange
var attribute = new RemoteAttribute("Action", "Controller")
{
HttpMethod = "POST",
AdditionalFields = "Password,ConfirmPassword",
};
var url = "/Controller/Action";
var urlHelper = new MockUrlHelper(url, routeName: null);
var context = GetValidationContext(urlHelper);
// Act & Assert
var rule = Assert.Single(attribute.GetClientValidationRules(context));
Assert.Equal("remote", rule.ValidationType);
Assert.Equal("'Length' is invalid.", rule.ErrorMessage);
Assert.Equal(3, rule.ValidationParameters.Count);
Assert.Equal("/Controller/Action", rule.ValidationParameters["url"]);
Assert.Equal("*.Length,*.Password,*.ConfirmPassword", rule.ValidationParameters["additionalfields"]);
Assert.Equal("POST", rule.ValidationParameters["type"]);
Assert.Equal(url, rule.ValidationParameters["url"]);
var routeDictionary = Assert.IsType<RouteValueDictionary>(urlHelper.RouteValues);
Assert.Equal(2, routeDictionary.Count);
Assert.Equal("Action", routeDictionary["action"] as string);
Assert.Equal("Controller", routeDictionary["controller"] as string);
}
// Current area is root in this case.
[Fact]
public void ActionController_RemoteFindsControllerInCurrentArea()
public void GetClientValidationRules_WithActionControllerArea_CallsUrlHelperWithExpectedValues()
{
// Arrange
ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForProperty(modelAccessor: null,
containerType: typeof(string), propertyName: "Length");
TestableRemoteAttribute attribute = new TestableRemoteAttribute("Action", "Controller");
attribute.HttpMethod = "POST";
var attribute = new RemoteAttribute("Action", "Controller", "Test")
{
HttpMethod = "POST",
};
var url = "/Test/Controller/Action";
var urlHelper = new MockUrlHelper(url, routeName: null);
var context = GetValidationContext(urlHelper);
var context = new AreaRegistrationContext("Test", attribute.RouteTable);
context.MapRoute(name: null, url: "Test/{controller}/{action}");
attribute.RouteTable.Add(new Route("{controller}/{action}", new MvcRouteHandler()));
// Act
ModelClientValidationRule rule =
attribute.GetClientValidationRules(metadata, GetMockControllerContext(url: null)).Single();
// Assert
// Act & Assert
var rule = Assert.Single(attribute.GetClientValidationRules(context));
Assert.Equal("remote", rule.ValidationType);
Assert.Equal("'Length' is invalid.", rule.ErrorMessage);
Assert.Equal(3, rule.ValidationParameters.Count);
Assert.Equal("*.Length", rule.ValidationParameters["additionalfields"]);
Assert.Equal("POST", rule.ValidationParameters["type"]);
Assert.Equal(url, rule.ValidationParameters["url"]);
var routeDictionary = Assert.IsType<RouteValueDictionary>(urlHelper.RouteValues);
Assert.Equal(3, routeDictionary.Count);
Assert.Equal("Action", routeDictionary["action"] as string);
Assert.Equal("Controller", routeDictionary["controller"] as string);
Assert.Equal("Test", routeDictionary["area"] as string);
}
// Root area is current in this case.
[Fact]
public void GetClientValidationRules_WithActionController_FindsControllerInCurrentArea()
{
// Arrange
var attribute = new RemoteAttribute("Action", "Controller");
var context = GetValidationContextWithArea(currentArea: null);
// Act & Assert
var rule = Assert.Single(attribute.GetClientValidationRules(context));
Assert.Equal("remote", rule.ValidationType);
Assert.Equal("'Length' is invalid.", rule.ErrorMessage);
Assert.Equal(2, rule.ValidationParameters.Count);
Assert.Equal("*.Length", rule.ValidationParameters["additionalfields"]);
Assert.Equal("/Controller/Action", rule.ValidationParameters["url"]);
}
// Test area is current in this case.
[Fact]
public void ActionControllerArea_RemoteFindsControllerInNamedArea()
public void GetClientValidationRules_WithActionControllerInArea_FindsControllerInCurrentArea()
{
// Arrange
ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForProperty(modelAccessor: null,
containerType: typeof(string), propertyName: "Length");
TestableRemoteAttribute attribute = new TestableRemoteAttribute("Action", "Controller", "Test");
attribute.HttpMethod = "POST";
var attribute = new RemoteAttribute("Action", "Controller");
var context = GetValidationContextWithArea(currentArea: "Test");
var context = new AreaRegistrationContext("Test", attribute.RouteTable);
context.MapRoute(name: null, url: "Test/{controller}/{action}");
attribute.RouteTable.Add(new Route("{controller}/{action}", new MvcRouteHandler()));
// Act
ModelClientValidationRule rule =
attribute.GetClientValidationRules(metadata, GetMockControllerContext(url: null)).Single();
// Assert
// Act & Assert
var rule = Assert.Single(attribute.GetClientValidationRules(context));
Assert.Equal("remote", rule.ValidationType);
Assert.Equal("'Length' is invalid.", rule.ErrorMessage);
Assert.Equal(2, rule.ValidationParameters.Count);
Assert.Equal("*.Length", rule.ValidationParameters["additionalfields"]);
Assert.Equal("/Test/Controller/Action", rule.ValidationParameters["url"]);
}
// Current area is root in this case.
[Fact]
public void ActionControllerArea_WithEmptyArea_RemoteFindsControllerInCurrentArea()
// Explicit reference to the (current) root area.
[Theory]
[MemberData(nameof(NullOrEmptyNames))]
public void GetClientValidationRules_WithActionControllerArea_FindsControllerInRootArea(string areaName)
{
// Arrange
ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForProperty(modelAccessor: null,
containerType: typeof(string), propertyName: "Length");
TestableRemoteAttribute attribute = new TestableRemoteAttribute("Action", "Controller", "");
attribute.HttpMethod = "POST";
var attribute = new RemoteAttribute("Action", "Controller", areaName);
var context = GetValidationContextWithArea(currentArea: null);
var context = new AreaRegistrationContext("Test", attribute.RouteTable);
context.MapRoute(name: null, url: "Test/{controller}/{action}");
attribute.RouteTable.Add(new Route("{controller}/{action}", new MvcRouteHandler()));
// Act
ModelClientValidationRule rule =
attribute.GetClientValidationRules(metadata, GetMockControllerContext(url: null)).Single();
// Assert
// Act & Assert
var rule = Assert.Single(attribute.GetClientValidationRules(context));
Assert.Equal("remote", rule.ValidationType);
Assert.Equal("'Length' is invalid.", rule.ErrorMessage);
Assert.Equal(2, rule.ValidationParameters.Count);
Assert.Equal("*.Length", rule.ValidationParameters["additionalfields"]);
Assert.Equal("/Controller/Action", rule.ValidationParameters["url"]);
}
// Current area is root in this case.
[Fact]
public void ActionControllerAreaReference_WithUseCurrent_RemoteFindsControllerInCurrentArea()
// Test area is current in this case.
[Theory]
[MemberData(nameof(NullOrEmptyNames))]
public void GetClientValidationRules_WithActionControllerAreaInArea_FindsControllerInRootArea(string areaName)
{
// Arrange
ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForProperty(modelAccessor: null,
containerType: typeof(string), propertyName: "Length");
TestableRemoteAttribute attribute = new TestableRemoteAttribute("Action", "Controller", AreaReference.UseCurrent);
attribute.HttpMethod = "POST";
var attribute = new RemoteAttribute("Action", "Controller", areaName);
var context = GetValidationContextWithArea(currentArea: "Test");
var context = new AreaRegistrationContext("Test", attribute.RouteTable);
context.MapRoute(name: null, url: "Test/{controller}/{action}");
attribute.RouteTable.Add(new Route("{controller}/{action}", new MvcRouteHandler()));
// Act
ModelClientValidationRule rule =
attribute.GetClientValidationRules(metadata, GetMockControllerContext(url: null)).Single();
// Assert
// Act & Assert
var rule = Assert.Single(attribute.GetClientValidationRules(context));
Assert.Equal("remote", rule.ValidationType);
Assert.Equal("'Length' is invalid.", rule.ErrorMessage);
Assert.Equal(2, rule.ValidationParameters.Count);
Assert.Equal("*.Length", rule.ValidationParameters["additionalfields"]);
Assert.Equal("/Controller/Action", rule.ValidationParameters["url"]);
}
// Root area is current in this case.
[Fact]
public void ActionControllerAreaReference_WithUseRoot_RemoteFindsControllerInRoot()
public void GetClientValidationRules_WithActionControllerArea_FindsControllerInNamedArea()
{
// Arrange
ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForProperty(modelAccessor: null,
containerType: typeof(string), propertyName: "Length");
TestableRemoteAttribute attribute = new TestableRemoteAttribute("Action", "Controller", AreaReference.UseRoot);
attribute.HttpMethod = "POST";
var attribute = new RemoteAttribute("Action", "Controller", "Test");
var context = GetValidationContextWithArea(currentArea: null);
var context = new AreaRegistrationContext("Test", attribute.RouteTable);
context.MapRoute(name: null, url: "Test/{controller}/{action}");
attribute.RouteTable.Add(new Route("{controller}/{action}", new MvcRouteHandler()));
// Act
ModelClientValidationRule rule =
attribute.GetClientValidationRules(metadata, GetMockControllerContext(url: null)).Single();
// Assert
// Act & Assert
var rule = Assert.Single(attribute.GetClientValidationRules(context));
Assert.Equal("remote", rule.ValidationType);
Assert.Equal("/Controller/Action", rule.ValidationParameters["url"]);
}
Assert.Equal("'Length' is invalid.", rule.ErrorMessage);
// Current area is Test in this case.
[Fact]
public void ActionController_InArea_RemoteFindsControllerInCurrentArea()
{
// Arrange
ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForProperty(modelAccessor: null,
containerType: typeof(string), propertyName: "Length");
TestableRemoteAttribute attribute = new TestableRemoteAttribute("Action", "Controller");
attribute.HttpMethod = "POST";
var context = new AreaRegistrationContext("Test", attribute.RouteTable);
context.MapRoute(name: null, url: "Test/{controller}/{action}");
attribute.RouteTable.Add(new Route("{controller}/{action}", new MvcRouteHandler()));
// Act
ModelClientValidationRule rule =
attribute.GetClientValidationRules(metadata, GetMockControllerContextWithArea(url: null, areaName: "Test"))
.Single();
// Assert
Assert.Equal("remote", rule.ValidationType);
Assert.Equal(2, rule.ValidationParameters.Count);
Assert.Equal("*.Length", rule.ValidationParameters["additionalfields"]);
Assert.Equal("/Test/Controller/Action", rule.ValidationParameters["url"]);
}
// Explicit reference to the Test area.
// Explicit reference to the current (Test) area.
[Fact]
public void ActionControllerArea_InSameArea_RemoteFindsControllerInNamedArea()
public void GetClientValidationRules_WithActionControllerAreaInArea_FindsControllerInNamedArea()
{
// Arrange
ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForProperty(modelAccessor: null,
containerType: typeof(string), propertyName: "Length");
TestableRemoteAttribute attribute = new TestableRemoteAttribute("Action", "Controller", "Test");
attribute.HttpMethod = "POST";
var attribute = new RemoteAttribute("Action", "Controller", "Test");
var context = GetValidationContextWithArea(currentArea: "Test");
var context = new AreaRegistrationContext("Test", attribute.RouteTable);
context.MapRoute(name: null, url: "Test/{controller}/{action}");
attribute.RouteTable.Add(new Route("{controller}/{action}", new MvcRouteHandler()));
// Act
ModelClientValidationRule rule =
attribute.GetClientValidationRules(metadata, GetMockControllerContextWithArea(url: null, areaName: "Test"))
.Single();
// Assert
// Act & Assert
var rule = Assert.Single(attribute.GetClientValidationRules(context));
Assert.Equal("remote", rule.ValidationType);
Assert.Equal("'Length' is invalid.", rule.ErrorMessage);
Assert.Equal(2, rule.ValidationParameters.Count);
Assert.Equal("*.Length", rule.ValidationParameters["additionalfields"]);
Assert.Equal("/Test/Controller/Action", rule.ValidationParameters["url"]);
}
// Test area is current in this case.
[Fact]
public void ActionControllerArea_InArea_RemoteFindsControllerInNamedArea()
public void GetClientValidationRules_WithActionControllerAreaInArea_FindsControllerInDifferentArea()
{
// Arrange
ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForProperty(modelAccessor: null,
containerType: typeof(string), propertyName: "Length");
TestableRemoteAttribute attribute = new TestableRemoteAttribute("Action", "Controller", "AnotherArea");
attribute.HttpMethod = "POST";
var attribute = new RemoteAttribute("Action", "Controller", "AnotherArea");
var context = GetValidationContextWithArea(currentArea: "Test");
var context = new AreaRegistrationContext("Test", attribute.RouteTable);
context.MapRoute(name: null, url: "Test/{controller}/{action}");
context = new AreaRegistrationContext("AnotherArea", attribute.RouteTable);
context.MapRoute(name: null, url: "AnotherArea/{controller}/{action}");
attribute.RouteTable.Add(new Route("{controller}/{action}", new MvcRouteHandler()));
// Act
ModelClientValidationRule rule =
attribute.GetClientValidationRules(metadata, GetMockControllerContextWithArea(url: null, areaName: "Test"))
.Single();
// Assert
// Act & Assert
var rule = Assert.Single(attribute.GetClientValidationRules(context));
Assert.Equal("remote", rule.ValidationType);
Assert.Equal("'Length' is invalid.", rule.ErrorMessage);
Assert.Equal(2, rule.ValidationParameters.Count);
Assert.Equal("*.Length", rule.ValidationParameters["additionalfields"]);
Assert.Equal("/AnotherArea/Controller/Action", rule.ValidationParameters["url"]);
}
// Current area is Test in this case.
[Fact]
public void ActionControllerArea_WithEmptyAreaInArea_RemoteFindsControllerInCurrentArea()
private static ClientModelValidationContext GetValidationContext(IUrlHelper urlHelper)
{
// Arrange
ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForProperty(modelAccessor: null,
containerType: typeof(string), propertyName: "Length");
TestableRemoteAttribute attribute = new TestableRemoteAttribute("Action", "Controller", "");
attribute.HttpMethod = "POST";
var serviceCollection = GetServiceCollection();
serviceCollection.AddInstance<IUrlHelper>(urlHelper);
var serviceProvider = serviceCollection.BuildServiceProvider();
var context = new AreaRegistrationContext("Test", attribute.RouteTable);
context.MapRoute(name: null, url: "Test/{controller}/{action}");
attribute.RouteTable.Add(new Route("{controller}/{action}", new MvcRouteHandler()));
// Act
ModelClientValidationRule rule =
attribute.GetClientValidationRules(metadata, GetMockControllerContextWithArea(url: null, areaName: "Test"))
.Single();
// Assert
Assert.Equal("remote", rule.ValidationType);
Assert.Equal("/Test/Controller/Action", rule.ValidationParameters["url"]);
return new ClientModelValidationContext(_metadata, _metadataProvider, serviceProvider);
}
// Current area is Test in this case.
[Fact]
public void ActionControllerAreaReference_WithUseCurrentInArea_RemoteFindsControllerInCurrentArea()
private static ClientModelValidationContext GetValidationContextWithArea(string currentArea)
{
// Arrange
ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForProperty(modelAccessor: null,
containerType: typeof(string), propertyName: "Length");
TestableRemoteAttribute attribute = new TestableRemoteAttribute("Action", "Controller", AreaReference.UseCurrent);
attribute.HttpMethod = "POST";
var serviceCollection = GetServiceCollection();
var serviceProvider = serviceCollection.BuildServiceProvider();
var routeCollection = GetRouteCollectionWithArea(serviceProvider);
var routeData = new RouteData
{
Routers =
{
routeCollection,
},
Values =
{
{ "action", "Index" },
{ "controller", "Home" },
},
};
if (!string.IsNullOrEmpty(currentArea))
{
routeData.Values["area"] = currentArea;
}
var context = new AreaRegistrationContext("Test", attribute.RouteTable);
context.MapRoute(name: null, url: "Test/{controller}/{action}");
var contextAccessor = GetContextAccessor(serviceProvider, routeData);
var actionSelector = new Mock<IActionSelector>(MockBehavior.Strict);
var urlHelper = new UrlHelper(contextAccessor, actionSelector.Object);
serviceCollection.AddInstance<IUrlHelper>(urlHelper);
serviceProvider = serviceCollection.BuildServiceProvider();
attribute.RouteTable.Add(new Route("{controller}/{action}", new MvcRouteHandler()));
// Act
ModelClientValidationRule rule =
attribute.GetClientValidationRules(metadata, GetMockControllerContextWithArea(url: null, areaName: "Test"))
.Single();
// Assert
Assert.Equal("remote", rule.ValidationType);
Assert.Equal("/Test/Controller/Action", rule.ValidationParameters["url"]);
return new ClientModelValidationContext(_metadata, _metadataProvider, serviceProvider);
}
[Fact]
public void ActionControllerAreaReference_WithUseRootInArea_RemoteFindsControllerInRoot()
private static ClientModelValidationContext GetValidationContextWithNoController()
{
// Arrange
ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForProperty(modelAccessor: null,
containerType: typeof(string), propertyName: "Length");
TestableRemoteAttribute attribute = new TestableRemoteAttribute("Action", "Controller", AreaReference.UseRoot);
attribute.HttpMethod = "POST";
var serviceCollection = GetServiceCollection();
var serviceProvider = serviceCollection.BuildServiceProvider();
var routeCollection = GetRouteCollectionWithNoController(serviceProvider);
var routeData = new RouteData
{
Routers =
{
routeCollection,
},
};
var context = new AreaRegistrationContext("Test", attribute.RouteTable);
context.MapRoute(name: null, url: "Test/{controller}/{action}");
var contextAccessor = GetContextAccessor(serviceProvider, routeData);
var actionSelector = new Mock<IActionSelector>(MockBehavior.Strict);
var urlHelper = new UrlHelper(contextAccessor, actionSelector.Object);
serviceCollection.AddInstance<IUrlHelper>(urlHelper);
serviceProvider = serviceCollection.BuildServiceProvider();
attribute.RouteTable.Add(new Route("{controller}/{action}", new MvcRouteHandler()));
// Act
ModelClientValidationRule rule =
attribute.GetClientValidationRules(metadata, GetMockControllerContextWithArea(url: null, areaName: "Test"))
.Single();
// Assert
Assert.Equal("remote", rule.ValidationType);
Assert.Equal("/Controller/Action", rule.ValidationParameters["url"]);
return new ClientModelValidationContext(_metadata, _metadataProvider, serviceProvider);
}
private ControllerContext GetMockControllerContext(string url)
private static IRouter GetRouteCollectionWithArea(IServiceProvider serviceProvider)
{
Mock<ControllerContext> context = new Mock<ControllerContext>();
context.Setup(c => c.HttpContext.Request.ApplicationPath)
.Returns("/");
context.Setup(c => c.HttpContext.Response.ApplyAppPathModifier(It.IsAny<string>()))
.Callback<string>(vpath => url = vpath)
.Returns(() => url);
var builder = GetRouteBuilder(serviceProvider, isBound: true);
return context.Object;
// Setting IsBound to true makes order more important than usual. First try the route that requires the
// area value. Skip usual "area:exists" constraint because that isn't relevant for link generation and it
// complicates the setup significantly.
builder.MapRoute("areaRoute", "{area}/{controller}/{action}");
builder.MapRoute("default", "{controller}/{action}", new { controller = "Home", action = "Index" });
return builder.Build();
}
private ControllerContext GetMockControllerContextWithArea(string url, string areaName)
private static IRouter GetRouteCollectionWithNoController(IServiceProvider serviceProvider)
{
Mock<ControllerContext> context = new Mock<ControllerContext>();
context.Setup(c => c.HttpContext.Request.ApplicationPath)
.Returns("/");
context.Setup(c => c.HttpContext.Response.ApplyAppPathModifier(It.IsAny<string>()))
.Callback<string>(vpath => url = vpath)
.Returns(() => url);
var builder = GetRouteBuilder(serviceProvider, isBound: false);
builder.MapRoute("default", "static/route");
var controllerContext = context.Object;
return builder.Build();
}
controllerContext.RequestContext.RouteData.DataTokens.Add("area", areaName);
private static RouteBuilder GetRouteBuilder(IServiceProvider serviceProvider, bool isBound)
{
var builder = new RouteBuilder
{
ServiceProvider = serviceProvider,
};
return controllerContext;
var handler = new Mock<IRouter>(MockBehavior.Strict);
handler
.Setup(router => router.GetVirtualPath(It.IsAny<VirtualPathContext>()))
.Callback<VirtualPathContext>(context => context.IsBound = isBound)
.Returns((string)null);
builder.DefaultHandler = handler.Object;
return builder;
}
private static IScopedInstance<ActionContext> GetContextAccessor(
IServiceProvider serviceProvider,
RouteData routeData = null)
{
// Set IServiceProvider properties because TemplateRoute gets services (e.g. an ILoggerFactory instance)
// through the HttpContext.
var httpContext = new DefaultHttpContext
{
ApplicationServices = serviceProvider,
RequestServices = serviceProvider,
};
if (routeData == null)
{
routeData = new RouteData
{
Routers = { Mock.Of<IRouter>(), },
};
}
var actionContext = new ActionContext(httpContext, routeData, new ActionDescriptor());
var contextAccessor = new Mock<IScopedInstance<ActionContext>>();
contextAccessor
.SetupGet(accessor => accessor.Value)
.Returns(actionContext);
return contextAccessor.Object;
}
private static ServiceCollection GetServiceCollection()
{
var serviceCollection = new ServiceCollection();
serviceCollection.AddInstance<ILoggerFactory>(new NullLoggerFactory());
var routeOptions = new RouteOptions();
var accessor = new Mock<IOptions<RouteOptions>>();
accessor
.SetupGet(options => options.Options)
.Returns(routeOptions);
// DefaultInlineConstraintResolver constructor does not currently check its serviceProvider parameter (e.g.
// for null) and the class does not look up any services.
serviceCollection.AddInstance<IInlineConstraintResolver>(new DefaultInlineConstraintResolver(
serviceProvider: null,
routeOptions: accessor.Object));
return serviceCollection;
}
private class MockUrlHelper : IUrlHelper
{
private readonly string _routeName;
private readonly string _url;
public MockUrlHelper(string url, string routeName)
{
_routeName = routeName;
_url = url;
}
public object RouteValues { get; private set; }
public string Action(
string action,
string controller,
object values,
string protocol,
string host,
string fragment)
{
throw new NotImplementedException();
}
public string Content(string contentPath)
{
throw new NotImplementedException();
}
public bool IsLocalUrl(string url)
{
throw new NotImplementedException();
}
public string RouteUrl(string routeName, object values, string protocol, string host, string fragment)
{
Assert.Equal(_routeName, routeName);
Assert.Null(protocol);
Assert.Null(host);
Assert.Null(fragment);
RouteValues = values;
return _url;
}
}
private class TestableRemoteAttribute : RemoteAttribute
{
public RouteCollection RouteTable = new RouteCollection();
public TestableRemoteAttribute(string action, string controller, AreaReference areaReference)
: base(action, controller, areaReference)
{
}
public TestableRemoteAttribute(string action, string controller, string areaName)
: base(action, controller, areaName)
public TestableRemoteAttribute(string routeName)
: base(routeName)
{
}
@ -453,14 +645,25 @@ namespace System.Web.Mvc.Test
{
}
public TestableRemoteAttribute(string routeName)
: base(routeName)
public TestableRemoteAttribute(string action, string controller, string areaName)
: base(action, controller, areaName)
{
}
protected override RouteCollection Routes
public new string RouteName
{
get { return RouteTable; }
get
{
return base.RouteName;
}
}
public new RouteValueDictionary RouteData
{
get
{
return base.RouteData;
}
}
}
}

View File

@ -13,7 +13,7 @@ using Microsoft.Framework.OptionsModel;
using Moq;
using Xunit;
namespace Microsoft.AspNet.Mvc.Core.Test
namespace Microsoft.AspNet.Mvc
{
public class UrlHelperTest
{
@ -695,7 +695,7 @@ namespace Microsoft.AspNet.Mvc.Core.Test
private static UrlHelper CreateUrlHelperWithRouteCollection(string appPrefix)
{
var routeCollection = GetRouter();
return CreateUrlHelper("/app", routeCollection);
return CreateUrlHelper(appPrefix, routeCollection);
}
private static IRouter GetRouter()
@ -708,13 +708,9 @@ namespace Microsoft.AspNet.Mvc.Core.Test
var rt = new RouteBuilder();
var target = new Mock<IRouter>(MockBehavior.Strict);
target
.Setup(e => e.GetVirtualPath(It.IsAny<VirtualPathContext>()))
.Callback<VirtualPathContext>(c =>
{
rt.ToString();
c.IsBound = true;
})
.Returns<VirtualPathContext>(rc => null);
.Setup(router => router.GetVirtualPath(It.IsAny<VirtualPathContext>()))
.Callback<VirtualPathContext>(context => context.IsBound = true)
.Returns<VirtualPathContext>(context => null);
rt.DefaultHandler = target.Object;
var serviceProviderMock = new Mock<IServiceProvider>();
var accessorMock = new Mock<IOptions<RouteOptions>>();

View File

@ -3,6 +3,8 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNet.Testing;
using Microsoft.Framework.DependencyInjection;
using Microsoft.Framework.DependencyInjection.Fallback;
using Xunit;
namespace Microsoft.AspNet.Mvc.ModelBinding
@ -17,7 +19,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var metadataProvider = new DataAnnotationsModelMetadataProvider();
var metadata = metadataProvider.GetMetadataForProperty(() => null, typeof(PropertyDisplayNameModel), "MyProperty");
var attribute = new CompareAttribute("OtherProperty");
var context = new ClientModelValidationContext(metadata, metadataProvider);
var serviceCollection = new ServiceCollection();
var requestServices = serviceCollection.BuildServiceProvider();
var context = new ClientModelValidationContext(metadata, metadataProvider, requestServices);
var adapter = new CompareAttributeAdapter(attribute);
// Act
@ -36,7 +40,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var metadataProvider = new DataAnnotationsModelMetadataProvider();
var metadata = metadataProvider.GetMetadataForProperty(() => null, typeof(PropertyNameModel), "MyProperty");
var attribute = new CompareAttribute("OtherProperty");
var context = new ClientModelValidationContext(metadata, metadataProvider);
var serviceCollection = new ServiceCollection();
var requestServices = serviceCollection.BuildServiceProvider();
var context = new ClientModelValidationContext(metadata, metadataProvider, requestServices);
var adapter = new CompareAttributeAdapter(attribute);
// Act
@ -57,7 +63,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{
ErrorMessage = "Hello '{0}', goodbye '{1}'."
};
var context = new ClientModelValidationContext(metadata, metadataProvider);
var serviceCollection = new ServiceCollection();
var requestServices = serviceCollection.BuildServiceProvider();
var context = new ClientModelValidationContext(metadata, metadataProvider, requestServices);
var adapter = new CompareAttributeAdapter(attribute);
// Act
@ -85,7 +93,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
ErrorMessageResourceName = "CompareAttributeTestResource",
ErrorMessageResourceType = typeof(Test.Resources),
};
var context = new ClientModelValidationContext(metadata, metadataProvider);
var serviceCollection = new ServiceCollection();
var requestServices = serviceCollection.BuildServiceProvider();
var context = new ClientModelValidationContext(metadata, metadataProvider, requestServices);
var adapter = new CompareAttributeAdapter(attribute);
// Act

View File

@ -1,10 +1,15 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
#if ASPNET50
using System.Linq;
#endif
using Microsoft.Framework.DependencyInjection;
using Microsoft.Framework.DependencyInjection.Fallback;
#if ASPNET50
using Moq;
using Moq.Protected;
#endif
@ -196,6 +201,51 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
}
#endif
[Fact]
public void GetClientValidationRules_ReturnsEmptyRuleSet()
{
// Arrange
var attribute = new FileExtensionsAttribute();
var validator = new DataAnnotationsModelValidator<FileExtensionsAttribute>(attribute);
var metadata = _metadataProvider.GetMetadataForProperty(
modelAccessor: null,
containerType: typeof(string),
propertyName: nameof(string.Length));
var serviceCollection = new ServiceCollection();
var requestServices = serviceCollection.BuildServiceProvider();
var context = new ClientModelValidationContext(metadata, _metadataProvider, requestServices);
// Act
var results = validator.GetClientValidationRules(context);
// Assert
Assert.Empty(results);
}
[Fact]
public void GetClientValidationRules_WithIClientModelValidator_CallsAttribute()
{
// Arrange
var attribute = new TestableAttribute();
var validator = new DataAnnotationsModelValidator<TestableAttribute>(attribute);
var metadata = _metadataProvider.GetMetadataForProperty(
modelAccessor: null,
containerType: typeof(string),
propertyName: nameof(string.Length));
var serviceCollection = new ServiceCollection();
var requestServices = serviceCollection.BuildServiceProvider();
var context = new ClientModelValidationContext(metadata, _metadataProvider, requestServices);
// Act
var results = validator.GetClientValidationRules(context);
// Assert
var rule = Assert.Single(results);
Assert.Equal("an error", rule.ErrorMessage);
Assert.Empty(rule.ValidationParameters);
Assert.Equal("testable", rule.ValidationType);
}
[Fact]
public void IsRequiredTests()
{
@ -221,5 +271,13 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{
public string Name { get; set; }
}
private class TestableAttribute : ValidationAttribute, IClientModelValidator
{
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ClientModelValidationContext context)
{
return new[] { new ModelClientValidationRule(validationType: "testable", errorMessage: "an error") };
}
}
}
}

View File

@ -3,6 +3,8 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNet.Testing;
using Microsoft.Framework.DependencyInjection;
using Microsoft.Framework.DependencyInjection.Fallback;
using Xunit;
namespace Microsoft.AspNet.Mvc.ModelBinding
@ -18,7 +20,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var metadata = provider.GetMetadataForProperty(() => null, typeof(string), "Length");
var attribute = new MaxLengthAttribute(10);
var adapter = new MaxLengthAttributeAdapter(attribute);
var context = new ClientModelValidationContext(metadata, provider);
var serviceCollection = new ServiceCollection();
var requestServices = serviceCollection.BuildServiceProvider();
var context = new ClientModelValidationContext(metadata, provider, requestServices);
// Act
var rules = adapter.GetClientValidationRules(context);
@ -42,7 +46,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var metadata = provider.GetMetadataForProperty(() => null, typeof(string), propertyName);
var attribute = new MaxLengthAttribute(5) { ErrorMessage = message };
var adapter = new MaxLengthAttributeAdapter(attribute);
var context = new ClientModelValidationContext(metadata, provider);
var serviceCollection = new ServiceCollection();
var requestServices = serviceCollection.BuildServiceProvider();
var context = new ClientModelValidationContext(metadata, provider, requestServices);
// Act
var rules = adapter.GetClientValidationRules(context);

View File

@ -3,6 +3,8 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNet.Testing;
using Microsoft.Framework.DependencyInjection;
using Microsoft.Framework.DependencyInjection.Fallback;
using Xunit;
namespace Microsoft.AspNet.Mvc.ModelBinding
@ -18,7 +20,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var metadata = provider.GetMetadataForProperty(() => null, typeof(string), "Length");
var attribute = new MinLengthAttribute(6);
var adapter = new MinLengthAttributeAdapter(attribute);
var context = new ClientModelValidationContext(metadata, provider);
var serviceCollection = new ServiceCollection();
var requestServices = serviceCollection.BuildServiceProvider();
var context = new ClientModelValidationContext(metadata, provider, requestServices);
// Act
var rules = adapter.GetClientValidationRules(context);
@ -42,7 +46,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var metadata = provider.GetMetadataForProperty(() => null, typeof(string), propertyName);
var attribute = new MinLengthAttribute(2) { ErrorMessage = message };
var adapter = new MinLengthAttributeAdapter(attribute);
var context = new ClientModelValidationContext(metadata, provider);
var serviceCollection = new ServiceCollection();
var requestServices = serviceCollection.BuildServiceProvider();
var context = new ClientModelValidationContext(metadata, provider, requestServices);
// Act
var rules = adapter.GetClientValidationRules(context);

View File

@ -3,6 +3,8 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNet.Testing;
using Microsoft.Framework.DependencyInjection;
using Microsoft.Framework.DependencyInjection.Fallback;
using Xunit;
namespace Microsoft.AspNet.Mvc.ModelBinding
@ -18,7 +20,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var metadata = provider.GetMetadataForProperty(() => null, typeof(string), "Length");
var attribute = new RangeAttribute(typeof(decimal), "0", "100");
var adapter = new RangeAttributeAdapter(attribute);
var context = new ClientModelValidationContext(metadata, provider);
var serviceCollection = new ServiceCollection();
var requestServices = serviceCollection.BuildServiceProvider();
var context = new ClientModelValidationContext(metadata, provider, requestServices);
// Act
var rules = adapter.GetClientValidationRules(context);

View File

@ -3,6 +3,8 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNet.Testing;
using Microsoft.Framework.DependencyInjection;
using Microsoft.Framework.DependencyInjection.Fallback;
using Xunit;
namespace Microsoft.AspNet.Mvc.ModelBinding
@ -19,7 +21,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var metadata = provider.GetMetadataForProperty(() => null, typeof(string), "Length");
var attribute = new RequiredAttribute();
var adapter = new RequiredAttributeAdapter(attribute);
var context = new ClientModelValidationContext(metadata, provider);
var serviceCollection = new ServiceCollection();
var requestServices = serviceCollection.BuildServiceProvider();
var context = new ClientModelValidationContext(metadata, provider, requestServices);
// Act
var rules = adapter.GetClientValidationRules(context);

View File

@ -3,6 +3,8 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNet.Testing;
using Microsoft.Framework.DependencyInjection;
using Microsoft.Framework.DependencyInjection.Fallback;
using Xunit;
namespace Microsoft.AspNet.Mvc.ModelBinding
@ -18,7 +20,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var metadata = provider.GetMetadataForProperty(() => null, typeof(string), "Length");
var attribute = new StringLengthAttribute(8);
var adapter = new StringLengthAttributeAdapter(attribute);
var context = new ClientModelValidationContext(metadata, provider);
var serviceCollection = new ServiceCollection();
var requestServices = serviceCollection.BuildServiceProvider();
var context = new ClientModelValidationContext(metadata, provider, requestServices);
// Act
var rules = adapter.GetClientValidationRules(context);
@ -40,7 +44,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var metadata = provider.GetMetadataForProperty(() => null, typeof(string), "Length");
var attribute = new StringLengthAttribute(10) { MinimumLength = 3 };
var adapter = new StringLengthAttributeAdapter(attribute);
var context = new ClientModelValidationContext(metadata, provider);
var serviceCollection = new ServiceCollection();
var requestServices = serviceCollection.BuildServiceProvider();
var context = new ClientModelValidationContext(metadata, provider, requestServices);
// Act
var rules = adapter.GetClientValidationRules(context);