diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ContentModelBinderTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ModelBinders/ContentModelBinderTests.cs
similarity index 97%
rename from src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ContentModelBinderTests.cs
rename to src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ModelBinders/ContentModelBinderTests.cs
index 99a3ea4fe5..0ad8e2c421 100644
--- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ContentModelBinderTests.cs
+++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ModelBinders/ContentModelBinderTests.cs
@@ -1,5 +1,4 @@
using System;
-using System.Collections.Generic;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
@@ -12,7 +11,7 @@ using Umbraco.Core.Models.PublishedContent;
using Umbraco.Web.Common.ModelBinders;
using Umbraco.Web.Models;
-namespace Umbraco.Tests.UnitTests.Umbraco.Web.Common
+namespace Umbraco.Tests.UnitTests.Umbraco.Web.Common.ModelBinders
{
[TestFixture]
public class ContentModelBinderTests
@@ -92,9 +91,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Common
var httpContext = new DefaultHttpContext();
var routeData = new RouteData();
if (withUmbracoDataToken)
- {
routeData.DataTokens.Add(Constants.Web.UmbracoDataToken, source);
- }
var actionContext = new ActionContext(httpContext, routeData, new ActionDescriptor());
var metadataProvider = new EmptyModelMetadataProvider();
diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ModelBinders/HttpQueryStringModelBinderTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ModelBinders/HttpQueryStringModelBinderTests.cs
new file mode 100644
index 0000000000..6dd3b024b3
--- /dev/null
+++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ModelBinders/HttpQueryStringModelBinderTests.cs
@@ -0,0 +1,90 @@
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Abstractions;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
+using Microsoft.AspNetCore.Routing;
+using Microsoft.Extensions.Primitives;
+using NUnit.Framework;
+using Umbraco.Web.Common.ModelBinders;
+
+namespace Umbraco.Tests.UnitTests.Umbraco.Web.Common.ModelBinders
+{
+ [TestFixture]
+ public class HttpQueryStringModelBinderTests
+ {
+ [Test]
+ public void Binds_Query_To_FormCollection()
+ {
+ // Arrange
+ var bindingContext = CreateBindingContext("?foo=bar&baz=buzz");
+ var binder = new HttpQueryStringModelBinder();
+
+ // Act
+ binder.BindModelAsync(bindingContext);
+
+ // Assert
+ Assert.True(bindingContext.Result.IsModelSet);
+
+ var typedModel = bindingContext.Result.Model as FormCollection;
+ Assert.IsNotNull(typedModel);
+ Assert.AreEqual(typedModel["foo"], "bar");
+ Assert.AreEqual(typedModel["baz"], "buzz");
+ }
+
+ [Test]
+ public void Sets_Culture_Form_Value_From_Query_If_Provided()
+ {
+ // Arrange
+ var bindingContext = CreateBindingContext("?foo=bar&baz=buzz&culture=en-gb");
+ var binder = new HttpQueryStringModelBinder();
+
+ // Act
+ binder.BindModelAsync(bindingContext);
+
+ // Assert
+ Assert.True(bindingContext.Result.IsModelSet);
+
+ var typedModel = bindingContext.Result.Model as FormCollection;
+ Assert.IsNotNull(typedModel);
+ Assert.AreEqual(typedModel["culture"], "en-gb");
+ }
+
+ [Test]
+ public void Sets_Culture_Form_Value_From_Header_If_Not_Provided_In_Query()
+ {
+ // Arrange
+ var bindingContext = CreateBindingContext("?foo=bar&baz=buzz");
+ var binder = new HttpQueryStringModelBinder();
+
+ // Act
+ binder.BindModelAsync(bindingContext);
+
+ // Assert
+ Assert.True(bindingContext.Result.IsModelSet);
+
+ var typedModel = bindingContext.Result.Model as FormCollection;
+ Assert.IsNotNull(typedModel);
+ Assert.AreEqual(typedModel["culture"], "en-gb");
+ }
+
+ private ModelBindingContext CreateBindingContext(string querystring)
+ {
+ var httpContext = new DefaultHttpContext();
+ httpContext.Request.QueryString = new QueryString(querystring);
+ httpContext.Request.Headers.Add("X-UMB-CULTURE", new StringValues("en-gb"));
+ var routeData = new RouteData();
+ var actionContext = new ActionContext(httpContext, routeData, new ActionDescriptor());
+ var metadataProvider = new EmptyModelMetadataProvider();
+ var routeValueDictionary = new RouteValueDictionary();
+ var valueProvider = new RouteValueProvider(BindingSource.Path, routeValueDictionary);
+ var modelType = typeof(FormCollection);
+ return new DefaultModelBindingContext
+ {
+ ActionContext = actionContext,
+ ModelMetadata = metadataProvider.GetMetadataForType(modelType),
+ ModelName = modelType.Name,
+ ValueProvider = valueProvider,
+ };
+ }
+ }
+}
diff --git a/src/Umbraco.Web.Common/Extensions/HttpRequestExtensions.cs b/src/Umbraco.Web.Common/Extensions/HttpRequestExtensions.cs
new file mode 100644
index 0000000000..9dab445992
--- /dev/null
+++ b/src/Umbraco.Web.Common/Extensions/HttpRequestExtensions.cs
@@ -0,0 +1,12 @@
+using Microsoft.AspNetCore.Http;
+
+namespace Umbraco.Web.Common.Extensions
+{
+ public static class HttpRequestExtensions
+ {
+ internal static string ClientCulture(this HttpRequest request)
+ {
+ return request.Headers.TryGetValue("X-UMB-CULTURE", out var values) ? values[0] : null;
+ }
+ }
+}
diff --git a/src/Umbraco.Web.Common/ModelBinders/HttpQueryStringModelBinder.cs b/src/Umbraco.Web.Common/ModelBinders/HttpQueryStringModelBinder.cs
new file mode 100644
index 0000000000..eb6a1ab7fb
--- /dev/null
+++ b/src/Umbraco.Web.Common/ModelBinders/HttpQueryStringModelBinder.cs
@@ -0,0 +1,51 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
+using Microsoft.Extensions.Primitives;
+using Umbraco.Core;
+using Umbraco.Web.Common.Extensions;
+
+namespace Umbraco.Web.Common.ModelBinders
+{
+ ///
+ /// Allows an Action to execute with an arbitrary number of QueryStrings
+ ///
+ ///
+ /// Just like you can POST an arbitrary number of parameters to an Action, you can't GET an arbitrary number
+ /// but this will allow you to do it.
+ ///
+ public sealed class HttpQueryStringModelBinder : IModelBinder
+ {
+ public Task BindModelAsync(ModelBindingContext bindingContext)
+ {
+ var queryStrings = GetQueryAsDictionary(bindingContext.ActionContext.HttpContext.Request.Query);
+ var queryStringKeys = queryStrings.Select(kvp => kvp.Key).ToArray();
+ if (queryStringKeys.InvariantContains("culture") == false)
+ {
+ queryStrings.Add("culture", new StringValues(bindingContext.ActionContext.HttpContext.Request.ClientCulture()));
+ }
+
+ var formData = new FormCollection(queryStrings);
+ bindingContext.Result = ModelBindingResult.Success(formData);
+ return Task.CompletedTask;
+ }
+
+ private Dictionary GetQueryAsDictionary(IQueryCollection query)
+ {
+ var result = new Dictionary();
+ if (query == null)
+ {
+ return result;
+ }
+
+ foreach (var item in query)
+ {
+ result.Add(item.Key, item.Value);
+ }
+
+ return result;
+ }
+ }
+}
diff --git a/src/Umbraco.Web/WebApi/Filters/HttpQueryStringModelBinder.cs b/src/Umbraco.Web/WebApi/Filters/HttpQueryStringModelBinder.cs
index 4d58e2b512..3b8e4bea3f 100644
--- a/src/Umbraco.Web/WebApi/Filters/HttpQueryStringModelBinder.cs
+++ b/src/Umbraco.Web/WebApi/Filters/HttpQueryStringModelBinder.cs
@@ -14,6 +14,7 @@ namespace Umbraco.Web.WebApi.Filters
/// Just like you can POST an arbitrary number of parameters to an Action, you can't GET an arbitrary number
/// but this will allow you to do it
///
+ /// Migrated to .NET core
public sealed class HttpQueryStringModelBinder : IModelBinder
{
public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)