diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/PageRemoteAttribute.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/PageRemoteAttribute.cs
new file mode 100644
index 0000000000..9d8978c9be
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/PageRemoteAttribute.cs
@@ -0,0 +1,59 @@
+// 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;
+using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
+using Microsoft.AspNetCore.Mvc.Routing;
+using Microsoft.Extensions.DependencyInjection;
+using Resources = Microsoft.AspNetCore.Mvc.ViewFeatures.Resources;
+
+namespace Microsoft.AspNetCore.Mvc
+{
+ ///
+ /// A for razor page handler which configures Unobtrusive validation
+ /// to send an Ajax request to the web site. The invoked handler should return JSON indicating
+ /// whether the value is valid.
+ ///
+ /// Does no server-side validation of the final form submission.
+ [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
+ public class PageRemoteAttribute : RemoteAttributeBase
+ {
+ ///
+ /// The handler name used when generating the URL where client should send a validation request.
+ ///
+ ///
+ /// If not set the ambient value will be used when generating the URL.
+ ///
+ public string PageHandler { get; set; }
+
+ ///
+ /// The page name used when generating the URL where client should send a validation request.
+ ///
+ ///
+ /// If not set the ambient value will be used when generating the URL.
+ ///
+ public string PageName { get; set; }
+
+ ///
+ protected override string GetUrl(ClientModelValidationContext context)
+ {
+ if (context == null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ var services = context.ActionContext.HttpContext.RequestServices;
+ var factory = services.GetRequiredService();
+ var urlHelper = factory.GetUrlHelper(context.ActionContext);
+
+ var url = urlHelper.Page(PageName, PageHandler, RouteData);
+
+ if (url == null)
+ {
+ throw new InvalidOperationException(Resources.RemoteAttribute_NoUrlFound);
+ }
+
+ return url;
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/RemoteAttribute.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/RemoteAttribute.cs
index 1254a79ed6..574f11cf91 100644
--- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/RemoteAttribute.cs
+++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/RemoteAttribute.cs
@@ -2,34 +2,21 @@
// 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.Globalization;
-using System.Linq;
-using Microsoft.AspNetCore.Mvc.DataAnnotations;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.AspNetCore.Mvc.Routing;
-using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Localization;
-using Microsoft.Extensions.Options;
using Resources = Microsoft.AspNetCore.Mvc.ViewFeatures.Resources;
namespace Microsoft.AspNetCore.Mvc
{
///
- /// A which configures Unobtrusive validation to send an Ajax request to the
+ /// A for controllers 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.
///
/// Does no server-side validation of the final form submission.
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
- public class RemoteAttribute : ValidationAttribute, IClientModelValidator
+ public class RemoteAttribute : RemoteAttributeBase
{
- private string _additionalFields = string.Empty;
- private string[] _additionalFieldsSplit = Array.Empty();
- private bool _checkedForLocalizer;
- private IStringLocalizer _stringLocalizer;
-
///
/// Initializes a new instance of the class.
///
@@ -37,10 +24,7 @@ namespace Microsoft.AspNetCore.Mvc
/// Intended for subclasses that support URL generation with no route, action, or controller names.
///
protected RemoteAttribute()
- : base(errorMessageAccessor: () => Resources.RemoteAttribute_RemoteValidationFailed)
- {
- RouteData = new RouteValueDictionary();
- }
+ { }
///
/// Initializes a new instance of the class.
@@ -112,90 +96,14 @@ namespace Microsoft.AspNetCore.Mvc
{
RouteData["area"] = areaName;
}
-
- ///
- /// Gets or sets the HTTP method ("Get" or "Post") client should use when sending a validation
- /// request.
- ///
- public string HttpMethod { get; set; }
-
- ///
- /// Gets or sets the comma-separated names of fields the client should include in a validation request.
- ///
- public string AdditionalFields
- {
- get => _additionalFields;
- set
- {
- _additionalFields = value ?? string.Empty;
- _additionalFieldsSplit = SplitAndTrimPropertyNames(value)
- .Select(field => FormatPropertyForClientValidation(field))
- .ToArray();
- }
- }
-
- ///
- /// Gets the used when generating the URL where client should send a
- /// validation request.
- ///
- protected RouteValueDictionary RouteData { get; }
-
+
///
/// Gets or sets the route name used when generating the URL where client should send a validation request.
///
protected string RouteName { get; set; }
- ///
- /// Formats and for use in generated HTML.
- ///
- ///
- /// Name of the property associated with this instance.
- ///
- /// Comma-separated names of fields the client should include in a validation request.
- ///
- /// Excludes any whitespace from in the return value.
- /// Prefixes each field name in the return value with "*.".
- ///
- public string FormatAdditionalFieldsForClientValidation(string property)
- {
- if (string.IsNullOrEmpty(property))
- {
- throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(property));
- }
-
- var delimitedAdditionalFields = string.Join(",", _additionalFieldsSplit);
- if (!string.IsNullOrEmpty(delimitedAdditionalFields))
- {
- delimitedAdditionalFields = "," + delimitedAdditionalFields;
- }
-
- var formattedString = FormatPropertyForClientValidation(property) + delimitedAdditionalFields;
-
- return formattedString;
- }
-
- ///
- /// Formats for use in generated HTML.
- ///
- /// One field name the client should include in a validation request.
- /// Name of a field the client should include in a validation request.
- /// Returns with a "*." prefix.
- public static string FormatPropertyForClientValidation(string property)
- {
- if (string.IsNullOrEmpty(property))
- {
- throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(property));
- }
-
- return "*." + property;
- }
-
- ///
- /// Returns the URL where the client should send a validation request.
- ///
- /// The used to generate the URL.
- /// The URL where the client should send a validation request.
- protected virtual string GetUrl(ClientModelValidationContext context)
+ ///
+ protected override string GetUrl(ClientModelValidationContext context)
{
if (context == null)
{
@@ -219,101 +127,5 @@ namespace Microsoft.AspNetCore.Mvc
return url;
}
-
- ///
- public override string FormatErrorMessage(string name)
- {
- return string.Format(CultureInfo.CurrentCulture, ErrorMessageString, name);
- }
-
- ///
- ///
- /// Always returns true since this does no validation itself.
- /// Related validations occur only when the client sends a validation request.
- ///
- public override bool IsValid(object value)
- {
- return true;
- }
-
- public virtual void AddValidation(ClientModelValidationContext context)
- {
- if (context == null)
- {
- throw new ArgumentNullException(nameof(context));
- }
-
- MergeAttribute(context.Attributes, "data-val", "true");
-
- CheckForLocalizer(context);
- var errorMessage = GetErrorMessage(context.ModelMetadata.GetDisplayName());
- MergeAttribute(context.Attributes, "data-val-remote", errorMessage);
-
- MergeAttribute(context.Attributes, "data-val-remote-url", GetUrl(context));
-
- if (!string.IsNullOrEmpty(HttpMethod))
- {
- MergeAttribute(context.Attributes, "data-val-remote-type", HttpMethod);
- }
-
- var additionalFields = FormatAdditionalFieldsForClientValidation(context.ModelMetadata.PropertyName);
- MergeAttribute(context.Attributes, "data-val-remote-additionalfields", additionalFields);
- }
-
- private static void MergeAttribute(IDictionary attributes, string key, string value)
- {
- if (!attributes.ContainsKey(key))
- {
- attributes.Add(key, value);
- }
- }
-
- private static IEnumerable SplitAndTrimPropertyNames(string original)
- {
- if (string.IsNullOrEmpty(original))
- {
- return Array.Empty();
- }
-
- var split = original
- .Split(',')
- .Select(piece => piece.Trim())
- .Where(trimmed => !string.IsNullOrEmpty(trimmed));
-
- return split;
- }
-
- private void CheckForLocalizer(ClientModelValidationContext context)
- {
- if (!_checkedForLocalizer)
- {
- _checkedForLocalizer = true;
-
- var services = context.ActionContext.HttpContext.RequestServices;
- var options = services.GetRequiredService>();
- var factory = services.GetService();
-
- var provider = options.Value.DataAnnotationLocalizerProvider;
- if (factory != null && provider != null)
- {
- _stringLocalizer = provider(
- context.ModelMetadata.ContainerType ?? context.ModelMetadata.ModelType,
- factory);
- }
- }
- }
-
- private string GetErrorMessage(string displayName)
- {
- if (_stringLocalizer != null &&
- !string.IsNullOrEmpty(ErrorMessage) &&
- string.IsNullOrEmpty(ErrorMessageResourceName) &&
- ErrorMessageResourceType == null)
- {
- return _stringLocalizer[ErrorMessage, displayName];
- }
-
- return FormatErrorMessage(displayName);
- }
}
}
diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/RemoteAttributeBase.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/RemoteAttributeBase.cs
new file mode 100644
index 0000000000..46b06b54a0
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/RemoteAttributeBase.cs
@@ -0,0 +1,224 @@
+// 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;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Globalization;
+using System.Linq;
+using Microsoft.AspNetCore.Mvc.DataAnnotations;
+using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
+using Microsoft.AspNetCore.Routing;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Localization;
+using Microsoft.Extensions.Options;
+using Resources = Microsoft.AspNetCore.Mvc.ViewFeatures.Resources;
+
+
+namespace Microsoft.AspNetCore.Mvc
+{
+ ///
+ /// A which configures Unobtrusive validation to send an Ajax request to the
+ /// web site. The invoked endpoint should return JSON indicating whether the value is valid.
+ ///
+ /// Does no server-side validation of the final form submission.
+ [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
+ public abstract class RemoteAttributeBase : ValidationAttribute, IClientModelValidator
+ {
+ private string _additionalFields = string.Empty;
+ private string[] _additionalFieldsSplit = Array.Empty();
+ private bool _checkedForLocalizer;
+ private IStringLocalizer _stringLocalizer;
+
+ protected RemoteAttributeBase()
+ : base(errorMessageAccessor: () => Resources.RemoteAttribute_RemoteValidationFailed)
+ {
+ RouteData = new RouteValueDictionary();
+ }
+
+ ///
+ /// Gets the used when generating the URL where client should send a
+ /// validation request.
+ ///
+ protected RouteValueDictionary RouteData { get; }
+
+ ///
+ /// Gets or sets the HTTP method ("Get" or "Post") client should use when sending a validation
+ /// request.
+ ///
+ public string HttpMethod { get; set; }
+
+ ///
+ /// Gets or sets the comma-separated names of fields the client should include in a validation request.
+ ///
+ public string AdditionalFields
+ {
+ get => _additionalFields;
+ set
+ {
+ _additionalFields = value ?? string.Empty;
+ _additionalFieldsSplit = SplitAndTrimPropertyNames(value)
+ .Select(field => FormatPropertyForClientValidation(field))
+ .ToArray();
+ }
+ }
+
+ ///
+ /// Formats and for use in generated HTML.
+ ///
+ ///
+ /// Name of the property associated with this instance.
+ ///
+ /// Comma-separated names of fields the client should include in a validation request.
+ ///
+ /// Excludes any whitespace from in the return value.
+ /// Prefixes each field name in the return value with "*.".
+ ///
+ public string FormatAdditionalFieldsForClientValidation(string property)
+ {
+ if (string.IsNullOrEmpty(property))
+ {
+ throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(property));
+ }
+
+ var delimitedAdditionalFields = string.Join(",", _additionalFieldsSplit);
+ if (!string.IsNullOrEmpty(delimitedAdditionalFields))
+ {
+ delimitedAdditionalFields = "," + delimitedAdditionalFields;
+ }
+
+ var formattedString = FormatPropertyForClientValidation(property) + delimitedAdditionalFields;
+
+ return formattedString;
+ }
+
+ ///
+ /// Formats for use in generated HTML.
+ ///
+ /// One field name the client should include in a validation request.
+ /// Name of a field the client should include in a validation request.
+ /// Returns with a "*." prefix.
+ public static string FormatPropertyForClientValidation(string property)
+ {
+ if (string.IsNullOrEmpty(property))
+ {
+ throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(property));
+ }
+
+ return "*." + property;
+ }
+
+ ///
+ /// Returns the URL where the client should send a validation request.
+ ///
+ /// The used to generate the URL.
+ /// The URL where the client should send a validation request.
+ protected abstract string GetUrl(ClientModelValidationContext context);
+
+ ///
+ public override string FormatErrorMessage(string name)
+ {
+ return string.Format(CultureInfo.CurrentCulture, ErrorMessageString, name);
+ }
+
+ ///
+ ///
+ /// Always returns true since this does no validation itself.
+ /// Related validations occur only when the client sends a validation request.
+ ///
+ public override bool IsValid(object value)
+ {
+ return true;
+ }
+
+ ///
+ /// Adds Unobtrusive validation HTML attributes to .
+ ///
+ ///
+ /// to add Unobtrusive validation HTML attributes to.
+ ///
+ ///
+ /// Calls derived implementation of .
+ ///
+ public virtual void AddValidation(ClientModelValidationContext context)
+ {
+ if (context == null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ MergeAttribute(context.Attributes, "data-val", "true");
+
+ CheckForLocalizer(context);
+ var errorMessage = GetErrorMessage(context.ModelMetadata.GetDisplayName());
+ MergeAttribute(context.Attributes, "data-val-remote", errorMessage);
+
+ MergeAttribute(context.Attributes, "data-val-remote-url", GetUrl(context));
+
+ if (!string.IsNullOrEmpty(HttpMethod))
+ {
+ MergeAttribute(context.Attributes, "data-val-remote-type", HttpMethod);
+ }
+
+ var additionalFields = FormatAdditionalFieldsForClientValidation(context.ModelMetadata.PropertyName);
+ MergeAttribute(context.Attributes, "data-val-remote-additionalfields", additionalFields);
+ }
+
+ private static void MergeAttribute(IDictionary attributes, string key, string value)
+ {
+ if (!attributes.ContainsKey(key))
+ {
+ attributes.Add(key, value);
+ }
+ }
+
+ private static IEnumerable SplitAndTrimPropertyNames(string original)
+ {
+ if (string.IsNullOrEmpty(original))
+ {
+ return Array.Empty();
+ }
+
+ var split = original
+ .Split(',')
+ .Select(piece => piece.Trim())
+ .Where(trimmed => !string.IsNullOrEmpty(trimmed));
+
+ return split;
+ }
+
+ private void CheckForLocalizer(ClientModelValidationContext context)
+ {
+ if (!_checkedForLocalizer)
+ {
+ _checkedForLocalizer = true;
+
+ var services = context.ActionContext.HttpContext.RequestServices;
+ var options = services.GetRequiredService>();
+ var factory = services.GetService();
+
+ var provider = options.Value.DataAnnotationLocalizerProvider;
+ if (factory != null && provider != null)
+ {
+ _stringLocalizer = provider(
+ context.ModelMetadata.ContainerType ?? context.ModelMetadata.ModelType,
+ factory);
+ }
+ }
+ }
+
+ private string GetErrorMessage(string displayName)
+ {
+ if (_stringLocalizer != null &&
+ !string.IsNullOrEmpty(ErrorMessage) &&
+ string.IsNullOrEmpty(ErrorMessageResourceName) &&
+ ErrorMessageResourceType == null)
+ {
+ return _stringLocalizer[ErrorMessage, displayName];
+ }
+
+ return FormatErrorMessage(displayName);
+ }
+ }
+}
+
diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/breakingchanges.netcore.json b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/breakingchanges.netcore.json
index 2535632381..638cfe4f77 100644
--- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/breakingchanges.netcore.json
+++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/breakingchanges.netcore.json
@@ -42,5 +42,9 @@
"TypeId": "public class Microsoft.AspNetCore.Mvc.ViewFeatures.HtmlHelper : Microsoft.AspNetCore.Mvc.ViewFeatures.HtmlHelper, Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper",
"MemberId": "public .ctor(Microsoft.AspNetCore.Mvc.ViewFeatures.IHtmlGenerator htmlGenerator, Microsoft.AspNetCore.Mvc.ViewEngines.ICompositeViewEngine viewEngine, Microsoft.AspNetCore.Mvc.ModelBinding.IModelMetadataProvider metadataProvider, Microsoft.AspNetCore.Mvc.ViewFeatures.Internal.IViewBufferScope bufferScope, System.Text.Encodings.Web.HtmlEncoder htmlEncoder, System.Text.Encodings.Web.UrlEncoder urlEncoder, Microsoft.AspNetCore.Mvc.ViewFeatures.Internal.ExpressionTextCache expressionTextCache)",
"Kind": "Removal"
+ },
+ {
+ "TypeId": "public class Microsoft.AspNetCore.Mvc.RemoteAttribute : System.ComponentModel.DataAnnotations.ValidationAttribute, Microsoft.AspNetCore.Mvc.ModelBinding.Validation.IClientModelValidator",
+ "Kind": "Removal"
}
]
\ No newline at end of file
diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/PageRemoteAttributeTest.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/PageRemoteAttributeTest.cs
new file mode 100644
index 0000000000..1cf02d3db5
--- /dev/null
+++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/PageRemoteAttributeTest.cs
@@ -0,0 +1,271 @@
+// 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;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc.Abstractions;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
+using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
+using Microsoft.AspNetCore.Mvc.Routing;
+using Microsoft.AspNetCore.Mvc.ViewFeatures;
+using Microsoft.AspNetCore.Routing;
+using Microsoft.AspNetCore.Testing;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.ObjectPool;
+using Microsoft.Extensions.Options;
+using Moq;
+using Xunit;
+using Resources = Microsoft.AspNetCore.Mvc.ViewFeatures.Resources;
+
+namespace Microsoft.AspNetCore.Mvc
+{
+ public class PageRemoteAttributeTest
+ {
+ [Fact]
+ public void GetUrl_CallsUrlHelperWithExpectedValues()
+ {
+ // Arrange
+ var testableAttribute = new TestablePageRemoteAttribute
+ {
+ PageName = "Foo",
+ PageHandler = "Bar"
+ };
+
+ var ambientValues = new RouteValueDictionary()
+ {
+ ["page"] = "/Foo"
+ };
+
+ var routeData = new RouteData(ambientValues)
+ {
+ Routers = { Mock.Of() }
+ };
+
+ var urlHelper = new MockUrlHelper(url: "/Foo?handler=Bar")
+ {
+ ActionContext = GetActionContext(new ServiceCollection().BuildServiceProvider(), routeData)
+ };
+
+ var validationContext = GetValidationContext(urlHelper);
+
+ // Act
+ testableAttribute.InvokeGetUrl(validationContext);
+
+ // Assert
+ var routeDictionary = Assert.IsType(urlHelper.RouteValues);
+
+ Assert.Equal(2, routeDictionary.Count);
+ Assert.Equal("/Foo", routeDictionary["page"] as string);
+ Assert.Equal("Bar", routeDictionary["handler"] as string);
+ }
+
+ [Fact]
+ public void GetUrl_WhenUrlHelperReturnsNull_Throws()
+ {
+ // Arrange
+ var testableAttribute = new TestablePageRemoteAttribute
+ {
+ PageName = "Foo",
+ PageHandler = "Bar"
+ };
+
+ var ambientValues = new RouteValueDictionary
+ {
+ ["page"] = "/Page"
+ };
+
+ var routeData = new RouteData(ambientValues)
+ {
+ Routers = { Mock.Of() }
+ };
+
+ var urlHelper = new MockUrlHelper(url: null)
+ {
+ ActionContext = GetActionContext(new ServiceCollection().BuildServiceProvider(), routeData)
+ };
+
+ var validationContext = GetValidationContext(urlHelper);
+
+ // Act && Assert
+ ExceptionAssert.Throws(
+ () => testableAttribute.InvokeGetUrl(validationContext),
+ Resources.RemoteAttribute_NoUrlFound);
+ }
+
+ [Fact]
+ public void GetUrl_WhenPageNameIsNotSet_WillUsePageNameFromAmbientValues()
+ {
+ // Arrange
+ var testableAttribute = new TestablePageRemoteAttribute()
+ {
+ PageHandler = "Handler"
+ };
+
+ var ambientValues = new RouteValueDictionary
+ {
+ ["page"] = "/Page"
+ };
+
+ var routeData = new RouteData(ambientValues)
+ {
+ Routers = { Mock.Of() }
+ };
+
+ var urlHelper = new MockUrlHelper(url: "/Page?handler=Handler")
+ {
+ ActionContext = GetActionContext(new ServiceCollection().BuildServiceProvider(), routeData)
+ };
+
+ var validationContext = GetValidationContext(urlHelper);
+
+ // Act
+ var actualUrl = testableAttribute.InvokeGetUrl(validationContext);
+
+ // Assert
+ Assert.Equal("/Page?handler=Handler", actualUrl);
+ }
+
+ [Fact]
+ public void GetUrl_WhenPageNameAndPageHandlerIsNotSet_WillUseAmbientValues()
+ {
+ // Arrange
+ var testableAttribute = new TestablePageRemoteAttribute();
+
+ var ambientValues = new RouteValueDictionary
+ {
+ ["page"] = "/Page",
+ ["handler"] = "Handler"
+ };
+
+ var routeData = new RouteData(ambientValues)
+ {
+ Routers = { Mock.Of() }
+ };
+
+ var urlHelper = new MockUrlHelper(url: "/Page?handler=Handler")
+ {
+ ActionContext = GetActionContext(new ServiceCollection().BuildServiceProvider(), routeData)
+ };
+
+ var validationContext = GetValidationContext(urlHelper);
+
+ // Act
+ var actualUrl = testableAttribute.InvokeGetUrl(validationContext);
+
+ // Assert
+ Assert.Equal("/Page?handler=Handler", actualUrl);
+ }
+
+ private static ClientModelValidationContext GetValidationContext(IUrlHelper urlHelper, RouteData routeData = null)
+ {
+ var serviceCollection = GetServiceCollection();
+ var factory = new Mock(MockBehavior.Strict);
+ serviceCollection.AddSingleton(factory.Object);
+
+ var serviceProvider = serviceCollection.BuildServiceProvider();
+ var actionContext = GetActionContext(serviceProvider, routeData);
+
+ factory
+ .Setup(f => f.GetUrlHelper(actionContext))
+ .Returns(urlHelper);
+
+ var metadataProvider = new EmptyModelMetadataProvider();
+ var metadata = metadataProvider.GetMetadataForProperty(
+ containerType: typeof(string),
+ propertyName: nameof(string.Length));
+
+ return new ClientModelValidationContext(
+ actionContext,
+ metadata,
+ metadataProvider,
+ new AttributeDictionary());
+ }
+
+ private static ServiceCollection GetServiceCollection()
+ {
+ var serviceCollection = new ServiceCollection();
+ serviceCollection
+ .AddSingleton()
+ .AddSingleton(new NullLoggerFactory());
+
+ serviceCollection.AddOptions();
+ serviceCollection.AddRouting();
+
+ serviceCollection.AddSingleton(
+ provider => new DefaultInlineConstraintResolver(provider.GetRequiredService>(), provider));
+
+ return serviceCollection;
+ }
+
+ private static ActionContext GetActionContext(IServiceProvider serviceProvider, RouteData routeData)
+ {
+ // Set IServiceProvider properties because TemplateRoute gets services (e.g. an ILoggerFactory instance)
+ // through the HttpContext.
+ var httpContext = new DefaultHttpContext
+ {
+ RequestServices = serviceProvider,
+ };
+
+ if (routeData == null)
+ {
+ routeData = new RouteData
+ {
+ Routers = { Mock.Of(), },
+ };
+ }
+
+ return new ActionContext(httpContext, routeData, new ActionDescriptor());
+ }
+
+ private class TestablePageRemoteAttribute : PageRemoteAttribute
+ {
+ public string InvokeGetUrl(ClientModelValidationContext context)
+ {
+ return base.GetUrl(context);
+ }
+ }
+
+ private class MockUrlHelper : IUrlHelper
+ {
+ private readonly string _url;
+
+ public MockUrlHelper(string url)
+ {
+ _url = url;
+ }
+
+ public ActionContext ActionContext { get; set; }
+
+ public object RouteValues { get; private set; }
+
+ public string Action(UrlActionContext actionContext)
+ {
+ throw new NotImplementedException();
+ }
+
+ public string Content(string contentPath)
+ {
+ throw new NotImplementedException();
+ }
+
+ public bool IsLocalUrl(string url)
+ {
+ throw new NotImplementedException();
+ }
+
+ public string Link(string routeName, object values)
+ {
+ throw new NotImplementedException();
+ }
+
+ public string RouteUrl(UrlRouteContext routeContext)
+ {
+ RouteValues = routeContext.Values;
+
+ return _url;
+ }
+ }
+ }
+}
diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/RemoteAttributeBaseTest.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/RemoteAttributeBaseTest.cs
new file mode 100644
index 0000000000..70dd3690f8
--- /dev/null
+++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/RemoteAttributeBaseTest.cs
@@ -0,0 +1,539 @@
+// 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 Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc.Abstractions;
+using Microsoft.AspNetCore.Mvc.DataAnnotations;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
+using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
+using Microsoft.AspNetCore.Mvc.ViewFeatures;
+using Microsoft.AspNetCore.Routing;
+using Microsoft.AspNetCore.Testing;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Localization;
+using Microsoft.Extensions.Options;
+using Moq;
+using Xunit;
+using Resources = Microsoft.AspNetCore.Mvc.ViewFeatures.Test.Resources;
+
+namespace Microsoft.AspNetCore.Mvc
+{
+ public class RemoteAttributeBaseTest
+ {
+ // Null or empty property names are invalid. (Those containing just whitespace are legal.)
+ public static TheoryData NullOrEmptyNames
+ {
+ get
+ {
+ return new TheoryData
+ {
+ null,
+ string.Empty,
+ };
+ }
+ }
+
+ [Fact]
+ public void IsValidAlwaysReturnsTrue()
+ {
+ // Arrange
+ var attribute = new TestableRemoteAttributeBase();
+
+ // Act & Assert
+ Assert.True(attribute.IsValid(value: null));
+ }
+
+ [Fact]
+ public void ErrorMessageProperties_HaveExpectedDefaultValues()
+ {
+ // Arrange & Act
+ var attribute = new TestableRemoteAttributeBase();
+
+ // Assert
+ Assert.Null(attribute.ErrorMessage);
+ Assert.Null(attribute.ErrorMessageResourceName);
+ Assert.Null(attribute.ErrorMessageResourceType);
+ }
+
+ [Fact]
+ [ReplaceCulture]
+ public void FormatErrorMessage_ReturnsDefaultErrorMessage()
+ {
+ // Arrange
+ // See ViewFeatures.Resources.RemoteAttribute_RemoteValidationFailed.
+ var expected = "'Property1' is invalid.";
+ var attribute = new TestableRemoteAttributeBase();
+
+ // Act
+ var message = attribute.FormatErrorMessage("Property1");
+
+ // Assert
+ Assert.Equal(expected, message);
+ }
+
+ [Fact]
+ public void FormatErrorMessage_UsesOverriddenErrorMessage()
+ {
+ // Arrange
+ var expected = "Error about 'Property1' from override.";
+ var attribute = new TestableRemoteAttributeBase()
+ {
+ ErrorMessage = "Error about '{0}' from override.",
+ };
+
+ // Act
+ var message = attribute.FormatErrorMessage("Property1");
+
+ // Assert
+ Assert.Equal(expected, message);
+ }
+
+ [Fact]
+ [ReplaceCulture]
+ public void FormatErrorMessage_UsesErrorMessageFromResource()
+ {
+ // Arrange
+ var expected = "Error about 'Property1' from resources.";
+ var attribute = new TestableRemoteAttributeBase()
+ {
+ ErrorMessageResourceName = nameof(Resources.RemoteAttribute_Error),
+ ErrorMessageResourceType = typeof(Resources)
+ };
+
+ // Act
+ var message = attribute.FormatErrorMessage("Property1");
+
+ // Assert
+ Assert.Equal(expected, message);
+ }
+
+ [Theory]
+ [MemberData(nameof(NullOrEmptyNames))]
+ public void FormatAdditionalFieldsForClientValidation_WithInvalidPropertyName_Throws(string property)
+ {
+ // Arrange
+ var attribute = new TestableRemoteAttributeBase();
+ var expectedMessage = "Value cannot be null or empty.";
+
+ // Act & Assert
+ ExceptionAssert.ThrowsArgument(
+ () => attribute.FormatAdditionalFieldsForClientValidation(property),
+ "property",
+ expectedMessage);
+ }
+
+ [Fact]
+ public void FormatAdditionalFieldsForClientValidation_WillFormat_AdditionalFields()
+ {
+ // Arrange
+ var attribute = new TestableRemoteAttributeBase
+ {
+ AdditionalFields = "FieldOne, FieldTwo"
+ };
+
+ // Act
+ var actual = attribute.FormatAdditionalFieldsForClientValidation("Property");
+
+ // Assert
+ var expected = "*.Property,*.FieldOne,*.FieldTwo";
+ Assert.Equal(expected, actual);
+ }
+
+ [Theory]
+ [MemberData(nameof(NullOrEmptyNames))]
+ public void FormatPropertyForClientValidation_WithInvalidPropertyName_Throws(string property)
+ {
+ // Arrange
+ var expected = "Value cannot be null or empty.";
+
+ // Act & Assert
+ ExceptionAssert.ThrowsArgument(
+ () => RemoteAttributeBase.FormatPropertyForClientValidation(property),
+ "property",
+ expected);
+ }
+
+ [Fact]
+ public void AddValidation_WithErrorMessage_SetsAttributesAsExpected()
+ {
+ // Arrange
+ var expected = "Error about 'Length' from override.";
+ var url = "/Controller/Action";
+ var context = GetValidationContext();
+ var attribute = new TestableRemoteAttributeBase(dummyGetUrlReturnValue: url)
+ {
+ HttpMethod = "POST",
+ ErrorMessage = "Error about '{0}' from override.",
+ };
+
+ // Act
+ attribute.AddValidation(context);
+
+ // Assert
+ Assert.Collection(
+ context.Attributes,
+ kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); },
+ kvp =>
+ {
+ Assert.Equal("data-val-remote", kvp.Key);
+ Assert.Equal(expected, kvp.Value);
+ },
+ kvp =>
+ {
+ Assert.Equal("data-val-remote-additionalfields", kvp.Key);
+ Assert.Equal("*.Length", kvp.Value);
+ },
+ kvp => { Assert.Equal("data-val-remote-type", kvp.Key); Assert.Equal("POST", kvp.Value); },
+ kvp => { Assert.Equal("data-val-remote-url", kvp.Key); Assert.Equal(url, kvp.Value); });
+ }
+
+ [Fact]
+ public void AddValidation_WithErrorMessageAndLocalizerFactory_SetsAttributesAsExpected()
+ {
+ // Arrange
+ var expected = "Error about 'Length' from override.";
+ var url = "/Controller/Action";
+ var localizerFactory = new Mock(MockBehavior.Strict).Object;
+ var context = GetValidationContext(localizerFactory);
+ var attribute = new TestableRemoteAttributeBase(dummyGetUrlReturnValue: url)
+ {
+ HttpMethod = "POST",
+ ErrorMessage = "Error about '{0}' from override.",
+ };
+
+ // Act
+ attribute.AddValidation(context);
+
+ // Assert
+ Assert.Collection(
+ context.Attributes,
+ kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); },
+ kvp =>
+ {
+ // IStringLocalizerFactory existence alone is insufficient to change error message.
+ Assert.Equal("data-val-remote", kvp.Key);
+ Assert.Equal(expected, kvp.Value);
+ },
+ kvp =>
+ {
+ Assert.Equal("data-val-remote-additionalfields", kvp.Key);
+ Assert.Equal("*.Length", kvp.Value);
+ },
+ kvp => { Assert.Equal("data-val-remote-type", kvp.Key); Assert.Equal("POST", kvp.Value); },
+ kvp => { Assert.Equal("data-val-remote-url", kvp.Key); Assert.Equal(url, kvp.Value); });
+ }
+
+ [Fact]
+ public void AddValidation_WithErrorMessageAndLocalizerProvider_SetsAttributesAsExpected()
+ {
+ // Arrange
+ var expected = "Error about 'Length' from override.";
+ var url = "/Controller/Action";
+ var context = GetValidationContext();
+ var attribute = new TestableRemoteAttributeBase(url)
+ {
+ HttpMethod = "POST",
+ ErrorMessage = "Error about '{0}' from override.",
+ };
+
+ var options = context.ActionContext.HttpContext.RequestServices
+ .GetRequiredService>();
+ var localizer = new Mock(MockBehavior.Strict);
+ options.Value.DataAnnotationLocalizerProvider = (type, factory) => localizer.Object;
+
+ // Act
+ attribute.AddValidation(context);
+
+ // Assert
+ Assert.Collection(
+ context.Attributes,
+ kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); },
+ kvp =>
+ {
+ // Non-null DataAnnotationLocalizerProvider alone is insufficient to change error message.
+ Assert.Equal("data-val-remote", kvp.Key);
+ Assert.Equal(expected, kvp.Value);
+ },
+ kvp =>
+ {
+ Assert.Equal("data-val-remote-additionalfields", kvp.Key);
+ Assert.Equal("*.Length", kvp.Value);
+ },
+ kvp => { Assert.Equal("data-val-remote-type", kvp.Key); Assert.Equal("POST", kvp.Value); },
+ kvp => { Assert.Equal("data-val-remote-url", kvp.Key); Assert.Equal(url, kvp.Value); });
+ }
+
+ [Fact]
+ public void AddValidation_WithErrorMessageLocalizerFactoryAndLocalizerProvider_SetsAttributesAsExpected()
+ {
+ // Arrange
+ var expected = "Error about 'Length' from localizer.";
+ var url = "/Controller/Action";
+ var localizerFactory = new Mock(MockBehavior.Strict).Object;
+ var context = GetValidationContext(localizerFactory);
+ var attribute = new TestableRemoteAttributeBase(dummyGetUrlReturnValue: url)
+ {
+ HttpMethod = "POST",
+ ErrorMessage = "Error about '{0}' from override.",
+ };
+
+ var localizedString = new LocalizedString("Fred", expected);
+ var localizer = new Mock(MockBehavior.Strict);
+ localizer
+ .Setup(l => l["Error about '{0}' from override.", "Length"])
+ .Returns(localizedString)
+ .Verifiable();
+ var options = context.ActionContext.HttpContext.RequestServices
+ .GetRequiredService>();
+ options.Value.DataAnnotationLocalizerProvider = (type, factory) => localizer.Object;
+
+ // Act
+ attribute.AddValidation(context);
+
+ // Assert
+ localizer.VerifyAll();
+
+ Assert.Collection(
+ context.Attributes,
+ kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); },
+ kvp =>
+ {
+ Assert.Equal("data-val-remote", kvp.Key);
+ Assert.Equal(expected, kvp.Value);
+ },
+ kvp =>
+ {
+ Assert.Equal("data-val-remote-additionalfields", kvp.Key);
+ Assert.Equal("*.Length", kvp.Value);
+ },
+ kvp => { Assert.Equal("data-val-remote-type", kvp.Key); Assert.Equal("POST", kvp.Value); },
+ kvp => { Assert.Equal("data-val-remote-url", kvp.Key); Assert.Equal(url, kvp.Value); });
+ }
+
+ [Fact]
+ [ReplaceCulture]
+ public void AddValidation_WithErrorResourcesLocalizerFactoryAndLocalizerProvider_SetsAttributesAsExpected()
+ {
+ // Arrange
+ var expected = "Error about 'Length' from resources.";
+ var url = "/Controller/Action";
+ var attribute = new TestableRemoteAttributeBase(dummyGetUrlReturnValue: url)
+ {
+ HttpMethod = "POST",
+ ErrorMessageResourceName = nameof(Resources.RemoteAttribute_Error),
+ ErrorMessageResourceType = typeof(Resources),
+ };
+
+ var localizerFactory = new Mock(MockBehavior.Strict).Object;
+ var context = GetValidationContext(localizerFactory);
+
+ var localizer = new Mock(MockBehavior.Strict);
+ var options = context.ActionContext.HttpContext.RequestServices
+ .GetRequiredService>();
+ options.Value.DataAnnotationLocalizerProvider = (type, factory) => localizer.Object;
+
+ // Act
+ attribute.AddValidation(context);
+
+ // Assert
+ Assert.Collection(
+ context.Attributes,
+ kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); },
+ kvp =>
+ {
+ // Configuring the attribute using ErrorMessageResource* trumps available IStringLocalizer etc.
+ Assert.Equal("data-val-remote", kvp.Key);
+ Assert.Equal(expected, kvp.Value);
+ },
+ kvp =>
+ {
+ Assert.Equal("data-val-remote-additionalfields", kvp.Key);
+ Assert.Equal("*.Length", kvp.Value);
+ },
+ kvp => { Assert.Equal("data-val-remote-type", kvp.Key); Assert.Equal("POST", kvp.Value); },
+ kvp => { Assert.Equal("data-val-remote-url", kvp.Key); Assert.Equal(url, kvp.Value); });
+ }
+
+ [Fact]
+ public void AddValidation_WithErrorMessageAndDisplayName_SetsAttributesAsExpected()
+ {
+ // Arrange
+ var expected = "Error about 'Display Length' from override.";
+ var url = "/Controller/Action";
+
+ var metadataProvider = new TestModelMetadataProvider();
+ metadataProvider
+ .ForProperty(typeof(string), nameof(string.Length))
+ .DisplayDetails(d => d.DisplayName = () => "Display Length");
+ var context = GetValidationContext(localizerFactory: null, metadataProvider: metadataProvider);
+
+ var attribute = new TestableRemoteAttributeBase(dummyGetUrlReturnValue: url)
+ {
+ HttpMethod = "POST",
+ ErrorMessage = "Error about '{0}' from override.",
+ };
+
+ // Act
+ attribute.AddValidation(context);
+
+ // Assert
+ Assert.Collection(
+ context.Attributes,
+ kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); },
+ kvp =>
+ {
+ Assert.Equal("data-val-remote", kvp.Key);
+ Assert.Equal(expected, kvp.Value);
+ },
+ kvp =>
+ {
+ Assert.Equal("data-val-remote-additionalfields", kvp.Key);
+ Assert.Equal("*.Length", kvp.Value);
+ },
+ kvp => { Assert.Equal("data-val-remote-type", kvp.Key); Assert.Equal("POST", kvp.Value); },
+ kvp => { Assert.Equal("data-val-remote-url", kvp.Key); Assert.Equal(url, kvp.Value); });
+ }
+
+ [Fact]
+ public void AddValidation_WithErrorMessageLocalizerFactoryLocalizerProviderAndDisplayName_SetsAttributesAsExpected()
+ {
+ // Arrange
+ var expected = "Error about 'Length' from localizer.";
+ var url = "/Controller/Action";
+
+ var metadataProvider = new TestModelMetadataProvider();
+ metadataProvider
+ .ForProperty(typeof(string), nameof(string.Length))
+ .DisplayDetails(d => d.DisplayName = () => "Display Length");
+ var localizerFactory = new Mock(MockBehavior.Strict).Object;
+ var context = GetValidationContext(localizerFactory, metadataProvider);
+
+ var attribute = new TestableRemoteAttributeBase(dummyGetUrlReturnValue: url)
+ {
+ HttpMethod = "POST",
+ ErrorMessage = "Error about '{0}' from override.",
+ };
+
+ var localizedString = new LocalizedString("Fred", expected);
+ var localizer = new Mock(MockBehavior.Strict);
+ localizer
+ .Setup(l => l["Error about '{0}' from override.", "Display Length"])
+ .Returns(localizedString)
+ .Verifiable();
+ var options = context.ActionContext.HttpContext.RequestServices
+ .GetRequiredService>();
+ options.Value.DataAnnotationLocalizerProvider = (type, factory) => localizer.Object;
+
+ // Act
+ attribute.AddValidation(context);
+
+ // Assert
+ localizer.VerifyAll();
+
+ Assert.Collection(
+ context.Attributes,
+ kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); },
+ kvp =>
+ {
+ Assert.Equal("data-val-remote", kvp.Key);
+ Assert.Equal(expected, kvp.Value);
+ },
+ kvp =>
+ {
+ Assert.Equal("data-val-remote-additionalfields", kvp.Key);
+ Assert.Equal("*.Length", kvp.Value);
+ },
+ kvp => { Assert.Equal("data-val-remote-type", kvp.Key); Assert.Equal("POST", kvp.Value); },
+ kvp => { Assert.Equal("data-val-remote-url", kvp.Key); Assert.Equal(url, kvp.Value); });
+ }
+
+ [Fact]
+ public void AddValidation_WillSetAttributes_ToExpectedValues()
+ {
+ // Arrange
+ var url = "/Controller/Action";
+ var attribute = new TestableRemoteAttributeBase(dummyGetUrlReturnValue: url)
+ {
+ HttpMethod = "POST",
+ AdditionalFields = "Password,ConfirmPassword",
+ ErrorMessage = "Error"
+ };
+ var context = GetValidationContext();
+
+ // Act
+ attribute.AddValidation(context);
+
+ // Assert
+ Assert.Collection(
+ context.Attributes,
+ kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); },
+ kvp => { Assert.Equal("data-val-remote", kvp.Key); Assert.Equal("Error", kvp.Value); },
+ kvp =>
+ {
+ Assert.Equal("data-val-remote-additionalfields", kvp.Key);
+ Assert.Equal("*.Length,*.Password,*.ConfirmPassword", kvp.Value);
+ },
+ kvp => { Assert.Equal("data-val-remote-type", kvp.Key); Assert.Equal("POST", kvp.Value); },
+ kvp => { Assert.Equal("data-val-remote-url", kvp.Key); Assert.Equal(url, kvp.Value); });
+ }
+
+ private static ClientModelValidationContext GetValidationContext(
+ IStringLocalizerFactory localizerFactory = null,
+ IModelMetadataProvider metadataProvider = null)
+ {
+ var serviceCollection = new ServiceCollection();
+ serviceCollection.AddOptions();
+ if (localizerFactory != null)
+ {
+ serviceCollection.AddSingleton(localizerFactory);
+ }
+
+ var serviceProvider = serviceCollection.BuildServiceProvider();
+
+ var httpContext = new DefaultHttpContext
+ {
+ RequestServices = serviceProvider,
+ };
+
+ var actionContext = new ActionContext(
+ httpContext,
+ routeData: new Mock().Object,
+ actionDescriptor: new ActionDescriptor());
+
+ var emptyMetadataProvider = new EmptyModelMetadataProvider();
+
+ if (metadataProvider == null)
+ {
+ metadataProvider = new EmptyModelMetadataProvider();
+ }
+
+ var metadata = metadataProvider.GetMetadataForProperty(
+ containerType: typeof(string),
+ propertyName: nameof(string.Length));
+
+ return new ClientModelValidationContext(
+ actionContext,
+ metadata,
+ metadataProvider,
+ new AttributeDictionary());
+ }
+
+ private class TestableRemoteAttributeBase : RemoteAttributeBase
+ {
+ private readonly string _dummyGetUrlReturnValue;
+
+ public TestableRemoteAttributeBase()
+ { }
+
+ public TestableRemoteAttributeBase(string dummyGetUrlReturnValue)
+ {
+ _dummyGetUrlReturnValue = dummyGetUrlReturnValue;
+ }
+
+ protected override string GetUrl(ClientModelValidationContext context)
+ {
+ return _dummyGetUrlReturnValue;
+ }
+ }
+ }
+}
diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/RemoteAttributeTest.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/RemoteAttributeTest.cs
index 5742da183b..649b350952 100644
--- a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/RemoteAttributeTest.cs
+++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/RemoteAttributeTest.cs
@@ -5,7 +5,6 @@ using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
-using Microsoft.AspNetCore.Mvc.DataAnnotations;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.AspNetCore.Mvc.Routing;
@@ -13,24 +12,17 @@ using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.ObjectPool;
using Microsoft.Extensions.Options;
using Moq;
using Xunit;
-using Resources = Microsoft.AspNetCore.Mvc.ViewFeatures.Test.Resources;
namespace Microsoft.AspNetCore.Mvc
{
public class RemoteAttributeTest
{
- private static readonly IModelMetadataProvider _metadataProvider = new EmptyModelMetadataProvider();
- private static readonly ModelMetadata _metadata = _metadataProvider.GetMetadataForProperty(
- typeof(string),
- nameof(string.Length));
-
public static TheoryData SomeNames
{
get
@@ -57,15 +49,7 @@ namespace Microsoft.AspNetCore.Mvc
};
}
}
-
- [Fact]
- public void IsValidAlwaysReturnsTrue()
- {
- // Act & Assert
- Assert.True(new RemoteAttribute("RouteName", "ParameterName").IsValid(value: null));
- Assert.True(new RemoteAttribute("ActionName", "ControllerName", "ParameterName").IsValid(value: null));
- }
-
+
[Fact]
public void Constructor_WithNullAction_IgnoresArgument()
{
@@ -157,516 +141,74 @@ namespace Microsoft.AspNetCore.Mvc
Assert.Equal(areaName, resultName);
Assert.Null(attribute.RouteName);
}
-
+
[Fact]
- public void ErrorMessageProperties_HaveExpectedDefaultValues()
- {
- // Arrange & Act
- var attribute = new RemoteAttribute("Action", "Controller");
-
- // Assert
- Assert.Null(attribute.ErrorMessage);
- Assert.Null(attribute.ErrorMessageResourceName);
- Assert.Null(attribute.ErrorMessageResourceType);
- }
-
- [Fact]
- [ReplaceCulture]
- public void FormatErrorMessage_ReturnsDefaultErrorMessage()
+ public void GetUrl_WithBadRouteName_Throws()
{
// Arrange
- // See ViewFeatures.Resources.RemoteAttribute_RemoteValidationFailed.
- var expected = "'Property1' is invalid.";
- var attribute = new RemoteAttribute("Action", "Controller");
-
- // Act
- var message = attribute.FormatErrorMessage("Property1");
-
- // Assert
- Assert.Equal(expected, message);
- }
-
- [Fact]
- public void FormatErrorMessage_UsesOverriddenErrorMessage()
- {
- // Arrange
- var expected = "Error about 'Property1' from override.";
- var attribute = new RemoteAttribute("Action", "Controller")
- {
- ErrorMessage = "Error about '{0}' from override.",
- };
-
- // Act
- var message = attribute.FormatErrorMessage("Property1");
-
- // Assert
- Assert.Equal(expected, message);
- }
-
- [Fact]
- [ReplaceCulture]
- public void FormatErrorMessage_UsesErrorMessageFromResource()
- {
- // Arrange
- var expected = "Error about 'Property1' from resources.";
- var attribute = new RemoteAttribute("Action", "Controller")
- {
- ErrorMessageResourceName = nameof(Resources.RemoteAttribute_Error),
- ErrorMessageResourceType = typeof(Resources),
- };
-
- // Act
- var message = attribute.FormatErrorMessage("Property1");
-
- // Assert
- Assert.Equal(expected, message);
- }
-
- [Theory]
- [MemberData(nameof(NullOrEmptyNames))]
- public void FormatAdditionalFieldsForClientValidation_WithInvalidPropertyName_Throws(string property)
- {
- // Arrange
- var attribute = new RemoteAttribute(routeName: "default");
- var expectedMessage = "Value cannot be null or empty.";
-
- // Act & Assert
- ExceptionAssert.ThrowsArgument(
- () => attribute.FormatAdditionalFieldsForClientValidation(property),
- "property",
- expectedMessage);
- }
-
- [Theory]
- [MemberData(nameof(NullOrEmptyNames))]
- public void FormatPropertyForClientValidation_WithInvalidPropertyName_Throws(string property)
- {
- // Arrange
- var expected = "Value cannot be null or empty.";
-
- // Act & Assert
- ExceptionAssert.ThrowsArgument(
- () => RemoteAttribute.FormatPropertyForClientValidation(property),
- "property",
- expected);
- }
-
- [Fact]
- public void AddValidation_WithBadRouteName_Throws()
- {
- // Arrange
- var attribute = new RemoteAttribute("nonexistentRoute");
+ var testableAttribute = new TestableRemoteAttribute("nonexistentRoute");
var context = GetValidationContextWithArea(currentArea: null);
// Act & Assert
- var exception = Assert.Throws(() => attribute.AddValidation(context));
+ var exception = Assert.Throws(() => testableAttribute.InvokeGetUrl(context));
Assert.Equal("No URL for remote validation could be found.", exception.Message);
}
[Fact]
- [ReplaceCulture]
- public void AddValidation_WithRoute_CallsUrlHelperWithExpectedValues()
+ public void GetUrl_WithRoute_CallsUrlHelperWithExpectedValues()
{
// Arrange
var routeName = "RouteName";
- var attribute = new RemoteAttribute(routeName);
+ var testableRemoteAttribute = new TestableRemoteAttribute(routeName);
var url = "/my/URL";
var urlHelper = new MockUrlHelper(url, routeName);
var context = GetValidationContext(urlHelper);
// Act
- attribute.AddValidation(context);
-
+ var actualUrl = testableRemoteAttribute.InvokeGetUrl(context);
+
// Assert
- Assert.Collection(
- context.Attributes,
- kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); },
- kvp => { Assert.Equal("data-val-remote", kvp.Key); Assert.Equal("'Length' is invalid.", kvp.Value); },
- kvp => { Assert.Equal("data-val-remote-additionalfields", kvp.Key); Assert.Equal("*.Length", kvp.Value); },
- kvp => { Assert.Equal("data-val-remote-url", kvp.Key); Assert.Equal(url, kvp.Value); });
+ Assert.Equal(url, actualUrl);
var routeDictionary = Assert.IsType(urlHelper.RouteValues);
Assert.Empty(routeDictionary);
}
[Fact]
- [ReplaceCulture]
- public void AddValidation_WithActionController_CallsUrlHelperWithExpectedValues()
+ public void GetUrl_WithActionController_CallsUrlHelperWithExpectedValues()
{
// Arrange
- var attribute = new RemoteAttribute("Action", "Controller");
+ var testableRemoteAttribute = new TestableRemoteAttribute("Action", "Controller");
var url = "/Controller/Action";
var urlHelper = new MockUrlHelper(url, routeName: null);
var context = GetValidationContext(urlHelper);
// Act
- attribute.AddValidation(context);
+ var actualUrl = testableRemoteAttribute.InvokeGetUrl(context);
// Assert
- Assert.Collection(
- context.Attributes,
- kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); },
- kvp => { Assert.Equal("data-val-remote", kvp.Key); Assert.Equal("'Length' is invalid.", kvp.Value); },
- kvp => { Assert.Equal("data-val-remote-additionalfields", kvp.Key); Assert.Equal("*.Length", kvp.Value); },
- kvp => { Assert.Equal("data-val-remote-url", kvp.Key); Assert.Equal(url, kvp.Value); });
+ Assert.Equal(url, actualUrl);
var routeDictionary = Assert.IsType(urlHelper.RouteValues);
Assert.Equal(2, routeDictionary.Count);
Assert.Equal("Action", routeDictionary["action"] as string);
Assert.Equal("Controller", routeDictionary["controller"] as string);
}
-
+
[Fact]
- [ReplaceCulture]
- public void AddValidation_WithActionController_PropertiesSet_CallsUrlHelperWithExpectedValues()
+ public void GetUrl_WithActionControllerArea_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
- attribute.AddValidation(context);
-
- // Assert
- Assert.Collection(
- context.Attributes,
- kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); },
- kvp => { Assert.Equal("data-val-remote", kvp.Key); Assert.Equal("'Length' is invalid.", kvp.Value); },
- kvp =>
- {
- Assert.Equal("data-val-remote-additionalfields", kvp.Key);
- Assert.Equal("*.Length,*.Password,*.ConfirmPassword", kvp.Value);
- },
- kvp => { Assert.Equal("data-val-remote-type", kvp.Key); Assert.Equal("POST", kvp.Value); },
- kvp => { Assert.Equal("data-val-remote-url", kvp.Key); Assert.Equal(url, kvp.Value); });
-
- var routeDictionary = Assert.IsType(urlHelper.RouteValues);
- Assert.Equal(2, routeDictionary.Count);
- Assert.Equal("Action", routeDictionary["action"] as string);
- Assert.Equal("Controller", routeDictionary["controller"] as string);
- }
-
- [Fact]
- public void AddValidation_WithErrorMessage_SetsAttributesAsExpected()
- {
- // Arrange
- var expected = "Error about 'Length' from override.";
- var attribute = new RemoteAttribute("Action", "Controller")
- {
- HttpMethod = "POST",
- ErrorMessage = "Error about '{0}' from override.",
- };
- var url = "/Controller/Action";
- var context = GetValidationContext(url);
-
- // Act
- attribute.AddValidation(context);
-
- // Assert
- Assert.Collection(
- context.Attributes,
- kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); },
- kvp =>
- {
- Assert.Equal("data-val-remote", kvp.Key);
- Assert.Equal(expected, kvp.Value);
- },
- kvp =>
- {
- Assert.Equal("data-val-remote-additionalfields", kvp.Key);
- Assert.Equal("*.Length", kvp.Value);
- },
- kvp => { Assert.Equal("data-val-remote-type", kvp.Key); Assert.Equal("POST", kvp.Value); },
- kvp => { Assert.Equal("data-val-remote-url", kvp.Key); Assert.Equal(url, kvp.Value); });
- }
-
- [Fact]
- public void AddValidation_WithErrorMessageAndLocalizerFactory_SetsAttributesAsExpected()
- {
- // Arrange
- var expected = "Error about 'Length' from override.";
- var attribute = new RemoteAttribute("Action", "Controller")
- {
- HttpMethod = "POST",
- ErrorMessage = "Error about '{0}' from override.",
- };
- var url = "/Controller/Action";
- var context = GetValidationContextWithLocalizerFactory(url);
-
- // Act
- attribute.AddValidation(context);
-
- // Assert
- Assert.Collection(
- context.Attributes,
- kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); },
- kvp =>
- {
- // IStringLocalizerFactory existence alone is insufficient to change error message.
- Assert.Equal("data-val-remote", kvp.Key);
- Assert.Equal(expected, kvp.Value);
- },
- kvp =>
- {
- Assert.Equal("data-val-remote-additionalfields", kvp.Key);
- Assert.Equal("*.Length", kvp.Value);
- },
- kvp => { Assert.Equal("data-val-remote-type", kvp.Key); Assert.Equal("POST", kvp.Value); },
- kvp => { Assert.Equal("data-val-remote-url", kvp.Key); Assert.Equal(url, kvp.Value); });
- }
-
- [Fact]
- public void AddValidation_WithErrorMessageAndLocalizerProvider_SetsAttributesAsExpected()
- {
- // Arrange
- var expected = "Error about 'Length' from override.";
- var attribute = new RemoteAttribute("Action", "Controller")
- {
- HttpMethod = "POST",
- ErrorMessage = "Error about '{0}' from override.",
- };
- var url = "/Controller/Action";
- var context = GetValidationContext(url);
-
- var options = context.ActionContext.HttpContext.RequestServices
- .GetRequiredService>();
- var localizer = new Mock(MockBehavior.Strict);
- options.Value.DataAnnotationLocalizerProvider = (type, factory) => localizer.Object;
-
- // Act
- attribute.AddValidation(context);
-
- // Assert
- Assert.Collection(
- context.Attributes,
- kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); },
- kvp =>
- {
- // Non-null DataAnnotationLocalizerProvider alone is insufficient to change error message.
- Assert.Equal("data-val-remote", kvp.Key);
- Assert.Equal(expected, kvp.Value);
- },
- kvp =>
- {
- Assert.Equal("data-val-remote-additionalfields", kvp.Key);
- Assert.Equal("*.Length", kvp.Value);
- },
- kvp => { Assert.Equal("data-val-remote-type", kvp.Key); Assert.Equal("POST", kvp.Value); },
- kvp => { Assert.Equal("data-val-remote-url", kvp.Key); Assert.Equal(url, kvp.Value); });
- }
-
- [Fact]
- public void AddValidation_WithErrorMessageLocalizerFactoryAndLocalizerProvider_SetsAttributesAsExpected()
- {
- // Arrange
- var expected = "Error about 'Length' from localizer.";
- var attribute = new RemoteAttribute("Action", "Controller")
- {
- HttpMethod = "POST",
- ErrorMessage = "Error about '{0}' from override.",
- };
- var url = "/Controller/Action";
- var context = GetValidationContextWithLocalizerFactory(url);
-
- var localizedString = new LocalizedString("Fred", expected);
- var localizer = new Mock(MockBehavior.Strict);
- localizer
- .Setup(l => l["Error about '{0}' from override.", "Length"])
- .Returns(localizedString)
- .Verifiable();
- var options = context.ActionContext.HttpContext.RequestServices
- .GetRequiredService>();
- options.Value.DataAnnotationLocalizerProvider = (type, factory) => localizer.Object;
-
- // Act
- attribute.AddValidation(context);
-
- // Assert
- localizer.VerifyAll();
-
- Assert.Collection(
- context.Attributes,
- kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); },
- kvp =>
- {
- Assert.Equal("data-val-remote", kvp.Key);
- Assert.Equal(expected, kvp.Value);
- },
- kvp =>
- {
- Assert.Equal("data-val-remote-additionalfields", kvp.Key);
- Assert.Equal("*.Length", kvp.Value);
- },
- kvp => { Assert.Equal("data-val-remote-type", kvp.Key); Assert.Equal("POST", kvp.Value); },
- kvp => { Assert.Equal("data-val-remote-url", kvp.Key); Assert.Equal(url, kvp.Value); });
- }
-
- [Fact]
- [ReplaceCulture]
- public void AddValidation_WithErrorResourcesLocalizerFactoryAndLocalizerProvider_SetsAttributesAsExpected()
- {
- // Arrange
- var expected = "Error about 'Length' from resources.";
- var attribute = new RemoteAttribute("Action", "Controller")
- {
- HttpMethod = "POST",
- ErrorMessageResourceName = nameof(Resources.RemoteAttribute_Error),
- ErrorMessageResourceType = typeof(Resources),
- };
- var url = "/Controller/Action";
- var context = GetValidationContextWithLocalizerFactory(url);
-
- var localizer = new Mock(MockBehavior.Strict);
- var options = context.ActionContext.HttpContext.RequestServices
- .GetRequiredService>();
- options.Value.DataAnnotationLocalizerProvider = (type, factory) => localizer.Object;
-
- // Act
- attribute.AddValidation(context);
-
- // Assert
- Assert.Collection(
- context.Attributes,
- kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); },
- kvp =>
- {
- // Configuring the attribute using ErrorMessageResource* trumps available IStringLocalizer etc.
- Assert.Equal("data-val-remote", kvp.Key);
- Assert.Equal(expected, kvp.Value);
- },
- kvp =>
- {
- Assert.Equal("data-val-remote-additionalfields", kvp.Key);
- Assert.Equal("*.Length", kvp.Value);
- },
- kvp => { Assert.Equal("data-val-remote-type", kvp.Key); Assert.Equal("POST", kvp.Value); },
- kvp => { Assert.Equal("data-val-remote-url", kvp.Key); Assert.Equal(url, kvp.Value); });
- }
-
- [Fact]
- public void AddValidation_WithErrorMessageAndDisplayName_SetsAttributesAsExpected()
- {
- // Arrange
- var expected = "Error about 'Display Length' from override.";
- var attribute = new RemoteAttribute("Action", "Controller")
- {
- HttpMethod = "POST",
- ErrorMessage = "Error about '{0}' from override.",
- };
-
- var url = "/Controller/Action";
- var metadataProvider = new TestModelMetadataProvider();
- metadataProvider
- .ForProperty(typeof(string), nameof(string.Length))
- .DisplayDetails(d => d.DisplayName = () => "Display Length");
- var context = GetValidationContext(url, metadataProvider);
-
- // Act
- attribute.AddValidation(context);
-
- // Assert
- Assert.Collection(
- context.Attributes,
- kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); },
- kvp =>
- {
- Assert.Equal("data-val-remote", kvp.Key);
- Assert.Equal(expected, kvp.Value);
- },
- kvp =>
- {
- Assert.Equal("data-val-remote-additionalfields", kvp.Key);
- Assert.Equal("*.Length", kvp.Value);
- },
- kvp => { Assert.Equal("data-val-remote-type", kvp.Key); Assert.Equal("POST", kvp.Value); },
- kvp => { Assert.Equal("data-val-remote-url", kvp.Key); Assert.Equal(url, kvp.Value); });
- }
-
- [Fact]
- public void AddValidation_WithErrorMessageLocalizerFactoryLocalizerProviderAndDisplayName_SetsAttributesAsExpected()
- {
- // Arrange
- var expected = "Error about 'Length' from localizer.";
- var attribute = new RemoteAttribute("Action", "Controller")
- {
- HttpMethod = "POST",
- ErrorMessage = "Error about '{0}' from override.",
- };
-
- var url = "/Controller/Action";
- var metadataProvider = new TestModelMetadataProvider();
- metadataProvider
- .ForProperty(typeof(string), nameof(string.Length))
- .DisplayDetails(d => d.DisplayName = () => "Display Length");
- var context = GetValidationContextWithLocalizerFactory(url, metadataProvider);
-
- var localizedString = new LocalizedString("Fred", expected);
- var localizer = new Mock(MockBehavior.Strict);
- localizer
- .Setup(l => l["Error about '{0}' from override.", "Display Length"])
- .Returns(localizedString)
- .Verifiable();
- var options = context.ActionContext.HttpContext.RequestServices
- .GetRequiredService>();
- options.Value.DataAnnotationLocalizerProvider = (type, factory) => localizer.Object;
-
- // Act
- attribute.AddValidation(context);
-
- // Assert
- localizer.VerifyAll();
-
- Assert.Collection(
- context.Attributes,
- kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); },
- kvp =>
- {
- Assert.Equal("data-val-remote", kvp.Key);
- Assert.Equal(expected, kvp.Value);
- },
- kvp =>
- {
- Assert.Equal("data-val-remote-additionalfields", kvp.Key);
- Assert.Equal("*.Length", kvp.Value);
- },
- kvp => { Assert.Equal("data-val-remote-type", kvp.Key); Assert.Equal("POST", kvp.Value); },
- kvp => { Assert.Equal("data-val-remote-url", kvp.Key); Assert.Equal(url, kvp.Value); });
- }
-
- [Fact]
- [ReplaceCulture]
- public void AddValidation_WithActionControllerArea_CallsUrlHelperWithExpectedValues()
- {
- // Arrange
- var attribute = new RemoteAttribute("Action", "Controller", "Test")
- {
- HttpMethod = "POST",
- };
+ var testableAttribute = new TestableRemoteAttribute("Action", "Controller", "Test");
var url = "/Test/Controller/Action";
var urlHelper = new MockUrlHelper(url, routeName: null);
var context = GetValidationContext(urlHelper);
// Act
- attribute.AddValidation(context);
+ var actualUrl = testableAttribute.InvokeGetUrl(context);
// Assert
- Assert.Collection(
- context.Attributes,
- kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); },
- kvp => { Assert.Equal("data-val-remote", kvp.Key); Assert.Equal("'Length' is invalid.", kvp.Value); },
- kvp =>
- {
- Assert.Equal("data-val-remote-additionalfields", kvp.Key);
- Assert.Equal("*.Length", kvp.Value);
- },
- kvp => { Assert.Equal("data-val-remote-type", kvp.Key); Assert.Equal("POST", kvp.Value); },
- kvp => { Assert.Equal("data-val-remote-url", kvp.Key); Assert.Equal(url, kvp.Value); });
+ Assert.Equal(url, actualUrl);
var routeDictionary = Assert.IsType(urlHelper.RouteValues);
Assert.Equal(3, routeDictionary.Count);
@@ -677,179 +219,109 @@ namespace Microsoft.AspNetCore.Mvc
// Root area is current in this case.
[Fact]
- [ReplaceCulture]
- public void AddValidation_WithActionController_FindsControllerInCurrentArea()
+ public void GetUrl_WithActionController_FindsControllerInCurrentArea()
{
// Arrange
- var attribute = new RemoteAttribute("Action", "Controller");
+ var testableAttribute = new TestableRemoteAttribute("Action", "Controller");
var context = GetValidationContextWithArea(currentArea: null);
// Act
- attribute.AddValidation(context);
+ var actualUrl = testableAttribute.InvokeGetUrl(context);
// Assert
- Assert.Collection(
- context.Attributes,
- kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); },
- kvp => { Assert.Equal("data-val-remote", kvp.Key); Assert.Equal("'Length' is invalid.", kvp.Value); },
- kvp => { Assert.Equal("data-val-remote-additionalfields", kvp.Key); Assert.Equal("*.Length", kvp.Value); },
- kvp =>
- {
- Assert.Equal("data-val-remote-url", kvp.Key);
- Assert.Equal("/Controller/Action", kvp.Value);
- });
+ Assert.Equal("/Controller/Action", actualUrl);
}
// Test area is current in this case.
[Fact]
- [ReplaceCulture]
- public void AddValidation_WithActionControllerInArea_FindsControllerInCurrentArea()
+ public void GetUrl_WithActionControllerInArea_FindsControllerInCurrentArea()
{
// Arrange
- var attribute = new RemoteAttribute("Action", "Controller");
+ var testableAttribute = new TestableRemoteAttribute("Action", "Controller");
var context = GetValidationContextWithArea(currentArea: "Test");
// Act
- attribute.AddValidation(context);
+ var actualUrl = testableAttribute.InvokeGetUrl(context);
// Assert
- Assert.Collection(
- context.Attributes,
- kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); },
- kvp => { Assert.Equal("data-val-remote", kvp.Key); Assert.Equal("'Length' is invalid.", kvp.Value); },
- kvp => { Assert.Equal("data-val-remote-additionalfields", kvp.Key); Assert.Equal("*.Length", kvp.Value); },
- kvp =>
- {
- Assert.Equal("data-val-remote-url", kvp.Key);
- Assert.Equal("/Test/Controller/Action", kvp.Value);
- });
+ Assert.Equal("/Test/Controller/Action", actualUrl);
}
// Explicit reference to the (current) root area.
[Theory]
[MemberData(nameof(NullOrEmptyNames))]
- [ReplaceCulture]
- public void AddValidation_WithActionControllerArea_FindsControllerInRootArea(string areaName)
+ public void GetUrl_WithActionControllerArea_FindsControllerInRootArea(string areaName)
{
// Arrange
- var attribute = new RemoteAttribute("Action", "Controller", areaName);
+ var testableAttribute = new TestableRemoteAttribute("Action", "Controller", areaName);
var context = GetValidationContextWithArea(currentArea: null);
// Act
- attribute.AddValidation(context);
+ var actualUrl = testableAttribute.InvokeGetUrl(context);
// Assert
- Assert.Collection(
- context.Attributes,
- kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); },
- kvp => { Assert.Equal("data-val-remote", kvp.Key); Assert.Equal("'Length' is invalid.", kvp.Value); },
- kvp => { Assert.Equal("data-val-remote-additionalfields", kvp.Key); Assert.Equal("*.Length", kvp.Value); },
- kvp =>
- {
- Assert.Equal("data-val-remote-url", kvp.Key);
- Assert.Equal("/Controller/Action", kvp.Value);
- });
+ Assert.Equal("/Controller/Action", actualUrl);
}
// Test area is current in this case.
[Theory]
[MemberData(nameof(NullOrEmptyNames))]
- [ReplaceCulture]
- public void AddValidation_WithActionControllerAreaInArea_FindsControllerInRootArea(string areaName)
+ public void GetUrl_WithActionControllerAreaInArea_FindsControllerInRootArea(string areaName)
{
// Arrange
- var attribute = new RemoteAttribute("Action", "Controller", areaName);
+ var testableAttribute = new TestableRemoteAttribute("Action", "Controller", areaName);
var context = GetValidationContextWithArea(currentArea: "Test");
// Act
- attribute.AddValidation(context);
+ var actualUrl = testableAttribute.InvokeGetUrl(context);
// Assert
- Assert.Collection(
- context.Attributes,
- kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); },
- kvp => { Assert.Equal("data-val-remote", kvp.Key); Assert.Equal("'Length' is invalid.", kvp.Value); },
- kvp => { Assert.Equal("data-val-remote-additionalfields", kvp.Key); Assert.Equal("*.Length", kvp.Value); },
- kvp =>
- {
- Assert.Equal("data-val-remote-url", kvp.Key);
- Assert.Equal("/Controller/Action", kvp.Value);
- });
+ Assert.Equal("/Controller/Action", actualUrl);
}
// Root area is current in this case.
[Fact]
- [ReplaceCulture]
- public void AddValidation_WithActionControllerArea_FindsControllerInNamedArea()
+ public void GetUrl_WithActionControllerArea_FindsControllerInNamedArea()
{
// Arrange
- var attribute = new RemoteAttribute("Action", "Controller", "Test");
+ var testableAttribute = new TestableRemoteAttribute("Action", "Controller", "Test");
var context = GetValidationContextWithArea(currentArea: null);
// Act
- attribute.AddValidation(context);
+ var actualUrl = testableAttribute.InvokeGetUrl(context);
// Assert
- Assert.Collection(
- context.Attributes,
- kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); },
- kvp => { Assert.Equal("data-val-remote", kvp.Key); Assert.Equal("'Length' is invalid.", kvp.Value); },
- kvp => { Assert.Equal("data-val-remote-additionalfields", kvp.Key); Assert.Equal("*.Length", kvp.Value); },
- kvp =>
- {
- Assert.Equal("data-val-remote-url", kvp.Key);
- Assert.Equal("/Test/Controller/Action", kvp.Value);
- });
+ Assert.Equal("/Test/Controller/Action", actualUrl);
}
// Explicit reference to the current (Test) area.
[Fact]
- [ReplaceCulture]
- public void AddValidation_WithActionControllerAreaInArea_FindsControllerInNamedArea()
+ public void GetUrl_WithActionControllerAreaInArea_FindsControllerInNamedArea()
{
// Arrange
- var attribute = new RemoteAttribute("Action", "Controller", "Test");
+ var testableAttribute = new TestableRemoteAttribute("Action", "Controller", "Test");
var context = GetValidationContextWithArea(currentArea: "Test");
// Act
- attribute.AddValidation(context);
+ var actualUrl = testableAttribute.InvokeGetUrl(context);
// Assert
- Assert.Collection(
- context.Attributes,
- kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); },
- kvp => { Assert.Equal("data-val-remote", kvp.Key); Assert.Equal("'Length' is invalid.", kvp.Value); },
- kvp => { Assert.Equal("data-val-remote-additionalfields", kvp.Key); Assert.Equal("*.Length", kvp.Value); },
- kvp =>
- {
- Assert.Equal("data-val-remote-url", kvp.Key);
- Assert.Equal("/Test/Controller/Action", kvp.Value);
- });
+ Assert.Equal("/Test/Controller/Action", actualUrl);
}
// Test area is current in this case.
[Fact]
- [ReplaceCulture]
- public void AddValidation_WithActionControllerAreaInArea_FindsControllerInDifferentArea()
+ public void GetUrl_WithActionControllerAreaInArea_FindsControllerInDifferentArea()
{
// Arrange
- var attribute = new RemoteAttribute("Action", "Controller", "AnotherArea");
+ var testableAttribute = new TestableRemoteAttribute("Action", "Controller", "AnotherArea");
var context = GetValidationContextWithArea(currentArea: "Test");
// Act
- attribute.AddValidation(context);
+ var actualUrl = testableAttribute.InvokeGetUrl(context);
// Assert
- Assert.Collection(
- context.Attributes,
- kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); },
- kvp => { Assert.Equal("data-val-remote", kvp.Key); Assert.Equal("'Length' is invalid.", kvp.Value); },
- kvp => { Assert.Equal("data-val-remote-additionalfields", kvp.Key); Assert.Equal("*.Length", kvp.Value); },
- kvp =>
- {
- Assert.Equal("data-val-remote-url", kvp.Key);
- Assert.Equal("/AnotherArea/Controller/Action", kvp.Value);
- });
+ Assert.Equal("/AnotherArea/Controller/Action", actualUrl);
}
// Test area is current in this case.
@@ -883,29 +355,16 @@ namespace Microsoft.AspNetCore.Mvc
kvp => { Assert.Equal("data-val-remote-url", kvp.Key); Assert.Equal("original", kvp.Value); });
}
- private static ClientModelValidationContext GetValidationContext(
- string url,
- IModelMetadataProvider metadataProvider = null)
+ private static ClientModelValidationContext GetValidationContext(string url)
{
var urlHelper = new MockUrlHelper(url, routeName: null);
- return GetValidationContext(urlHelper, localizerFactory: null, metadataProvider: metadataProvider);
+ return GetValidationContext(urlHelper);
}
-
- private static ClientModelValidationContext GetValidationContextWithLocalizerFactory(
- string url,
- IModelMetadataProvider metadataProvider = null)
- {
- var urlHelper = new MockUrlHelper(url, routeName: null);
- var localizerFactory = new Mock(MockBehavior.Strict);
- return GetValidationContext(urlHelper, localizerFactory.Object, metadataProvider);
- }
-
+
private static ClientModelValidationContext GetValidationContext(
- IUrlHelper urlHelper,
- IStringLocalizerFactory localizerFactory = null,
- IModelMetadataProvider metadataProvider = null)
+ IUrlHelper urlHelper)
{
- var serviceCollection = GetServiceCollection(localizerFactory);
+ var serviceCollection = GetServiceCollection();
var factory = new Mock(MockBehavior.Strict);
serviceCollection.AddSingleton(factory.Object);
@@ -915,17 +374,12 @@ namespace Microsoft.AspNetCore.Mvc
factory
.Setup(f => f.GetUrlHelper(actionContext))
.Returns(urlHelper);
-
- var metadata = _metadata;
- if (metadataProvider == null)
- {
- metadataProvider = _metadataProvider;
- }
- else
- {
- metadata = metadataProvider.GetMetadataForProperty(typeof(string), nameof(string.Length));
- }
-
+
+ var metadataProvider = new EmptyModelMetadataProvider();
+ var metadata = metadataProvider.GetMetadataForProperty(
+ containerType: typeof(string),
+ propertyName: nameof(string.Length));
+
return new ClientModelValidationContext(
actionContext,
metadata,
@@ -935,7 +389,7 @@ namespace Microsoft.AspNetCore.Mvc
private static ClientModelValidationContext GetValidationContextWithArea(string currentArea)
{
- var serviceCollection = GetServiceCollection(localizerFactory: null);
+ var serviceCollection = GetServiceCollection();
var serviceProvider = serviceCollection.BuildServiceProvider();
var routeCollection = GetRouteCollectionWithArea(serviceProvider);
var routeData = new RouteData
@@ -968,10 +422,15 @@ namespace Microsoft.AspNetCore.Mvc
serviceProvider = serviceCollection.BuildServiceProvider();
actionContext.HttpContext.RequestServices = serviceProvider;
+ var metadataProvider = new EmptyModelMetadataProvider();
+ var metadata = metadataProvider.GetMetadataForProperty(
+ containerType: typeof(string),
+ propertyName: nameof(string.Length));
+
return new ClientModelValidationContext(
actionContext,
- _metadata,
- _metadataProvider,
+ metadata,
+ metadataProvider,
new AttributeDictionary());
}
@@ -987,15 +446,7 @@ namespace Microsoft.AspNetCore.Mvc
return builder.Build();
}
-
- private static IRouter GetRouteCollectionWithNoController(IServiceProvider serviceProvider)
- {
- var builder = GetRouteBuilder(serviceProvider);
- builder.MapRoute("default", "static/route");
-
- return builder.Build();
- }
-
+
private static RouteBuilder GetRouteBuilder(IServiceProvider serviceProvider)
{
var app = new Mock(MockBehavior.Strict);
@@ -1034,7 +485,7 @@ namespace Microsoft.AspNetCore.Mvc
return new ActionContext(httpContext, routeData, new ActionDescriptor());
}
- private static ServiceCollection GetServiceCollection(IStringLocalizerFactory localizerFactory)
+ private static ServiceCollection GetServiceCollection()
{
var serviceCollection = new ServiceCollection();
serviceCollection
@@ -1047,11 +498,6 @@ namespace Microsoft.AspNetCore.Mvc
serviceCollection.AddSingleton(
provider => new DefaultInlineConstraintResolver(provider.GetRequiredService>(), provider));
- if (localizerFactory != null)
- {
- serviceCollection.AddSingleton(localizerFactory);
- }
-
return serviceCollection;
}
@@ -1135,6 +581,11 @@ namespace Microsoft.AspNetCore.Mvc
return base.RouteData;
}
}
+
+ public string InvokeGetUrl(ClientModelValidationContext context)
+ {
+ return base.GetUrl(context);
+ }
}
}
}