diff --git a/src/Umbraco.Core/Constants-HttpContextItemsKeys.cs b/src/Umbraco.Core/Constants-HttpContextItemsKeys.cs
new file mode 100644
index 0000000000..7be1fbd140
--- /dev/null
+++ b/src/Umbraco.Core/Constants-HttpContextItemsKeys.cs
@@ -0,0 +1,19 @@
+namespace Umbraco.Cms.Core
+{
+ public static partial class Constants
+ {
+ public static class HttpContext
+ {
+ ///
+ /// Defines keys for items stored in HttpContext.Items
+ ///
+ public static class Items
+ {
+ ///
+ /// Key for current requests body deserialized as JObject.
+ ///
+ public const string RequestBodyAsJObject = "RequestBodyAsJObject";
+ }
+ }
+ }
+}
diff --git a/src/Umbraco.Web.BackOffice/Controllers/EntityController.cs b/src/Umbraco.Web.BackOffice/Controllers/EntityController.cs
index c3f36e92cb..408ae224ab 100644
--- a/src/Umbraco.Web.BackOffice/Controllers/EntityController.cs
+++ b/src/Umbraco.Web.BackOffice/Controllers/EntityController.cs
@@ -54,6 +54,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
[ParameterSwapControllerActionSelector(nameof(GetById), "id", typeof(int), typeof(Guid), typeof(Udi))]
[ParameterSwapControllerActionSelector(nameof(GetByIds), "ids", typeof(int[]), typeof(Guid[]), typeof(Udi[]))]
[ParameterSwapControllerActionSelector(nameof(GetUrl), "id", typeof(int), typeof(Udi))]
+ [ParameterSwapControllerActionSelector(nameof(GetUrlsByIds), "ids", typeof(int[]), typeof(Guid[]), typeof(Udi[]))]
public class EntityController : UmbracoAuthorizedJsonController
{
private static readonly string[] _postFilterSplitStrings = { "=", "==", "!=", "<>", ">", "<", ">=", "<=" };
@@ -318,6 +319,145 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
return GetUrl(intId.Result, entityType, culture);
}
+ ///
+ /// Get entity URLs by IDs
+ ///
+ ///
+ /// A list of IDs to lookup items by
+ ///
+ /// The entity type to look for.
+ /// The culture to fetch the URL for.
+ /// Dictionary mapping Udi -> Url
+ ///
+ /// We allow for POST because there could be quite a lot of Ids.
+ ///
+ [HttpGet]
+ [HttpPost]
+ public IDictionary GetUrlsByIds([FromJsonPath] int[] ids, [FromQuery] UmbracoEntityTypes type, [FromQuery] string culture = null)
+ {
+ if (ids == null || !ids.Any())
+ {
+ return new Dictionary();
+ }
+
+ string MediaOrDocumentUrl(int id)
+ {
+ switch (type)
+ {
+ case UmbracoEntityTypes.Document:
+ return _publishedUrlProvider.GetUrl(id, culture: culture ?? ClientCulture());
+
+ case UmbracoEntityTypes.Media:
+ {
+ IPublishedContent media = _publishedContentQuery.Media(id);
+
+ // NOTE: If culture is passed here we get an empty string rather than a media item URL.
+ return _publishedUrlProvider.GetMediaUrl(media, culture: null);
+ }
+
+ default:
+ return null;
+ }
+ }
+
+ return ids
+ .Distinct()
+ .Select(id => new {
+ Id = id,
+ Url = MediaOrDocumentUrl(id)
+ }).ToDictionary(x => x.Id, x => x.Url);
+ }
+
+ ///
+ /// Get entity URLs by IDs
+ ///
+ ///
+ /// A list of IDs to lookup items by
+ ///
+ /// The entity type to look for.
+ /// The culture to fetch the URL for.
+ /// Dictionary mapping Udi -> Url
+ ///
+ /// We allow for POST because there could be quite a lot of Ids.
+ ///
+ [HttpGet]
+ [HttpPost]
+ public IDictionary GetUrlsByIds([FromJsonPath] Guid[] ids, [FromQuery] UmbracoEntityTypes type, [FromQuery] string culture = null)
+ {
+ if (ids == null || !ids.Any())
+ {
+ return new Dictionary();
+ }
+
+ string MediaOrDocumentUrl(Guid id)
+ {
+ return type switch
+ {
+ UmbracoEntityTypes.Document => _publishedUrlProvider.GetUrl(id, culture: culture ?? ClientCulture()),
+
+ // NOTE: If culture is passed here we get an empty string rather than a media item URL.
+ UmbracoEntityTypes.Media => _publishedUrlProvider.GetMediaUrl(id, culture: null),
+
+ _ => null
+ };
+ }
+
+ return ids
+ .Distinct()
+ .Select(id => new {
+ Id = id,
+ Url = MediaOrDocumentUrl(id)
+ }).ToDictionary(x => x.Id, x => x.Url);
+ }
+
+ ///
+ /// Get entity URLs by IDs
+ ///
+ ///
+ /// A list of IDs to lookup items by
+ ///
+ /// The entity type to look for.
+ /// The culture to fetch the URL for.
+ /// Dictionary mapping Udi -> Url
+ ///
+ /// We allow for POST because there could be quite a lot of Ids.
+ ///
+ [HttpGet]
+ [HttpPost]
+ public IDictionary GetUrlsByIds([FromJsonPath] Udi[] ids, [FromQuery] UmbracoEntityTypes type, [FromQuery] string culture = null)
+ {
+ if (ids == null || !ids.Any())
+ {
+ return new Dictionary();
+ }
+
+ // TODO: PMJ 2021-09-27 - Should GetUrl(Udi) exist as an extension method on UrlProvider/IUrlProvider (in v9)
+ string MediaOrDocumentUrl(Udi id)
+ {
+ if (id is not GuidUdi guidUdi)
+ {
+ return null;
+ }
+
+ return type switch
+ {
+ UmbracoEntityTypes.Document => _publishedUrlProvider.GetUrl(guidUdi.Guid, culture: culture ?? ClientCulture()),
+
+ // NOTE: If culture is passed here we get an empty string rather than a media item URL.
+ UmbracoEntityTypes.Media => _publishedUrlProvider.GetMediaUrl(guidUdi.Guid, culture: null),
+
+ _ => null
+ };
+ }
+
+ return ids
+ .Distinct()
+ .Select(id => new {
+ Id = id,
+ Url = MediaOrDocumentUrl(id)
+ }).ToDictionary(x => x.Id, x => x.Url);
+ }
+
///
/// Get entity URLs by UDIs
///
@@ -331,33 +471,31 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
///
[HttpGet]
[HttpPost]
+ [Obsolete("Use GetUrlsByIds instead.")]
public IDictionary GetUrlsByUdis([FromJsonPath] Udi[] udis, string culture = null)
{
- if (udis == null || udis.Length == 0)
+ if (udis == null || !udis.Any())
{
return new Dictionary();
}
- // TODO: PMJ 2021-09-27 - Should GetUrl(Udi) exist as an extension method on UrlProvider/IUrlProvider (in v9)
- string MediaOrDocumentUrl(Udi udi)
- {
- if (udi is not GuidUdi guidUdi)
- {
- return null;
- }
+ var udiEntityType = udis.First().EntityType;
+ UmbracoEntityTypes entityType;
- return guidUdi.EntityType switch
- {
- Constants.UdiEntityType.Document => _publishedUrlProvider.GetUrl(guidUdi.Guid,
- culture: culture ?? ClientCulture()),
- // NOTE: If culture is passed here we get an empty string rather than a media item URL WAT
- Constants.UdiEntityType.Media => _publishedUrlProvider.GetMediaUrl(guidUdi.Guid, culture: null),
- _ => null
- };
+ switch (udiEntityType)
+ {
+ case Constants.UdiEntityType.Document:
+ entityType = UmbracoEntityTypes.Document;
+ break;
+ case Constants.UdiEntityType.Media:
+ entityType = UmbracoEntityTypes.Media;
+ break;
+ default:
+ entityType = (UmbracoEntityTypes)(-1);
+ break;
}
- return udis
- .Select(udi => new { Udi = udi, Url = MediaOrDocumentUrl(udi) }).ToDictionary(x => x.Udi, x => x.Url);
+ return GetUrlsByIds(udis, entityType, culture);
}
///
diff --git a/src/Umbraco.Web.BackOffice/Controllers/ParameterSwapControllerActionSelectorAttribute.cs b/src/Umbraco.Web.BackOffice/Controllers/ParameterSwapControllerActionSelectorAttribute.cs
index ac0a7fea84..3fafd2c1e4 100644
--- a/src/Umbraco.Web.BackOffice/Controllers/ParameterSwapControllerActionSelectorAttribute.cs
+++ b/src/Umbraco.Web.BackOffice/Controllers/ParameterSwapControllerActionSelectorAttribute.cs
@@ -1,22 +1,48 @@
-using System;
+using System;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.ActionConstraints;
using Microsoft.AspNetCore.Mvc.Controllers;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
+using Umbraco.Cms.Core;
using Umbraco.Extensions;
namespace Umbraco.Cms.Web.BackOffice.Controllers
{
+ ///
+ ///
+ /// This attribute is odd because it applies at class level where some methods may use it whilst others don't.
+ ///
+ ///
+ ///
+ /// What we should probably have (if we really even need something like this at all) is an attribute for method level.
+ ///
+ ///
+ ///
+ ///
+ /// [HasParameterFromUriOrBodyOfType("ids", typeof(Guid[]))]
+ /// public IActionResult GetByIds([FromJsonPath] Guid[] ids) { }
+ ///
+ /// [HasParameterFromUriOrBodyOfType("ids", typeof(int[]))]
+ /// public IActionResult GetByIds([FromJsonPath] int[] ids) { }
+ ///
+ ///
+ ///
+ ///
+ ///
+ /// That way we wouldn't need confusing things like Accept returning true when action name doesn't even match attribute metadata.
+ ///
+ ///
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
internal class ParameterSwapControllerActionSelectorAttribute : Attribute, IActionConstraint
{
+
private readonly string _actionName;
private readonly string _parameterName;
private readonly Type[] _supportedTypes;
- private string _requestBody;
public ParameterSwapControllerActionSelectorAttribute(string actionName, string parameterName, params Type[] supportedTypes)
{
@@ -33,10 +59,12 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
{
if (!IsValidCandidate(context.CurrentCandidate))
{
+ // See remarks on class, required because we apply at class level
+ // and some controllers have some actions with parameter swaps and others without.
return true;
}
- var chosenCandidate = SelectAction(context);
+ ActionSelectorCandidate? chosenCandidate = SelectAction(context);
var found = context.CurrentCandidate.Equals(chosenCandidate);
return found;
@@ -49,23 +77,45 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
return candidate;
}
+ HttpContext httpContext = context.RouteContext.HttpContext;
+
// if it's a post we can try to read from the body and bind from the json value
- if (context.RouteContext.HttpContext.Request.Method == HttpMethod.Post.ToString())
+ if (context.RouteContext.HttpContext.Request.Method.Equals(HttpMethod.Post.Method))
{
- // We need to use the asynchronous method here if synchronous IO is not allowed (it may or may not be, depending
- // on configuration in UmbracoBackOfficeServiceCollectionExtensions.AddUmbraco()).
- // We can't use async/await due to the need to override IsValidForRequest, which doesn't have an async override, so going with
- // this, which seems to be the least worst option for "sync to async" (https://stackoverflow.com/a/32429753/489433).
- var strJson = _requestBody ??= Task.Run(() => context.RouteContext.HttpContext.Request.GetRawBodyStringAsync()).GetAwaiter().GetResult();
+ JObject postBodyJson;
- var json = JsonConvert.DeserializeObject(strJson);
+ if (httpContext.Items.TryGetValue(Constants.HttpContext.Items.RequestBodyAsJObject, out var value) && value is JObject cached)
+ {
+ postBodyJson = cached;
+ }
+ else
+ {
+ // We need to use the asynchronous method here if synchronous IO is not allowed (it may or may not be, depending
+ // on configuration in UmbracoBackOfficeServiceCollectionExtensions.AddUmbraco()).
+ // We can't use async/await due to the need to override IsValidForRequest, which doesn't have an async override, so going with
+ // this, which seems to be the least worst option for "sync to async" (https://stackoverflow.com/a/32429753/489433).
+ //
+ // To expand on the above, if KestrelServerOptions/IISServerOptions is AllowSynchronousIO=false
+ // And you attempt to read stream sync an InvalidOperationException is thrown with message
+ // "Synchronous operations are disallowed. Call ReadAsync or set AllowSynchronousIO to true instead."
+ var rawBody = Task.Run(() => httpContext.Request.GetRawBodyStringAsync()).GetAwaiter().GetResult();
+ try
+ {
+ postBodyJson = JsonConvert.DeserializeObject(rawBody);
+ httpContext.Items[Constants.HttpContext.Items.RequestBodyAsJObject] = postBodyJson;
+ }
+ catch (JsonException)
+ {
+ postBodyJson = null;
+ }
+ }
- if (json == null)
+ if (postBodyJson == null)
{
return null;
}
- var requestParam = json[_parameterName];
+ var requestParam = postBodyJson[_parameterName];
if (requestParam != null)
{
@@ -85,11 +135,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
}
}
}
- catch (JsonReaderException)
- {
- // can't convert
- }
- catch (JsonSerializationException)
+ catch (JsonException)
{
// can't convert
}
diff --git a/src/Umbraco.Web.BackOffice/ModelBinders/FromJsonPathAttribute.cs b/src/Umbraco.Web.BackOffice/ModelBinders/FromJsonPathAttribute.cs
index a27243714f..f53fbd0b43 100644
--- a/src/Umbraco.Web.BackOffice/ModelBinders/FromJsonPathAttribute.cs
+++ b/src/Umbraco.Web.BackOffice/ModelBinders/FromJsonPathAttribute.cs
@@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
+using Umbraco.Cms.Core;
using Umbraco.Extensions;
namespace Umbraco.Cms.Web.BackOffice.ModelBinders
@@ -36,6 +37,10 @@ namespace Umbraco.Cms.Web.BackOffice.ModelBinders
return;
}
+ if (TryModelBindFromHttpContextItems(bindingContext))
+ {
+ return;
+ }
var strJson = await bindingContext.HttpContext.Request.GetRawBodyStringAsync();
@@ -60,6 +65,30 @@ namespace Umbraco.Cms.Web.BackOffice.ModelBinders
bindingContext.Result = ModelBindingResult.Success(model);
}
+ public static bool TryModelBindFromHttpContextItems(ModelBindingContext bindingContext)
+ {
+ const string key = Constants.HttpContext.Items.RequestBodyAsJObject;
+
+ if (!bindingContext.HttpContext.Items.TryGetValue(key, out var cached))
+ {
+ return false;
+ }
+
+ if (cached is not JObject json)
+ {
+ return false;
+ }
+
+ JToken match = json.SelectToken(bindingContext.FieldName);
+
+ // ReSharper disable once InvertIf
+ if (match != null)
+ {
+ bindingContext.Result = ModelBindingResult.Success(match.ToObject(bindingContext.ModelType));
+ }
+
+ return true;
+ }
}
}
}
diff --git a/src/Umbraco.Web.UI.Client/src/common/mocks/resources/entity.mocks.js b/src/Umbraco.Web.UI.Client/src/common/mocks/resources/entity.mocks.js
index 05594115e1..08c28fcbd1 100644
--- a/src/Umbraco.Web.UI.Client/src/common/mocks/resources/entity.mocks.js
+++ b/src/Umbraco.Web.UI.Client/src/common/mocks/resources/entity.mocks.js
@@ -34,7 +34,7 @@ angular.module('umbraco.mocks').
return [200, nodes, null];
}
- function returnUrlsbyUdis(status, data, headers) {
+ function returnUrlsByIds(status, data, headers) {
if (!mocksUtils.checkAuth()) {
return [401, null, null];
@@ -83,8 +83,8 @@ angular.module('umbraco.mocks').
.respond(returnEntitybyIdsPost);
$httpBackend
- .whenPOST(mocksUtils.urlRegex('/umbraco/UmbracoApi/Entity/GetUrlsByUdis'))
- .respond(returnUrlsbyUdis);
+ .whenPOST(mocksUtils.urlRegex('/umbraco/UmbracoApi/Entity/GetUrlsByIds'))
+ .respond(returnUrlsByIds);
$httpBackend
.whenGET(mocksUtils.urlRegex('/umbraco/UmbracoApi/Entity/GetAncestors'))
diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js
index c6dc313bc7..d94bb4e6be 100644
--- a/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js
+++ b/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js
@@ -127,6 +127,24 @@ function entityResource($q, $http, umbRequestHelper) {
'Failed to retrieve url for id:' + id);
},
+ getUrlsByIds: function(ids, type, culture) {
+ var query = `type=${type}&culture=${culture || ""}`;
+
+ return umbRequestHelper.resourcePromise(
+ $http.post(
+ umbRequestHelper.getApiUrl(
+ "entityApiBaseUrl",
+ "GetUrlsByIds",
+ query),
+ {
+ ids: ids
+ }),
+ 'Failed to retrieve url map for ids ' + ids);
+ },
+
+ /**
+ * @deprecated use getUrlsByIds instead.
+ */
getUrlsByUdis: function(udis, culture) {
var query = "culture=" + (culture || "");
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js
index 1ecd6bdf26..d2a1710e49 100644
--- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js
+++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js
@@ -421,7 +421,7 @@ function contentPickerController($scope, $q, $routeParams, $location, entityReso
var requests = [
entityResource.getByIds(missingIds, entityType),
- entityResource.getUrlsByUdis(missingIds)
+ entityResource.getUrlsByIds(missingIds, entityType)
];
return $q.all(requests).then(function ([data, urlMap]) {
diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/Controllers/EntityControllerTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/Controllers/EntityControllerTests.cs
new file mode 100644
index 0000000000..4e4ce29e9a
--- /dev/null
+++ b/tests/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/Controllers/EntityControllerTests.cs
@@ -0,0 +1,483 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Net;
+using System.Net.Http;
+using System.Net.Http.Json;
+using System.Threading.Tasks;
+using Microsoft.Extensions.DependencyInjection;
+using NUnit.Framework;
+using Umbraco.Cms.Core;
+using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.Services;
+using Umbraco.Cms.Tests.Common.Builders;
+using Umbraco.Cms.Tests.Common.Builders.Extensions;
+using Umbraco.Cms.Tests.Integration.TestServerTest;
+using Umbraco.Cms.Web.BackOffice.Controllers;
+using Umbraco.Cms.Web.Common.Formatters;
+using Umbraco.Extensions;
+
+namespace Umbraco.Cms.Tests.Integration.Umbraco.Web.BackOffice.Controllers
+{
+ [TestFixture]
+ public class EntityControllerTests : UmbracoTestServerTestBase
+ {
+ [Test]
+ public async Task GetUrlsByIds_MediaWithIntegerIds_ReturnsValidMap()
+ {
+ IMediaTypeService mediaTypeService = Services.GetRequiredService();
+ IMediaService mediaService = Services.GetRequiredService();
+
+ var mediaItems = new List();
+
+ using (ScopeProvider.CreateScope(autoComplete: true))
+ {
+ IMediaType mediaType = mediaTypeService.Get("image");
+ mediaTypeService.Save(mediaType);
+
+ mediaItems.Add(MediaBuilder.CreateMediaImage(mediaType, -1));
+ mediaItems.Add(MediaBuilder.CreateMediaImage(mediaType, -1));
+
+ foreach (Media media in mediaItems)
+ {
+ mediaService.Save(media);
+ }
+ }
+
+ var queryParameters = new Dictionary
+ {
+ ["type"] = Constants.UdiEntityType.Media
+ };
+
+ var url = LinkGenerator.GetUmbracoControllerUrl("GetUrlsByIds", typeof(EntityController), queryParameters);
+
+ var payload = new
+ {
+ ids = new[]
+ {
+ mediaItems[0].Id,
+ mediaItems[1].Id,
+ }
+ };
+
+ HttpResponseMessage response = await HttpClientJsonExtensions.PostAsJsonAsync(Client, url, payload);
+
+ // skip pointless un-parseable cruft.
+ (await response.Content.ReadAsStreamAsync()).Seek(AngularJsonMediaTypeFormatter.XsrfPrefix.Length, SeekOrigin.Begin);
+
+ IDictionary results = await response.Content.ReadFromJsonAsync>();
+
+ Assert.Multiple(() =>
+ {
+ Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
+ Assert.IsTrue(results![payload.ids[0]].StartsWith("/media"));
+ Assert.IsTrue(results![payload.ids[1]].StartsWith("/media"));
+ });
+ }
+
+ [Test]
+ public async Task GetUrlsByIds_Media_ReturnsEmptyStringsInMapForUnknownItems()
+ {
+ var queryParameters = new Dictionary
+ {
+ ["type"] = Constants.UdiEntityType.Media
+ };
+
+ var url = LinkGenerator.GetUmbracoControllerUrl("GetUrlsByIds", typeof(EntityController), queryParameters);
+
+ var payload = new
+ {
+ ids = new[] { 1, 2 }
+ };
+
+ HttpResponseMessage response = await HttpClientJsonExtensions.PostAsJsonAsync(Client, url, payload);
+
+ // skip pointless un-parseable cruft.
+ (await response.Content.ReadAsStreamAsync()).Seek(AngularJsonMediaTypeFormatter.XsrfPrefix.Length, SeekOrigin.Begin);
+
+ IDictionary results = await response.Content.ReadFromJsonAsync>();
+
+ Assert.Multiple(() =>
+ {
+ Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
+ Assert.That(results!.Keys.Count, Is.EqualTo(2));
+ Assert.AreEqual(results![payload.ids[0]], string.Empty);
+ });
+ }
+
+ [Test]
+ public async Task GetUrlsByIds_MediaWithGuidIds_ReturnsValidMap()
+ {
+ IMediaTypeService mediaTypeService = Services.GetRequiredService();
+ IMediaService mediaService = Services.GetRequiredService();
+
+ var mediaItems = new List();
+
+ using (ScopeProvider.CreateScope(autoComplete: true))
+ {
+ IMediaType mediaType = mediaTypeService.Get("image");
+ mediaTypeService.Save(mediaType);
+
+ mediaItems.Add(MediaBuilder.CreateMediaImage(mediaType, -1));
+ mediaItems.Add(MediaBuilder.CreateMediaImage(mediaType, -1));
+
+ foreach (Media media in mediaItems)
+ {
+ mediaService.Save(media);
+ }
+ }
+
+ var queryParameters = new Dictionary
+ {
+ ["type"] = Constants.UdiEntityType.Media
+ };
+
+ var url = LinkGenerator.GetUmbracoControllerUrl("GetUrlsByIds", typeof(EntityController), queryParameters);
+
+ var payload = new
+ {
+ ids = new[]
+ {
+ mediaItems[0].Key.ToString(),
+ mediaItems[1].Key.ToString(),
+ }
+ };
+
+ HttpResponseMessage response = await HttpClientJsonExtensions.PostAsJsonAsync(Client, url, payload);
+
+ // skip pointless un-parseable cruft.
+ (await response.Content.ReadAsStreamAsync()).Seek(AngularJsonMediaTypeFormatter.XsrfPrefix.Length, SeekOrigin.Begin);
+
+ IDictionary results = await response.Content.ReadFromJsonAsync>();
+
+ Assert.Multiple(() =>
+ {
+ Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
+ Assert.IsTrue(results![payload.ids[0]].StartsWith("/media"));
+ Assert.IsTrue(results![payload.ids[1]].StartsWith("/media"));
+ });
+ }
+
+ [Test]
+ public async Task GetUrlsByIds_MediaWithUdiIds_ReturnsValidMap()
+ {
+ IMediaTypeService mediaTypeService = Services.GetRequiredService();
+ IMediaService mediaService = Services.GetRequiredService();
+
+ var mediaItems = new List();
+
+ using (ScopeProvider.CreateScope(autoComplete: true))
+ {
+ IMediaType mediaType = mediaTypeService.Get("image");
+ mediaTypeService.Save(mediaType);
+
+ mediaItems.Add(MediaBuilder.CreateMediaImage(mediaType, -1));
+ mediaItems.Add(MediaBuilder.CreateMediaImage(mediaType, -1));
+
+ foreach (Media media in mediaItems)
+ {
+ mediaService.Save(media);
+ }
+ }
+
+ var queryParameters = new Dictionary
+ {
+ ["type"] = Constants.UdiEntityType.Media
+ };
+
+ var url = LinkGenerator.GetUmbracoControllerUrl("GetUrlsByIds", typeof(EntityController), queryParameters);
+
+ var payload = new
+ {
+ ids = new[]
+ {
+ mediaItems[0].GetUdi().ToString(),
+ mediaItems[1].GetUdi().ToString(),
+ }
+ };
+
+ HttpResponseMessage response = await HttpClientJsonExtensions.PostAsJsonAsync(Client, url, payload);
+
+ // skip pointless un-parseable cruft.
+ (await response.Content.ReadAsStreamAsync()).Seek(AngularJsonMediaTypeFormatter.XsrfPrefix.Length, SeekOrigin.Begin);
+
+ IDictionary results = await response.Content.ReadFromJsonAsync>();
+
+ Assert.Multiple(() =>
+ {
+ Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
+ Assert.IsTrue(results![payload.ids[0]].StartsWith("/media"));
+ Assert.IsTrue(results![payload.ids[1]].StartsWith("/media"));
+ });
+ }
+
+ [Test]
+ public async Task GetUrlsByIds_Documents_ReturnsHashesInMapForUnknownItems()
+ {
+ var queryParameters = new Dictionary
+ {
+ ["type"] = Constants.UdiEntityType.Document
+ };
+
+ var url = LinkGenerator.GetUmbracoControllerUrl("GetUrlsByIds", typeof(EntityController), queryParameters);
+
+ var payload = new
+ {
+ ids = new[] { 1, 2 }
+ };
+
+ HttpResponseMessage response = await HttpClientJsonExtensions.PostAsJsonAsync(Client, url, payload);
+
+ // skip pointless un-parseable cruft.
+ (await response.Content.ReadAsStreamAsync()).Seek(AngularJsonMediaTypeFormatter.XsrfPrefix.Length, SeekOrigin.Begin);
+
+ IDictionary results = await response.Content.ReadFromJsonAsync>();
+
+ Assert.Multiple(() =>
+ {
+ Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
+ Assert.That(results!.Keys.Count, Is.EqualTo(2));
+ Assert.AreEqual(results![payload.ids[0]], "#");
+ });
+ }
+
+ [Test]
+ public async Task GetUrlsByIds_DocumentWithIntIds_ReturnsValidMap()
+ {
+ IContentTypeService contentTypeService = Services.GetRequiredService();
+ IContentService contentService = Services.GetRequiredService();
+
+ var contentItems = new List();
+
+ using (ScopeProvider.CreateScope(autoComplete: true))
+ {
+ IContentType contentType = ContentTypeBuilder.CreateBasicContentType();
+ contentTypeService.Save(contentType);
+
+ ContentBuilder builder = new ContentBuilder()
+ .WithContentType(contentType);
+
+ Content root = builder.WithName("foo").Build();
+ contentService.SaveAndPublish(root);
+
+ contentItems.Add(builder.WithParent(root).WithName("bar").Build());
+ contentItems.Add(builder.WithParent(root).WithName("baz").Build());
+
+ foreach (IContent content in contentItems)
+ {
+ contentService.SaveAndPublish(content);
+ }
+ }
+
+ var queryParameters = new Dictionary
+ {
+ ["type"] = Constants.UdiEntityType.Document
+ };
+
+ var url = LinkGenerator.GetUmbracoControllerUrl("GetUrlsByIds", typeof(EntityController), queryParameters);
+
+ var payload = new
+ {
+ ids = new[]
+ {
+ contentItems[0].Id,
+ contentItems[1].Id,
+ }
+ };
+
+ HttpResponseMessage response = await HttpClientJsonExtensions.PostAsJsonAsync(Client, url, payload);
+
+ // skip pointless un-parseable cruft.
+ (await response.Content.ReadAsStreamAsync()).Seek(AngularJsonMediaTypeFormatter.XsrfPrefix.Length, SeekOrigin.Begin);
+
+ IDictionary results = await response.Content.ReadFromJsonAsync>();
+
+ Assert.Multiple(() =>
+ {
+ Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
+ Assert.IsTrue(results![payload.ids[0]].StartsWith("/bar"));
+ Assert.IsTrue(results![payload.ids[1]].StartsWith("/baz"));
+ });
+ }
+
+ [Test]
+ public async Task GetUrlsByIds_DocumentWithGuidIds_ReturnsValidMap()
+ {
+ IContentTypeService contentTypeService = Services.GetRequiredService();
+ IContentService contentService = Services.GetRequiredService();
+
+ var contentItems = new List();
+
+ using (ScopeProvider.CreateScope(autoComplete: true))
+ {
+ IContentType contentType = ContentTypeBuilder.CreateBasicContentType();
+ contentTypeService.Save(contentType);
+
+ ContentBuilder builder = new ContentBuilder()
+ .WithContentType(contentType);
+
+ Content root = builder.WithName("foo").Build();
+ contentService.SaveAndPublish(root);
+
+ contentItems.Add(builder.WithParent(root).WithName("bar").Build());
+ contentItems.Add(builder.WithParent(root).WithName("baz").Build());
+
+ foreach (IContent content in contentItems)
+ {
+ contentService.SaveAndPublish(content);
+ }
+ }
+
+ var queryParameters = new Dictionary
+ {
+ ["type"] = Constants.UdiEntityType.Document
+ };
+
+ var url = LinkGenerator.GetUmbracoControllerUrl("GetUrlsByIds", typeof(EntityController), queryParameters);
+
+ var payload = new
+ {
+ ids = new[]
+ {
+ contentItems[0].Key.ToString(),
+ contentItems[1].Key.ToString(),
+ }
+ };
+
+ HttpResponseMessage response = await HttpClientJsonExtensions.PostAsJsonAsync(Client, url, payload);
+
+ // skip pointless un-parseable cruft.
+ (await response.Content.ReadAsStreamAsync()).Seek(AngularJsonMediaTypeFormatter.XsrfPrefix.Length, SeekOrigin.Begin);
+
+ IDictionary results = await response.Content.ReadFromJsonAsync>();
+
+ Assert.Multiple(() =>
+ {
+ Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
+ Assert.IsTrue(results![payload.ids[0]].StartsWith("/bar"));
+ Assert.IsTrue(results![payload.ids[1]].StartsWith("/baz"));
+ });
+ }
+
+ [Test]
+ public async Task GetUrlsByIds_DocumentWithUdiIds_ReturnsValidMap()
+ {
+ IContentTypeService contentTypeService = Services.GetRequiredService();
+ IContentService contentService = Services.GetRequiredService();
+
+ var contentItems = new List();
+
+ using (ScopeProvider.CreateScope(autoComplete: true))
+ {
+ IContentType contentType = ContentTypeBuilder.CreateBasicContentType();
+ contentTypeService.Save(contentType);
+
+ ContentBuilder builder = new ContentBuilder()
+ .WithContentType(contentType);
+
+ Content root = builder.WithName("foo").Build();
+ contentService.SaveAndPublish(root);
+
+ contentItems.Add(builder.WithParent(root).WithName("bar").Build());
+ contentItems.Add(builder.WithParent(root).WithName("baz").Build());
+
+ foreach (IContent content in contentItems)
+ {
+ contentService.SaveAndPublish(content);
+ }
+ }
+
+ var queryParameters = new Dictionary
+ {
+ ["type"] = Constants.UdiEntityType.Document
+ };
+
+ var url = LinkGenerator.GetUmbracoControllerUrl("GetUrlsByIds", typeof(EntityController), queryParameters);
+
+ var payload = new
+ {
+ ids = new[]
+ {
+ contentItems[0].GetUdi().ToString(),
+ contentItems[1].GetUdi().ToString(),
+ }
+ };
+
+ HttpResponseMessage response = await HttpClientJsonExtensions.PostAsJsonAsync(Client, url, payload);
+
+ // skip pointless un-parseable cruft.
+ (await response.Content.ReadAsStreamAsync()).Seek(AngularJsonMediaTypeFormatter.XsrfPrefix.Length, SeekOrigin.Begin);
+
+ IDictionary results = await response.Content.ReadFromJsonAsync>();
+
+ Assert.Multiple(() =>
+ {
+ Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
+ Assert.IsTrue(results![payload.ids[0]].StartsWith("/bar"));
+ Assert.IsTrue(results![payload.ids[1]].StartsWith("/baz"));
+ });
+ }
+
+ [Test]
+ public async Task GetByIds_MultipleCalls_WorksAsExpected()
+ {
+ IContentTypeService contentTypeService = Services.GetRequiredService();
+ IContentService contentService = Services.GetRequiredService();
+
+ var contentItems = new List();
+
+ using (ScopeProvider.CreateScope(autoComplete: true))
+ {
+ IContentType contentType = ContentTypeBuilder.CreateBasicContentType();
+ contentTypeService.Save(contentType);
+
+ ContentBuilder builder = new ContentBuilder()
+ .WithContentType(contentType);
+
+ Content root = builder.WithName("foo").Build();
+ contentService.SaveAndPublish(root);
+
+ contentItems.Add(builder.WithParent(root).WithName("bar").Build());
+ contentItems.Add(builder.WithParent(root).WithName("baz").Build());
+
+ foreach (IContent content in contentItems)
+ {
+ contentService.SaveAndPublish(content);
+ }
+ }
+
+ var queryParameters = new Dictionary
+ {
+ ["type"] = Constants.UdiEntityType.Document
+ };
+
+ var url = LinkGenerator.GetUmbracoControllerUrl("GetByIds", typeof(EntityController), queryParameters);
+
+ var udiPayload = new
+ {
+ ids = new[]
+ {
+ contentItems[0].GetUdi().ToString(),
+ contentItems[1].GetUdi().ToString(),
+ }
+ };
+
+ var intPayload = new
+ {
+ ids = new[]
+ {
+ contentItems[0].Id,
+ contentItems[1].Id,
+ }
+ };
+
+ HttpResponseMessage udiResponse = await HttpClientJsonExtensions.PostAsJsonAsync(Client, url, udiPayload);
+ HttpResponseMessage intResponse = await HttpClientJsonExtensions.PostAsJsonAsync(Client, url, intPayload);
+
+ Assert.Multiple(() =>
+ {
+ Assert.AreEqual(HttpStatusCode.OK, udiResponse.StatusCode, "First request error");
+ Assert.AreEqual(HttpStatusCode.OK, intResponse.StatusCode, "Second request error");
+ });
+ }
+ }
+}