diff --git a/src/Microsoft.AspNet.Mvc.Core/AreaReference.cs b/src/Microsoft.AspNet.Mvc.Core/AreaReference.cs
new file mode 100644
index 0000000000..c4fccb132f
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.Core/AreaReference.cs
@@ -0,0 +1,20 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
+
+namespace System.Web.Mvc
+{
+ ///
+ /// Controls interpretation of a controller name when constructing a .
+ ///
+ public enum AreaReference
+ {
+ ///
+ /// Find the controller in the current area.
+ ///
+ UseCurrent = 0,
+
+ ///
+ /// Find the controller in the root area.
+ ///
+ UseRoot = 1,
+ }
+}
diff --git a/src/Microsoft.AspNet.Mvc.Core/Internal/ModelClientValidationRemoteRule.cs b/src/Microsoft.AspNet.Mvc.Core/Internal/ModelClientValidationRemoteRule.cs
new file mode 100644
index 0000000000..374da299a4
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.Core/Internal/ModelClientValidationRemoteRule.cs
@@ -0,0 +1,26 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
+
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.CompilerServices;
+
+namespace System.Web.Mvc
+{
+ [TypeForwardedFrom("System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35")]
+ 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;
+
+ if (!String.IsNullOrEmpty(httpMethod))
+ {
+ ValidationParameters["type"] = httpMethod;
+ }
+
+ ValidationParameters["additionalfields"] = additionalFields;
+ }
+ }
+}
diff --git a/src/Microsoft.AspNet.Mvc.Core/RemoteAttribute.cs b/src/Microsoft.AspNet.Mvc.Core/RemoteAttribute.cs
new file mode 100644
index 0000000000..21868ee211
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.Core/RemoteAttribute.cs
@@ -0,0 +1,158 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
+
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Web.Mvc.Properties;
+using System.Web.Routing;
+
+namespace System.Web.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
+ {
+ private string _additionalFields;
+ private string[] _additonalFieldsSplit = new string[0];
+
+ protected RemoteAttribute()
+ : base(MvcResources.RemoteAttribute_RemoteValidationFailed)
+ {
+ RouteData = new RouteValueDictionary();
+ }
+
+ public RemoteAttribute(string routeName)
+ : this()
+ {
+ if (String.IsNullOrWhiteSpace(routeName))
+ {
+ throw new ArgumentException(MvcResources.Common_NullOrEmpty, "routeName");
+ }
+
+ RouteName = routeName;
+ }
+
+ public RemoteAttribute(string action, string controller)
+ :
+ this(action, controller, null /* areaName */)
+ {
+ }
+
+ public RemoteAttribute(string action, string controller, string areaName)
+ : this()
+ {
+ if (String.IsNullOrWhiteSpace(action))
+ {
+ throw new ArgumentException(MvcResources.Common_NullOrEmpty, "action");
+ }
+ if (String.IsNullOrWhiteSpace(controller))
+ {
+ throw new ArgumentException(MvcResources.Common_NullOrEmpty, "controller");
+ }
+
+ RouteData["controller"] = controller;
+ RouteData["action"] = action;
+
+ if (!String.IsNullOrWhiteSpace(areaName))
+ {
+ RouteData["area"] = areaName;
+ }
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The route name.
+ /// The name of the controller.
+ ///
+ /// Find the controller in the root if . Otherwise look in the current area.
+ ///
+ public RemoteAttribute(string action, string controller, AreaReference areaReference)
+ : this(action, controller)
+ {
+ if (areaReference == AreaReference.UseRoot)
+ {
+ RouteData["area"] = null;
+ }
+ }
+
+ public string HttpMethod { get; set; }
+
+ public string AdditionalFields
+ {
+ get { return _additionalFields ?? String.Empty; }
+ set
+ {
+ _additionalFields = value;
+ _additonalFieldsSplit = AuthorizeAttribute.SplitString(value);
+ }
+ }
+
+ protected RouteValueDictionary RouteData { get; private set; }
+
+ protected string RouteName { get; set; }
+
+ protected virtual RouteCollection Routes
+ {
+ get { return RouteTable.Routes; }
+ }
+
+ public string FormatAdditionalFieldsForClientValidation(string property)
+ {
+ if (String.IsNullOrEmpty(property))
+ {
+ throw new ArgumentException(MvcResources.Common_NullOrEmpty, "property");
+ }
+
+ string delimitedAdditionalFields = FormatPropertyForClientValidation(property);
+
+ foreach (string field in _additonalFieldsSplit)
+ {
+ delimitedAdditionalFields += "," + FormatPropertyForClientValidation(field);
+ }
+
+ return delimitedAdditionalFields;
+ }
+
+ public static string FormatPropertyForClientValidation(string property)
+ {
+ if (String.IsNullOrEmpty(property))
+ {
+ throw new ArgumentException(MvcResources.Common_NullOrEmpty, "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)
+ {
+ var pathData = Routes.GetVirtualPathForArea(controllerContext.RequestContext,
+ RouteName,
+ RouteData);
+
+ if (pathData == null)
+ {
+ throw new InvalidOperationException(MvcResources.RemoteAttribute_NoUrlFound);
+ }
+
+ return pathData.VirtualPath;
+ }
+
+ public override string FormatErrorMessage(string name)
+ {
+ return String.Format(CultureInfo.CurrentCulture, ErrorMessageString, name);
+ }
+
+ public override bool IsValid(object value)
+ {
+ return true;
+ }
+
+ public IEnumerable GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
+ {
+ yield return new ModelClientValidationRemoteRule(FormatErrorMessage(metadata.GetDisplayName()), GetUrl(context), HttpMethod, FormatAdditionalFieldsForClientValidation(metadata.PropertyName));
+ }
+ }
+}
diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/RemoteAttributeTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/RemoteAttributeTest.cs
new file mode 100644
index 0000000000..9b7ff69921
--- /dev/null
+++ b/test/Microsoft.AspNet.Mvc.Core.Test/RemoteAttributeTest.cs
@@ -0,0 +1,467 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
+
+using System.Collections.Generic;
+using System.Linq;
+using System.Web.Routing;
+using Microsoft.TestCommon;
+using Moq;
+
+namespace System.Web.Mvc.Test
+{
+ public class RemoteAttributeTest
+ {
+ // Good route name, bad route name
+ // Controller + Action
+
+ [Fact]
+ public void GuardClauses()
+ {
+ // 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");
+ }
+
+ [Fact]
+ public void IsValidAlwaysReturnsTrue()
+ {
+ // Act & Assert
+ Assert.True(new RemoteAttribute("RouteName", "ParameterName").IsValid(null));
+ Assert.True(new RemoteAttribute("ActionName", "ControllerName", "ParameterName").IsValid(null));
+ }
+
+ [Fact]
+ public void BadRouteNameThrows()
+ {
+ // Arrange
+ ControllerContext context = new ControllerContext();
+ ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForType(null, typeof(object));
+ TestableRemoteAttribute attribute = new TestableRemoteAttribute("RouteName");
+
+ // Act & Assert
+ Assert.Throws(
+ () => new List(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(
+ () => new List(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();
+
+ // 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"]);
+ }
+
+ [Fact]
+ public void ActionControllerReturnsCorrectClientDataWithoutNamedParameters()
+ {
+ // 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();
+
+ // Assert
+ 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(
+ () => rule.ValidationParameters["type"],
+ "The given key was not present in the dictionary.");
+ }
+
+ [Fact]
+ public void ActionControllerReturnsCorrectClientDataWithNamedParameters()
+ {
+ // Arrange
+ string url = null;
+
+ 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
+ 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"]);
+ }
+
+ // Current area is root in this case.
+ [Fact]
+ public void ActionController_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, GetMockControllerContext(url: null)).Single();
+
+ // Assert
+ Assert.Equal("remote", rule.ValidationType);
+ Assert.Equal("/Controller/Action", rule.ValidationParameters["url"]);
+ }
+
+ [Fact]
+ public void ActionControllerArea_RemoteFindsControllerInNamedArea()
+ {
+ // Arrange
+ ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForProperty(modelAccessor: null,
+ containerType: typeof(string), propertyName: "Length");
+ TestableRemoteAttribute attribute = new TestableRemoteAttribute("Action", "Controller", "Test");
+ 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, GetMockControllerContext(url: null)).Single();
+
+ // Assert
+ Assert.Equal("remote", rule.ValidationType);
+ Assert.Equal("/Test/Controller/Action", rule.ValidationParameters["url"]);
+ }
+
+ // Current area is root in this case.
+ [Fact]
+ public void ActionControllerArea_WithEmptyArea_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, GetMockControllerContext(url: null)).Single();
+
+ // Assert
+ Assert.Equal("remote", rule.ValidationType);
+ Assert.Equal("/Controller/Action", rule.ValidationParameters["url"]);
+ }
+
+ // Current area is root in this case.
+ [Fact]
+ public void ActionControllerAreaReference_WithUseCurrent_RemoteFindsControllerInCurrentArea()
+ {
+ // 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 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
+ Assert.Equal("remote", rule.ValidationType);
+ Assert.Equal("/Controller/Action", rule.ValidationParameters["url"]);
+ }
+
+ [Fact]
+ public void ActionControllerAreaReference_WithUseRoot_RemoteFindsControllerInRoot()
+ {
+ // 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 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
+ Assert.Equal("remote", rule.ValidationType);
+ Assert.Equal("/Controller/Action", rule.ValidationParameters["url"]);
+ }
+
+ // 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("/Test/Controller/Action", rule.ValidationParameters["url"]);
+ }
+
+ // Explicit reference to the Test area.
+ [Fact]
+ public void ActionControllerArea_InSameArea_RemoteFindsControllerInNamedArea()
+ {
+ // Arrange
+ ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForProperty(modelAccessor: null,
+ containerType: typeof(string), propertyName: "Length");
+ TestableRemoteAttribute attribute = new TestableRemoteAttribute("Action", "Controller", "Test");
+ 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("/Test/Controller/Action", rule.ValidationParameters["url"]);
+ }
+
+ [Fact]
+ public void ActionControllerArea_InArea_RemoteFindsControllerInNamedArea()
+ {
+ // Arrange
+ ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForProperty(modelAccessor: null,
+ containerType: typeof(string), propertyName: "Length");
+ TestableRemoteAttribute attribute = new TestableRemoteAttribute("Action", "Controller", "AnotherArea");
+ attribute.HttpMethod = "POST";
+
+ 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
+ Assert.Equal("remote", rule.ValidationType);
+ Assert.Equal("/AnotherArea/Controller/Action", rule.ValidationParameters["url"]);
+ }
+
+ // Current area is Test in this case.
+ [Fact]
+ public void ActionControllerArea_WithEmptyAreaInArea_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("/Test/Controller/Action", rule.ValidationParameters["url"]);
+ }
+
+ // Current area is Test in this case.
+ [Fact]
+ public void ActionControllerAreaReference_WithUseCurrentInArea_RemoteFindsControllerInCurrentArea()
+ {
+ // 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 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"]);
+ }
+
+ [Fact]
+ public void ActionControllerAreaReference_WithUseRootInArea_RemoteFindsControllerInRoot()
+ {
+ // 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 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("/Controller/Action", rule.ValidationParameters["url"]);
+ }
+
+ private ControllerContext GetMockControllerContext(string url)
+ {
+ Mock context = new Mock();
+ context.Setup(c => c.HttpContext.Request.ApplicationPath)
+ .Returns("/");
+ context.Setup(c => c.HttpContext.Response.ApplyAppPathModifier(It.IsAny()))
+ .Callback(vpath => url = vpath)
+ .Returns(() => url);
+
+ return context.Object;
+ }
+
+ private ControllerContext GetMockControllerContextWithArea(string url, string areaName)
+ {
+ Mock context = new Mock();
+ context.Setup(c => c.HttpContext.Request.ApplicationPath)
+ .Returns("/");
+ context.Setup(c => c.HttpContext.Response.ApplyAppPathModifier(It.IsAny()))
+ .Callback(vpath => url = vpath)
+ .Returns(() => url);
+
+ var controllerContext = context.Object;
+
+ controllerContext.RequestContext.RouteData.DataTokens.Add("area", areaName);
+
+ return controllerContext;
+ }
+
+ 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 action, string controller)
+ : base(action, controller)
+ {
+ }
+
+ public TestableRemoteAttribute(string routeName)
+ : base(routeName)
+ {
+ }
+
+ protected override RouteCollection Routes
+ {
+ get { return RouteTable; }
+ }
+ }
+ }
+}