Document and document type read API (#13853)

* Basic structure for document and document type read API

* Handle unpublished, non-variant content

* Expose content type key on ContentTypeSort

* Add the remaining properties to document type (minus list view info, still pending)

* Obsolete more ILocalizationService usage

* Add URLs and template data to document view model

* Clean up + add proprety type appearance

* update submodule commit

* front-end commit

* latest front-end commit

* latest commit

* latest front-end commit

* Rename content property to content value in view model layer

* Add contextual JSON serialization as default JSON serializer

* Add FIXME to content type sort + rearrange constructor parameters

* Fix broken remark tag

* Whitelist breakage for ContentTypeSort

* Add variance info to property type output

* Update src/Umbraco.Cms.Api.Management/Controllers/Document/ByKeyDocumentController.cs

Co-authored-by: Bjarke Berg <mail@bergmania.dk>

* Update src/Umbraco.Cms.Api.Management/Controllers/DocumentType/ByKeyDocumentTypeController.cs

Co-authored-by: Bjarke Berg <mail@bergmania.dk>

* Update src/Umbraco.Cms.Api.Management/Factories/ContentUrlFactory.cs

Co-authored-by: Bjarke Berg <mail@bergmania.dk>

* Add a few FIXME comments about async entity retrieval

---------

Co-authored-by: Niels Lyngsø <niels.lyngso@gmail.com>
Co-authored-by: Bjarke Berg <mail@bergmania.dk>
This commit is contained in:
Kenn Jacobsen
2023-02-21 13:40:41 +01:00
committed by GitHub
parent 0ce5a5bb29
commit 2eebd0558c
54 changed files with 1444 additions and 109 deletions

View File

@@ -0,0 +1,37 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.ViewModels.Document;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Management.Controllers.Document;
public class ByKeyDocumentController : DocumentControllerBase
{
private readonly IContentService _contentService;
private readonly IDocumentViewModelFactory _documentViewModelFactory;
public ByKeyDocumentController(IContentService contentService, IDocumentViewModelFactory documentViewModelFactory)
{
_contentService = contentService;
_documentViewModelFactory = documentViewModelFactory;
}
[HttpGet("{key:guid}")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(DocumentViewModel), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> ByKey(Guid key)
{
// FIXME: create and use an async get method here.
IContent? content = _contentService.GetById(key);
if (content == null)
{
return NotFound();
}
DocumentViewModel model = await _documentViewModelFactory.CreateViewModelAsync(content);
return Ok(model);
}
}

View File

@@ -0,0 +1,13 @@
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Routing;
using Umbraco.Cms.Core;
namespace Umbraco.Cms.Api.Management.Controllers.Document;
[ApiVersion("1.0")]
[ApiController]
[VersionedApiBackOfficeRoute(Constants.UdiEntityType.Document)]
[ApiExplorerSettings(GroupName = nameof(Constants.UdiEntityType.Document))]
public abstract class DocumentControllerBase : ManagementApiControllerBase
{
}

View File

@@ -0,0 +1,37 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.DocumentType;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Management.Controllers.DocumentType;
public class ByKeyDocumentTypeController : DocumentTypeControllerBase
{
private readonly IContentTypeService _contentTypeService;
private readonly IUmbracoMapper _umbracoMapper;
public ByKeyDocumentTypeController(IContentTypeService contentTypeService, IUmbracoMapper umbracoMapper)
{
_contentTypeService = contentTypeService;
_umbracoMapper = umbracoMapper;
}
[HttpGet("{key:guid}")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(DocumentTypeViewModel), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> ByKey(Guid key)
{
// FIXME: create and use an async get method here.
IContentType? contentType = _contentTypeService.Get(key);
if (contentType == null)
{
return NotFound();
}
DocumentTypeViewModel model = _umbracoMapper.Map<DocumentTypeViewModel>(contentType)!;
return await Task.FromResult(Ok(model));
}
}

View File

@@ -0,0 +1,13 @@
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Routing;
using Umbraco.Cms.Core;
namespace Umbraco.Cms.Api.Management.Controllers.DocumentType;
[ApiVersion("1.0")]
[ApiController]
[VersionedApiBackOfficeRoute(Constants.UdiEntityType.DocumentType)]
[ApiExplorerSettings(GroupName = "Document Type")]
public abstract class DocumentTypeControllerBase : ManagementApiControllerBase
{
}

View File

@@ -0,0 +1,20 @@
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.Mapping.Document;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Mapping;
namespace Umbraco.Cms.Api.Management.DependencyInjection;
internal static class DocumentBuilderExtensions
{
internal static IUmbracoBuilder AddDocuments(this IUmbracoBuilder builder)
{
builder.Services.AddTransient<IDocumentViewModelFactory, DocumentViewModelFactory>();
builder.Services.AddTransient<IContentUrlFactory, ContentUrlFactory>();
builder.WithCollectionBuilder<MapDefinitionCollectionBuilder>().Add<DocumentMapDefinition>();
return builder;
}
}

View File

@@ -0,0 +1,15 @@
using Umbraco.Cms.Api.Management.Mapping.DocumentType;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Mapping;
namespace Umbraco.Cms.Api.Management.DependencyInjection;
internal static class DocumentTypeBuilderExtensions
{
internal static IUmbracoBuilder AddDocumentTypes(this IUmbracoBuilder builder)
{
builder.WithCollectionBuilder<MapDefinitionCollectionBuilder>().Add<DocumentTypeMapDefinition>();
return builder;
}
}

View File

@@ -0,0 +1,66 @@
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Api.Management.ViewModels.Content;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Web;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Management.Factories;
public class ContentUrlFactory : IContentUrlFactory
{
private readonly IPublishedRouter _publishedRouter;
private readonly IUmbracoContextAccessor _umbracoContextAccessor;
private readonly ILanguageService _languageService;
private readonly ILocalizedTextService _localizedTextService;
private readonly IContentService _contentService;
private readonly IVariationContextAccessor _variationContextAccessor;
private readonly ILoggerFactory _loggerFactory;
private readonly UriUtility _uriUtility;
private readonly IPublishedUrlProvider _publishedUrlProvider;
public ContentUrlFactory(
IPublishedRouter publishedRouter,
IUmbracoContextAccessor umbracoContextAccessor,
ILanguageService languageService,
ILocalizedTextService localizedTextService,
IContentService contentService,
IVariationContextAccessor variationContextAccessor,
ILoggerFactory loggerFactory,
UriUtility uriUtility,
IPublishedUrlProvider publishedUrlProvider)
{
_publishedRouter = publishedRouter;
_umbracoContextAccessor = umbracoContextAccessor;
_languageService = languageService;
_localizedTextService = localizedTextService;
_contentService = contentService;
_variationContextAccessor = variationContextAccessor;
_loggerFactory = loggerFactory;
_uriUtility = uriUtility;
_publishedUrlProvider = publishedUrlProvider;
}
public async Task<IEnumerable<ContentUrlInfo>> GetUrlsAsync(IContent content)
{
IUmbracoContext umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext();
UrlInfo[] urlInfos = (await content.GetContentUrlsAsync(
_publishedRouter,
umbracoContext,
_languageService,
_localizedTextService,
_contentService,
_variationContextAccessor,
_loggerFactory.CreateLogger<IContent>(),
_uriUtility,
_publishedUrlProvider)).ToArray();
return urlInfos
.Where(urlInfo => urlInfo.IsUrl)
.Select(urlInfo => new ContentUrlInfo { Culture = urlInfo.Culture, Url = urlInfo.Text })
.ToArray();
}
}

View File

@@ -0,0 +1,36 @@
using Umbraco.Cms.Api.Management.ViewModels.Document;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Management.Factories;
public class DocumentViewModelFactory : IDocumentViewModelFactory
{
private readonly IUmbracoMapper _umbracoMapper;
private readonly IContentUrlFactory _contentUrlFactory;
private readonly IFileService _fileService;
public DocumentViewModelFactory(
IUmbracoMapper umbracoMapper,
IContentUrlFactory contentUrlFactory,
IFileService fileService)
{
_umbracoMapper = umbracoMapper;
_contentUrlFactory = contentUrlFactory;
_fileService = fileService;
}
public async Task<DocumentViewModel> CreateViewModelAsync(IContent content)
{
DocumentViewModel viewModel = _umbracoMapper.Map<DocumentViewModel>(content)!;
viewModel.Urls = await _contentUrlFactory.GetUrlsAsync(content);
viewModel.TemplateKey = content.TemplateId.HasValue
? _fileService.GetTemplate(content.TemplateId.Value)?.Key
: null;
return viewModel;
}
}

View File

@@ -0,0 +1,9 @@
using Umbraco.Cms.Api.Management.ViewModels.Content;
using Umbraco.Cms.Core.Models;
namespace Umbraco.Cms.Api.Management.Factories;
public interface IContentUrlFactory
{
Task<IEnumerable<ContentUrlInfo>> GetUrlsAsync(IContent content);
}

View File

@@ -0,0 +1,9 @@
using Umbraco.Cms.Api.Management.ViewModels.Document;
using Umbraco.Cms.Core.Models;
namespace Umbraco.Cms.Api.Management.Factories;
public interface IDocumentViewModelFactory
{
Task<DocumentViewModel> CreateViewModelAsync(IContent content);
}

View File

@@ -25,6 +25,8 @@ public class ManagementApiComposer : IComposer
.AddUpgrader()
.AddSearchManagement()
.AddTrees()
.AddDocuments()
.AddDocumentTypes()
.AddLanguages()
.AddDictionary()
.AddFileUpload()

View File

@@ -0,0 +1,71 @@
using Umbraco.Cms.Api.Management.ViewModels.Content;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Management.Mapping.Content;
public abstract class ContentMapDefinition<TContent, TValueViewModel, TVariantViewModel>
where TContent : IContentBase
where TValueViewModel : ValueViewModelBase, new()
where TVariantViewModel : VariantViewModelBase, new()
{
private readonly PropertyEditorCollection _propertyEditorCollection;
protected ContentMapDefinition(PropertyEditorCollection propertyEditorCollection) => _propertyEditorCollection = propertyEditorCollection;
protected delegate void ValueViewModelMapping(IDataEditor propertyEditor, TValueViewModel variantViewModel);
protected delegate void VariantViewModelMapping(string? culture, string? segment, TVariantViewModel variantViewModel);
protected IEnumerable<TValueViewModel> MapValueViewModels(TContent source, ValueViewModelMapping? additionalPropertyMapping = null) =>
source
.Properties
.SelectMany(property => property
.Values
.Select(propertyValue =>
{
IDataEditor? propertyEditor = _propertyEditorCollection[property.PropertyType.PropertyEditorAlias];
if (propertyEditor == null)
{
return null;
}
var variantViewModel = new TValueViewModel
{
Culture = propertyValue.Culture,
Segment = propertyValue.Segment,
Alias = property.Alias,
Value = propertyEditor.GetValueEditor().ToEditor(property, propertyValue.Culture, propertyValue.Segment)
};
additionalPropertyMapping?.Invoke(propertyEditor, variantViewModel);
return variantViewModel;
}))
.WhereNotNull()
.ToArray();
protected IEnumerable<TVariantViewModel> MapVariantViewModels(TContent source, VariantViewModelMapping? additionalVariantMapping = null)
{
IPropertyValue[] propertyValues = source.Properties.SelectMany(propertyCollection => propertyCollection.Values).ToArray();
var cultures = source.AvailableCultures.DefaultIfEmpty(null).ToArray();
var segments = propertyValues.Select(property => property.Segment).Distinct().DefaultIfEmpty(null).ToArray();
return cultures
.SelectMany(culture => segments.Select(segment =>
{
var variantViewModel = new TVariantViewModel
{
Culture = culture,
Segment = segment,
Name = source.GetCultureName(culture) ?? string.Empty,
CreateDate = source.CreateDate, // apparently there is no culture specific creation date
UpdateDate = culture == null
? source.UpdateDate
: source.GetUpdateDate(culture) ?? source.UpdateDate,
};
additionalVariantMapping?.Invoke(culture, segment, variantViewModel);
return variantViewModel;
}))
.ToArray();
}
}

View File

@@ -0,0 +1,78 @@
using Umbraco.Cms.Api.Management.ViewModels.ContentType;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Management.Mapping.ContentType;
public abstract class ContentTypeMapDefinition<TContentType, TPropertyTypeViewModel, TPropertyTypeContainerViewModel>
where TContentType : IContentTypeBase
where TPropertyTypeViewModel : PropertyTypeViewModelBase, new()
where TPropertyTypeContainerViewModel : PropertyTypeContainerViewModelBase, new()
{
protected IEnumerable<TPropertyTypeViewModel> MapPropertyTypes(TContentType source)
{
// create a mapping table between properties and their associated groups
var groupKeysByPropertyKeys = source
.PropertyGroups
.SelectMany(propertyGroup => (propertyGroup.PropertyTypes?.ToArray() ?? Array.Empty<PropertyType>())
.Select(propertyType => new { GroupKey = propertyGroup.Key, PropertyTypeKey = propertyType.Key }))
.ToDictionary(map => map.PropertyTypeKey, map => map.GroupKey);
return source.PropertyTypes.Select(propertyType =>
new TPropertyTypeViewModel
{
Key = propertyType.Key,
ContainerKey = groupKeysByPropertyKeys.ContainsKey(propertyType.Key)
? groupKeysByPropertyKeys[propertyType.Key]
: null,
Name = propertyType.Name,
Alias = propertyType.Alias,
Description = propertyType.Description,
DataTypeKey = propertyType.DataTypeKey,
VariesByCulture = propertyType.VariesByCulture(),
VariesBySegment = propertyType.VariesBySegment(),
Validation = new PropertyTypeValidation
{
Mandatory = propertyType.Mandatory,
MandatoryMessage = propertyType.MandatoryMessage,
RegEx = propertyType.ValidationRegExp,
RegExMessage = propertyType.ValidationRegExpMessage
},
Appearance = new PropertyTypeAppearance
{
LabelOnTop = propertyType.LabelOnTop
}
})
.ToArray();
}
protected IEnumerable<TPropertyTypeContainerViewModel> MapPropertyTypeContainers(TContentType source)
{
// create a mapping table between property group aliases and keys
var groupKeysByGroupAliases = source
.PropertyGroups
.ToDictionary(propertyGroup => propertyGroup.Alias, propertyGroup => propertyGroup.Key);
Guid? ParentGroupKey(PropertyGroup group)
{
var path = group.Alias.Split(Constants.CharArrays.ForwardSlash);
return path.Length == 1 || groupKeysByGroupAliases.TryGetValue(path.First(), out Guid parentGroupKey) == false
? null
: parentGroupKey;
}
return source
.PropertyGroups
.Select(propertyGroup =>
new TPropertyTypeContainerViewModel
{
Key = propertyGroup.Key,
ParentKey = ParentGroupKey(propertyGroup),
Type = propertyGroup.Type.ToString(),
SortOrder = propertyGroup.SortOrder,
Name = propertyGroup.Name,
})
.ToArray();
}
}

View File

@@ -0,0 +1,57 @@
using Umbraco.Cms.Api.Management.Mapping.Content;
using Umbraco.Cms.Api.Management.ViewModels.Content;
using Umbraco.Cms.Api.Management.ViewModels.Document;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.PropertyEditors;
namespace Umbraco.Cms.Api.Management.Mapping.Document;
public class DocumentMapDefinition : ContentMapDefinition<IContent, DocumentValueViewModel, DocumentVariantViewModel>, IMapDefinition
{
public DocumentMapDefinition(PropertyEditorCollection propertyEditorCollection)
: base(propertyEditorCollection)
{
}
public void DefineMaps(IUmbracoMapper mapper)
=> mapper.Define<IContent, DocumentViewModel>((_, _) => new DocumentViewModel(), Map);
// Umbraco.Code.MapAll -Urls -TemplateKey
private void Map(IContent source, DocumentViewModel target, MapperContext context)
{
target.Key = source.Key;
target.ContentTypeKey = source.ContentType.Key;
target.Values = MapValueViewModels(source);
target.Variants = MapVariantViewModels(
source,
(culture, _, documentVariantViewModel) =>
{
documentVariantViewModel.State = GetSavedState(source, culture);
documentVariantViewModel.PublishDate = culture == null
? source.PublishDate
: source.GetPublishDate(culture);
});
}
private ContentState GetSavedState(IContent content, string? culture)
{
if (content.Id <= 0 || (culture != null && content.IsCultureAvailable(culture) == false))
{
return ContentState.NotCreated;
}
var isDraft = content.PublishedState == PublishedState.Unpublished ||
(culture != null && content.IsCulturePublished(culture) == false);
if (isDraft)
{
return ContentState.Draft;
}
var isEdited = culture != null
? content.IsCultureEdited(culture)
: content.Edited;
return isEdited ? ContentState.PublishedPendingChanges : ContentState.Published;
}
}

View File

@@ -0,0 +1,62 @@
using Umbraco.Cms.Api.Management.Mapping.ContentType;
using Umbraco.Cms.Api.Management.ViewModels.ContentType;
using Umbraco.Cms.Api.Management.ViewModels.DocumentType;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Extensions;
using ContentTypeSort = Umbraco.Cms.Api.Management.ViewModels.ContentType.ContentTypeSort;
namespace Umbraco.Cms.Api.Management.Mapping.DocumentType;
public class DocumentTypeMapDefinition : ContentTypeMapDefinition<IContentType, DocumentTypePropertyTypeViewModel, DocumentTypePropertyTypeContainerViewModel>, IMapDefinition
{
public void DefineMaps(IUmbracoMapper mapper)
=> mapper.Define<IContentType, DocumentTypeViewModel>((_, _) => new DocumentTypeViewModel(), Map);
// Umbraco.Code.MapAll
private void Map(IContentType source, DocumentTypeViewModel target, MapperContext context)
{
target.Key = source.Key;
target.Alias = source.Alias;
target.Name = source.Name ?? string.Empty;
target.Description = source.Description;
target.Icon = source.Icon ?? string.Empty;
target.AllowedAsRoot = source.AllowedAsRoot;
target.VariesByCulture = source.VariesByCulture();
target.VariesBySegment = source.VariesBySegment();
target.IsElement = source.IsElement;
target.Containers = MapPropertyTypeContainers(source);
target.Properties = MapPropertyTypes(source);
if (source.AllowedContentTypes != null)
{
target.AllowedContentTypes = source.AllowedContentTypes.Select(contentTypeSort
=> new ContentTypeSort { Key = contentTypeSort.Key, SortOrder = contentTypeSort.SortOrder });
}
if (source.AllowedTemplates != null)
{
target.AllowedTemplateKeys = source.AllowedTemplates.Select(template => template.Key);
}
target.DefaultTemplateKey = source.DefaultTemplate?.Key;
if (source.HistoryCleanup != null)
{
target.Cleanup = new ContentTypeCleanup
{
PreventCleanup = source.HistoryCleanup.PreventCleanup,
KeepAllVersionsNewerThanDays = source.HistoryCleanup.KeepAllVersionsNewerThanDays,
KeepLatestVersionPerDayForDays = source.HistoryCleanup.KeepLatestVersionPerDayForDays
};
}
target.Compositions = source.ContentTypeComposition.Select(contentType => new ContentTypeComposition
{
Key = contentType.Key,
CompositionType = contentType.Id == source.ParentId
? ContentTypeCompositionType.Inheritance
: ContentTypeCompositionType.Composition
}).ToArray();
}
}

View File

@@ -1241,6 +1241,44 @@
}
}
},
"/umbraco/management/api/v1/document-type/{key}": {
"get": {
"tags": [
"Document Type"
],
"operationId": "GetDocumentTypeByKey",
"parameters": [
{
"name": "key",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"oneOf": [
{
"$ref": "#/components/schemas/DocumentTypeModel"
}
]
}
}
}
},
"404": {
"description": "Not Found"
}
}
}
},
"/umbraco/management/api/v1/tree/document-type/children": {
"get": {
"tags": [
@@ -1385,6 +1423,44 @@
}
}
},
"/umbraco/management/api/v1/document/{key}": {
"get": {
"tags": [
"Document"
],
"operationId": "GetDocumentByKey",
"parameters": [
{
"name": "key",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"oneOf": [
{
"$ref": "#/components/schemas/DocumentModel"
}
]
}
}
}
},
"404": {
"description": "Not Found"
}
}
}
},
"/umbraco/management/api/v1/recycle-bin/document/children": {
"get": {
"tags": [
@@ -5465,6 +5541,16 @@
},
"additionalProperties": false
},
"ContentStateModel": {
"enum": [
"NotCreated",
"Draft",
"Published",
"PublishedPendingChanges"
],
"type": "integer",
"format": "int32"
},
"ContentTreeItemModel": {
"required": [
"$type"
@@ -5495,6 +5581,189 @@
}
}
},
"ContentTypeCleanupModel": {
"type": "object",
"properties": {
"preventCleanup": {
"type": "boolean"
},
"keepAllVersionsNewerThanDays": {
"type": "integer",
"format": "int32",
"nullable": true
},
"keepLatestVersionPerDayForDays": {
"type": "integer",
"format": "int32",
"nullable": true
}
},
"additionalProperties": false
},
"ContentTypeCompositionModel": {
"type": "object",
"properties": {
"key": {
"type": "string",
"format": "uuid"
},
"compositionType": {
"$ref": "#/components/schemas/ContentTypeCompositionTypeModel"
}
},
"additionalProperties": false
},
"ContentTypeCompositionTypeModel": {
"enum": [
"Composition",
"Inheritance"
],
"type": "integer",
"format": "int32"
},
"ContentTypeSortModel": {
"type": "object",
"properties": {
"key": {
"type": "string",
"format": "uuid"
},
"sortOrder": {
"type": "integer",
"format": "int32"
}
},
"additionalProperties": false
},
"ContentTypeViewModelBaseDocumentTypePropertyTypeDocumentTypePropertyTypeContainerModel": {
"type": "object",
"properties": {
"key": {
"type": "string",
"format": "uuid"
},
"alias": {
"type": "string"
},
"name": {
"type": "string"
},
"description": {
"type": "string",
"nullable": true
},
"icon": {
"type": "string"
},
"allowedAsRoot": {
"type": "boolean"
},
"variesByCulture": {
"type": "boolean"
},
"variesBySegment": {
"type": "boolean"
},
"isElement": {
"type": "boolean"
},
"properties": {
"type": "array",
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/DocumentTypePropertyTypeModel"
}
]
}
},
"containers": {
"type": "array",
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/DocumentTypePropertyTypeContainerModel"
}
]
}
},
"allowedContentTypes": {
"type": "array",
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/ContentTypeSortModel"
}
]
}
},
"compositions": {
"type": "array",
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/ContentTypeCompositionModel"
}
]
}
},
"cleanup": {
"oneOf": [
{
"$ref": "#/components/schemas/ContentTypeCleanupModel"
}
]
}
},
"additionalProperties": false
},
"ContentUrlInfoModel": {
"type": "object",
"properties": {
"culture": {
"type": "string",
"nullable": true
},
"url": {
"type": "string"
}
},
"additionalProperties": false
},
"ContentViewModelBaseDocumentValueDocumentVariantModel": {
"type": "object",
"properties": {
"key": {
"type": "string",
"format": "uuid"
},
"contentTypeKey": {
"type": "string",
"format": "uuid"
},
"values": {
"type": "array",
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/DocumentValueModel"
}
]
}
},
"variants": {
"type": "array",
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/DocumentVariantModel"
}
]
}
}
},
"additionalProperties": false
},
"CultureModel": {
"type": "object",
"properties": {
@@ -5955,6 +6224,32 @@
}
}
},
"DocumentModel": {
"type": "object",
"allOf": [
{
"$ref": "#/components/schemas/ContentViewModelBaseDocumentValueDocumentVariantModel"
}
],
"properties": {
"urls": {
"type": "array",
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/ContentUrlInfoModel"
}
]
}
},
"templateKey": {
"type": "string",
"format": "uuid",
"nullable": true
}
},
"additionalProperties": false
},
"DocumentTreeItemModel": {
"required": [
"$type"
@@ -5987,6 +6282,47 @@
}
}
},
"DocumentTypeModel": {
"type": "object",
"allOf": [
{
"$ref": "#/components/schemas/ContentTypeViewModelBaseDocumentTypePropertyTypeDocumentTypePropertyTypeContainerModel"
}
],
"properties": {
"allowedTemplateKeys": {
"type": "array",
"items": {
"type": "string",
"format": "uuid"
}
},
"defaultTemplateKey": {
"type": "string",
"format": "uuid",
"nullable": true
}
},
"additionalProperties": false
},
"DocumentTypePropertyTypeContainerModel": {
"type": "object",
"allOf": [
{
"$ref": "#/components/schemas/PropertyTypeContainerViewModelBaseModel"
}
],
"additionalProperties": false
},
"DocumentTypePropertyTypeModel": {
"type": "object",
"allOf": [
{
"$ref": "#/components/schemas/PropertyTypeViewModelBaseModel"
}
],
"additionalProperties": false
},
"DocumentTypeTreeItemModel": {
"required": [
"$type"
@@ -6013,6 +6349,34 @@
}
}
},
"DocumentValueModel": {
"type": "object",
"allOf": [
{
"$ref": "#/components/schemas/ValueViewModelBaseModel"
}
],
"additionalProperties": false
},
"DocumentVariantModel": {
"type": "object",
"allOf": [
{
"$ref": "#/components/schemas/VariantViewModelBaseModel"
}
],
"properties": {
"state": {
"$ref": "#/components/schemas/ContentStateModel"
},
"publishDate": {
"type": "string",
"format": "date-time",
"nullable": true
}
},
"additionalProperties": false
},
"EntityTreeItemModel": {
"required": [
"$type"
@@ -7345,6 +7709,111 @@
},
"additionalProperties": false
},
"PropertyTypeAppearanceModel": {
"type": "object",
"properties": {
"labelOnTop": {
"type": "boolean"
}
},
"additionalProperties": false
},
"PropertyTypeContainerViewModelBaseModel": {
"type": "object",
"properties": {
"key": {
"type": "string",
"format": "uuid"
},
"parentKey": {
"type": "string",
"format": "uuid",
"nullable": true
},
"name": {
"type": "string",
"nullable": true
},
"type": {
"type": "string"
},
"sortOrder": {
"type": "integer",
"format": "int32"
}
},
"additionalProperties": false
},
"PropertyTypeValidationModel": {
"type": "object",
"properties": {
"mandatory": {
"type": "boolean"
},
"mandatoryMessage": {
"type": "string",
"nullable": true
},
"regEx": {
"type": "string",
"nullable": true
},
"regExMessage": {
"type": "string",
"nullable": true
}
},
"additionalProperties": false
},
"PropertyTypeViewModelBaseModel": {
"type": "object",
"properties": {
"key": {
"type": "string",
"format": "uuid"
},
"containerKey": {
"type": "string",
"format": "uuid",
"nullable": true
},
"alias": {
"type": "string"
},
"name": {
"type": "string"
},
"description": {
"type": "string",
"nullable": true
},
"dataTypeKey": {
"type": "string",
"format": "uuid"
},
"variesByCulture": {
"type": "boolean"
},
"variesBySegment": {
"type": "boolean"
},
"validation": {
"oneOf": [
{
"$ref": "#/components/schemas/PropertyTypeValidationModel"
}
]
},
"appearance": {
"oneOf": [
{
"$ref": "#/components/schemas/PropertyTypeAppearanceModel"
}
]
}
},
"additionalProperties": false
},
"RecycleBinItemModel": {
"required": [
"$type"
@@ -8029,6 +8498,51 @@
},
"additionalProperties": false
},
"ValueViewModelBaseModel": {
"type": "object",
"properties": {
"culture": {
"type": "string",
"nullable": true
},
"segment": {
"type": "string",
"nullable": true
},
"alias": {
"type": "string"
},
"value": {
"nullable": true
}
},
"additionalProperties": false
},
"VariantViewModelBaseModel": {
"type": "object",
"properties": {
"culture": {
"type": "string",
"nullable": true
},
"segment": {
"type": "string",
"nullable": true
},
"name": {
"type": "string"
},
"createDate": {
"type": "string",
"format": "date-time"
},
"updateDate": {
"type": "string",
"format": "date-time"
}
},
"additionalProperties": false
},
"VersionModel": {
"type": "object",
"properties": {

View File

@@ -0,0 +1,27 @@
namespace Umbraco.Cms.Api.Management.ViewModels.Content;
/// <summary>
/// The saved state of a content item
/// </summary>
public enum ContentState
{
/// <summary>
/// The item isn't created yet
/// </summary>
NotCreated = 1,
/// <summary>
/// The item is saved but isn't published
/// </summary>
Draft = 2,
/// <summary>
/// The item is published and there are no pending changes
/// </summary>
Published = 3,
/// <summary>
/// The item is published and there are pending changes
/// </summary>
PublishedPendingChanges = 4,
}

View File

@@ -0,0 +1,8 @@
namespace Umbraco.Cms.Api.Management.ViewModels.Content;
public class ContentUrlInfo
{
public required string? Culture { get; init; }
public required string Url { get; init; }
}

View File

@@ -0,0 +1,14 @@
namespace Umbraco.Cms.Api.Management.ViewModels.Content;
public abstract class ContentViewModelBase<TValueViewModelBase, TVariantViewModel>
where TValueViewModelBase : ValueViewModelBase
where TVariantViewModel : VariantViewModelBase
{
public Guid Key { get; set; }
public Guid ContentTypeKey { get; set; }
public IEnumerable<TValueViewModelBase> Values { get; set; } = Array.Empty<TValueViewModelBase>();
public IEnumerable<TVariantViewModel> Variants { get; set; } = Array.Empty<TVariantViewModel>();
}

View File

@@ -0,0 +1,12 @@
namespace Umbraco.Cms.Api.Management.ViewModels.Content;
public abstract class ValueViewModelBase
{
public string? Culture { get; set; }
public string? Segment { get; set; }
public string Alias { get; set; } = string.Empty;
public object? Value { get; set; }
}

View File

@@ -0,0 +1,14 @@
namespace Umbraco.Cms.Api.Management.ViewModels.Content;
public abstract class VariantViewModelBase
{
public string? Culture { get; set; }
public string? Segment { get; set; }
public string Name { get; set; } = string.Empty;
public DateTime CreateDate { get; set; }
public DateTime UpdateDate { get; set; }
}

View File

@@ -0,0 +1,10 @@
namespace Umbraco.Cms.Api.Management.ViewModels.ContentType;
public class ContentTypeCleanup
{
public bool PreventCleanup { get; init; }
public int? KeepAllVersionsNewerThanDays { get; init; }
public int? KeepLatestVersionPerDayForDays { get; init; }
}

View File

@@ -0,0 +1,8 @@
namespace Umbraco.Cms.Api.Management.ViewModels.ContentType;
public class ContentTypeComposition
{
public required Guid Key { get; init; }
public required ContentTypeCompositionType CompositionType { get; init; }
}

View File

@@ -0,0 +1,7 @@
namespace Umbraco.Cms.Api.Management.ViewModels.ContentType;
public enum ContentTypeCompositionType
{
Composition,
Inheritance
}

View File

@@ -0,0 +1,8 @@
namespace Umbraco.Cms.Api.Management.ViewModels.ContentType;
public class ContentTypeSort
{
public required Guid Key { get; init; }
public required int SortOrder { get; init; }
}

View File

@@ -0,0 +1,34 @@
namespace Umbraco.Cms.Api.Management.ViewModels.ContentType;
public abstract class ContentTypeViewModelBase<TPropertyType, TPropertyTypeContainer>
where TPropertyType : PropertyTypeViewModelBase
where TPropertyTypeContainer : PropertyTypeContainerViewModelBase
{
public Guid Key { get; set; }
public string Alias { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
public string Icon { get; set; } = string.Empty;
public bool AllowedAsRoot { get; set; }
public bool VariesByCulture { get; set; }
public bool VariesBySegment { get; set; }
public bool IsElement { get; set; }
public IEnumerable<TPropertyType> Properties { get; set; } = Array.Empty<TPropertyType>();
public IEnumerable<TPropertyTypeContainer> Containers { get; set; } = Array.Empty<TPropertyTypeContainer>();
public IEnumerable<ContentTypeSort> AllowedContentTypes { get; set; } = Array.Empty<ContentTypeSort>();
public IEnumerable<ContentTypeComposition> Compositions { get; set; } = Array.Empty<ContentTypeComposition>();
public ContentTypeCleanup Cleanup { get; set; } = new();
}

View File

@@ -0,0 +1,6 @@
namespace Umbraco.Cms.Api.Management.ViewModels.ContentType;
public class PropertyTypeAppearance
{
public bool LabelOnTop { get; set; }
}

View File

@@ -0,0 +1,15 @@
namespace Umbraco.Cms.Api.Management.ViewModels.ContentType;
public abstract class PropertyTypeContainerViewModelBase
{
public Guid Key { get; set; }
public Guid? ParentKey { get; set; }
public string? Name { get; set; }
// NOTE: This needs to be a string because it can be anything in the future (= not necessarily limited to "tab" or "group")
public string Type { get; set; } = string.Empty;
public int SortOrder { get; set; }
}

View File

@@ -0,0 +1,12 @@
namespace Umbraco.Cms.Api.Management.ViewModels.ContentType;
public class PropertyTypeValidation
{
public bool Mandatory { get; set; }
public string? MandatoryMessage { get; set; }
public string? RegEx { get; set; }
public string? RegExMessage { get; set; }
}

View File

@@ -0,0 +1,24 @@
namespace Umbraco.Cms.Api.Management.ViewModels.ContentType;
public abstract class PropertyTypeViewModelBase
{
public Guid Key { get; set; }
public Guid? ContainerKey { get; set; }
public string Alias { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
public Guid DataTypeKey { get; set; }
public bool VariesByCulture { get; set; }
public bool VariesBySegment { get; set; }
public PropertyTypeValidation Validation { get; set; } = new();
public PropertyTypeAppearance Appearance { get; set; } = new();
}

View File

@@ -0,0 +1,7 @@
using Umbraco.Cms.Api.Management.ViewModels.Content;
namespace Umbraco.Cms.Api.Management.ViewModels.Document;
public class DocumentValueViewModel : ValueViewModelBase
{
}

View File

@@ -0,0 +1,10 @@
using Umbraco.Cms.Api.Management.ViewModels.Content;
namespace Umbraco.Cms.Api.Management.ViewModels.Document;
public class DocumentVariantViewModel : VariantViewModelBase
{
public ContentState State { get; set; }
public DateTime? PublishDate { get; set; }
}

View File

@@ -0,0 +1,10 @@
using Umbraco.Cms.Api.Management.ViewModels.Content;
namespace Umbraco.Cms.Api.Management.ViewModels.Document;
public class DocumentViewModel : ContentViewModelBase<DocumentValueViewModel, DocumentVariantViewModel>
{
public IEnumerable<ContentUrlInfo> Urls { get; set; } = Array.Empty<ContentUrlInfo>();
public Guid? TemplateKey { get; set; }
}

View File

@@ -0,0 +1,7 @@
using Umbraco.Cms.Api.Management.ViewModels.ContentType;
namespace Umbraco.Cms.Api.Management.ViewModels.DocumentType;
public class DocumentTypePropertyTypeContainerViewModel : PropertyTypeContainerViewModelBase
{
}

View File

@@ -0,0 +1,7 @@
using Umbraco.Cms.Api.Management.ViewModels.ContentType;
namespace Umbraco.Cms.Api.Management.ViewModels.DocumentType;
public class DocumentTypePropertyTypeViewModel : PropertyTypeViewModelBase
{
}

View File

@@ -0,0 +1,10 @@
using Umbraco.Cms.Api.Management.ViewModels.ContentType;
namespace Umbraco.Cms.Api.Management.ViewModels.DocumentType;
public class DocumentTypeViewModel : ContentTypeViewModelBase<DocumentTypePropertyTypeViewModel, DocumentTypePropertyTypeContainerViewModel>
{
public IEnumerable<Guid> AllowedTemplateKeys { get; set; } = Array.Empty<Guid>();
public Guid? DefaultTemplateKey { get; set; }
}

View File

@@ -56,6 +56,13 @@
<Right>lib/net7.0/Umbraco.Core.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Umbraco.Cms.Core.Models.ContentTypeSort.#ctor(System.Lazy{System.Int32},System.Int32,System.String)</Target>
<Left>lib/net7.0/Umbraco.Core.dll</Left>
<Right>lib/net7.0/Umbraco.Core.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Umbraco.Cms.Core.Models.DataType.get_Configuration</Target>

View File

@@ -21,16 +21,19 @@ public class ContentTypeSort : IValueObject, IDeepCloneable
SortOrder = sortOrder;
}
public ContentTypeSort(Lazy<int> id, int sortOrder, string alias)
// FIXME: remove integer ID in constructor
public ContentTypeSort(Lazy<int> id, Guid key, int sortOrder, string alias)
{
Id = id;
SortOrder = sortOrder;
Alias = alias;
Key = key;
}
/// <summary>
/// Gets or sets the Id of the ContentType
/// </summary>
// FIXME: remove this in favor of Key (Id should only be used at repository level)
public Lazy<int> Id { get; set; } = new(() => 0);
/// <summary>
@@ -43,6 +46,11 @@ public class ContentTypeSort : IValueObject, IDeepCloneable
/// </summary>
public string Alias { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the unique Key of the ContentType
/// </summary>
public Guid Key { get; set; }
public object DeepClone()
{
var clone = (ContentTypeSort)MemberwiseClone();

View File

@@ -1,5 +1,7 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.Routing;
@@ -10,13 +12,7 @@ namespace Umbraco.Extensions;
public static class UrlProviderExtensions
{
/// <summary>
/// Gets the URLs of the content item.
/// </summary>
/// <remarks>
/// <para>Use when displaying URLs. If errors occur when generating the URLs, they will show in the list.</para>
/// <para>Contains all the URLs that we can figure out (based upon domains, etc).</para>
/// </remarks>
[Obsolete("Use GetContentUrlsAsync that takes ILanguageService instead of ILocalizationService. Will be removed in V15.")]
public static async Task<IEnumerable<UrlInfo>> GetContentUrlsAsync(
this IContent content,
IPublishedRouter publishedRouter,
@@ -28,11 +24,40 @@ public static class UrlProviderExtensions
ILogger<IContent> logger,
UriUtility uriUtility,
IPublishedUrlProvider publishedUrlProvider)
=> await content.GetContentUrlsAsync(
publishedRouter,
umbracoContext,
StaticServiceProvider.Instance.GetRequiredService<ILanguageService>(),
textService,
contentService,
variationContextAccessor,
logger,
uriUtility,
publishedUrlProvider);
/// <summary>
/// Gets the URLs of the content item.
/// </summary>
/// <remarks>
/// <para>Use when displaying URLs. If errors occur when generating the URLs, they will show in the list.</para>
/// <para>Contains all the URLs that we can figure out (based upon domains, etc).</para>
/// </remarks>
public static async Task<IEnumerable<UrlInfo>> GetContentUrlsAsync(
this IContent content,
IPublishedRouter publishedRouter,
IUmbracoContext umbracoContext,
ILanguageService languageService,
ILocalizedTextService textService,
IContentService contentService,
IVariationContextAccessor variationContextAccessor,
ILogger<IContent> logger,
UriUtility uriUtility,
IPublishedUrlProvider publishedUrlProvider)
{
ArgumentNullException.ThrowIfNull(content);
ArgumentNullException.ThrowIfNull(publishedRouter);
ArgumentNullException.ThrowIfNull(umbracoContext);
ArgumentNullException.ThrowIfNull(localizationService);
ArgumentNullException.ThrowIfNull(languageService);
ArgumentNullException.ThrowIfNull(textService);
ArgumentNullException.ThrowIfNull(contentService);
ArgumentNullException.ThrowIfNull(variationContextAccessor);
@@ -60,7 +85,7 @@ public static class UrlProviderExtensions
// and, not only for those assigned to domains in the branch, because we want
// to show what GetUrl() would return, for every culture.
var urls = new HashSet<UrlInfo>();
var cultures = localizationService.GetAllLanguages().Select(x => x.IsoCode).ToList();
var cultures = (await languageService.GetAllAsync()).Select(x => x.IsoCode).ToList();
// get all URLs for all cultures
// in a HashSet, so de-duplicates too

View File

@@ -117,7 +117,7 @@ public static partial class UmbracoBuilderExtensions
builder.Services.AddScoped<IHttpScopeReference, HttpScopeReference>();
builder.Services.AddSingleton<IJsonSerializer, JsonNetSerializer>();
builder.Services.AddSingleton<IJsonSerializer, ContextualJsonSerializer>();
builder.Services.AddSingleton<IConfigurationEditorJsonSerializer, ContextualConfigurationEditorJsonSerializer>();
builder.Services.AddSingleton<IMenuItemCollectionFactory, MenuItemCollectionFactory>();

View File

@@ -129,6 +129,7 @@ public class EntityMapDefinition : IMapDefinition
private static void Map(EntityBasic source, ContentTypeSort target, MapperContext context)
{
target.Alias = source.Alias;
target.Key = source.Key;
target.Id = new Lazy<int>(() => Convert.ToInt32(source.Id));
}

View File

@@ -1176,8 +1176,7 @@ namespace Umbraco.Cms.Infrastructure.Packaging
continue;
}
allowedChildren?.Add(new ContentTypeSort(new Lazy<int>(() => allowedChild.Id), sortOrder,
allowedChild.Alias));
allowedChildren?.Add(new ContentTypeSort(new Lazy<int>(() => allowedChild.Id), allowedChild.Key, sortOrder, allowedChild.Alias));
sortOrder++;
}

View File

@@ -88,6 +88,7 @@ internal class ContentTypeCommonRepository : IContentTypeCommonRepository
// prepare
// note: same alias could be used for media, content... but always different ids = ok
var aliases = contentTypeDtos.ToDictionary(x => x.NodeId, x => x.Alias);
var keys = contentTypeDtos.ToDictionary(x => x.NodeId, x => x.NodeDto.UniqueId);
// create
var allowedDtoIx = 0;
@@ -120,14 +121,17 @@ internal class ContentTypeCommonRepository : IContentTypeCommonRepository
while (allowedDtoIx < allowedDtos?.Count && allowedDtos[allowedDtoIx].Id == contentTypeDto.NodeId)
{
ContentTypeAllowedContentTypeDto allowedDto = allowedDtos[allowedDtoIx];
if (!aliases.TryGetValue(allowedDto.AllowedId, out var alias))
if (!aliases.TryGetValue(allowedDto.AllowedId, out var alias)
|| !keys.TryGetValue(allowedDto.AllowedId, out Guid key))
{
continue;
}
allowedContentTypes.Add(new ContentTypeSort(
new Lazy<int>(() => allowedDto.AllowedId),
allowedDto.SortOrder, alias!));
key,
allowedDto.SortOrder,
alias!));
allowedDtoIx++;
}

View File

@@ -0,0 +1,49 @@
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Web;
namespace Umbraco.Cms.Infrastructure.Serialization;
// FIXME: move away from Json.NET; this is a temporary fix that attempts to use System.Text.Json for management API operations, Json.NET for other operations
public class ContextualJsonSerializer : IJsonSerializer
{
private readonly IRequestAccessor _requestAccessor;
private readonly IJsonSerializer _jsonNetSerializer;
private readonly IJsonSerializer _systemTextSerializer;
public ContextualJsonSerializer(IRequestAccessor requestAccessor)
{
_requestAccessor = requestAccessor;
_jsonNetSerializer = new JsonNetSerializer();
_systemTextSerializer = new SystemTextJsonSerializer();
}
public string Serialize(object? input) => ContextualizedSerializer().Serialize(input);
public T? Deserialize<T>(string input) => ContextualizedSerializer().Deserialize<T>(input);
public T? DeserializeSubset<T>(string input, string key) => throw new NotSupportedException();
private IJsonSerializer ContextualizedSerializer()
{
try
{
var requestedPath = _requestAccessor.GetRequestUrl()?.AbsolutePath;
if (requestedPath != null)
{
// add white listed paths for the System.Text.Json config serializer here
// - always use it for the new management API
if (requestedPath.Contains("/umbraco/management/api/"))
{
return _systemTextSerializer;
}
}
}
catch (Exception ex)
{
// ignore - this whole thing is a temporary workaround, let's not make a fuss
}
return _jsonNetSerializer;
}
}

View File

@@ -1,10 +1,10 @@
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using Umbraco.Cms.Core.Serialization;
namespace Umbraco.Cms.Infrastructure.Serialization;
// TODO: clean up all config editor serializers when we can migrate fully to System.Text.Json
// FIXME: clean up all config editor serializers when we can migrate fully to System.Text.Json
// - move this implementation to ConfigurationEditorJsonSerializer (delete the old implementation)
// - use this implementation as the registered singleton (delete ContextualConfigurationEditorJsonSerializer)
// - reuse the JsonObjectConverter implementation from management API (delete the local implementation - pending V12 branch update)
@@ -21,9 +21,9 @@ public class SystemTextConfigurationEditorJsonSerializer : IConfigurationEditorJ
// in some cases, configs aren't camel cased in the DB, so we have to resort to case insensitive
// property name resolving when creating configuration objects (deserializing DB configs)
PropertyNameCaseInsensitive = true,
NumberHandling = System.Text.Json.Serialization.JsonNumberHandling.AllowReadingFromString
NumberHandling = JsonNumberHandling.AllowReadingFromString
};
_jsonSerializerOptions.Converters.Add(new System.Text.Json.Serialization.JsonStringEnumConverter());
_jsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
_jsonSerializerOptions.Converters.Add(new JsonObjectConverter());
}
@@ -32,73 +32,4 @@ public class SystemTextConfigurationEditorJsonSerializer : IConfigurationEditorJ
public T? Deserialize<T>(string input) => JsonSerializer.Deserialize<T>(input, _jsonSerializerOptions);
public T? DeserializeSubset<T>(string input, string key) => throw new NotSupportedException();
// TODO: reuse the JsonObjectConverter implementation from management API
private class JsonObjectConverter : System.Text.Json.Serialization.JsonConverter<object>
{
public override object Read(
ref Utf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options) =>
ParseObject(ref reader);
public override void Write(
Utf8JsonWriter writer,
object objectToWrite,
JsonSerializerOptions options)
{
if (objectToWrite is null)
{
return;
}
// If an object is equals "new object()", Json.Serialize would recurse forever and cause a stack overflow
// We have no good way of checking if its an empty object
// which is why we try to check if the object has any properties, and thus will be empty.
if (objectToWrite.GetType().Name is "Object" && !objectToWrite.GetType().GetProperties().Any())
{
writer.WriteStartObject();
writer.WriteEndObject();
}
else
{
JsonSerializer.Serialize(writer, objectToWrite, objectToWrite.GetType(), options);
}
}
private object ParseObject(ref Utf8JsonReader reader)
{
if (reader.TokenType == JsonTokenType.StartArray)
{
var items = new List<object>();
while (reader.Read() && reader.TokenType != JsonTokenType.EndArray)
{
items.Add(ParseObject(ref reader));
}
return items.ToArray();
}
if (reader.TokenType == JsonTokenType.StartObject)
{
var jsonNode = JsonNode.Parse(ref reader);
if (jsonNode is JsonObject jsonObject)
{
return jsonObject;
}
}
return reader.TokenType switch
{
JsonTokenType.True => true,
JsonTokenType.False => false,
JsonTokenType.Number when reader.TryGetInt32(out int i) => i,
JsonTokenType.Number when reader.TryGetInt64(out long l) => l,
JsonTokenType.Number => reader.GetDouble(),
JsonTokenType.String when reader.TryGetDateTime(out DateTime datetime) => datetime,
JsonTokenType.String => reader.GetString()!,
_ => JsonDocument.ParseValue(ref reader).RootElement.Clone()
};
}
}
}

View File

@@ -0,0 +1,24 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Umbraco.Cms.Core.Serialization;
namespace Umbraco.Cms.Infrastructure.Serialization;
public class SystemTextJsonSerializer : IJsonSerializer
{
private readonly JsonSerializerOptions _jsonSerializerOptions;
public SystemTextJsonSerializer()
{
_jsonSerializerOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
_jsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
// we may need to add JsonObjectConverter at some point, but for the time being things work fine without
// _jsonSerializerOptions.Converters.Add(new JsonObjectConverter());
}
public string Serialize(object? input) => JsonSerializer.Serialize(input, _jsonSerializerOptions);
public T? Deserialize<T>(string input) => JsonSerializer.Deserialize<T>(input, _jsonSerializerOptions);
public T? DeserializeSubset<T>(string input, string key) => throw new NotSupportedException();
}

View File

@@ -217,7 +217,7 @@ public class LoadTestController : Controller
};
containerType.AllowedContentTypes = containerType.AllowedContentTypes.Union(new[]
{
new ContentTypeSort(new Lazy<int>(() => contentType.Id), 0, contentType.Alias)
new ContentTypeSort(new Lazy<int>(() => contentType.Id), contentType.Key, 0, contentType.Alias)
});
containerType.AllowedTemplates = containerType.AllowedTemplates.Union(new[] { containerTemplate });
containerType.SetDefaultTemplate(containerTemplate);

View File

@@ -51,7 +51,8 @@ public class ContentTypeSortBuilder
var id = _id ?? 1;
var alias = _alias ?? Guid.NewGuid().ToString().ToCamelCase();
var sortOrder = _sortOrder ?? 0;
var key = Guid.NewGuid();
return new ContentTypeSort(new Lazy<int>(() => id), sortOrder, alias);
return new ContentTypeSort(new Lazy<int>(() => id), key, sortOrder, alias);
}
}

View File

@@ -809,8 +809,8 @@ public class ContentTypeRepositoryTest : UmbracoIntegrationTest
var contentType = repository.Get(_simpleContentType.Id);
contentType.AllowedContentTypes = new List<ContentTypeSort>
{
new(new Lazy<int>(() => subpageContentType.Id), 0, subpageContentType.Alias),
new(new Lazy<int>(() => simpleSubpageContentType.Id), 1, simpleSubpageContentType.Alias)
new(new Lazy<int>(() => subpageContentType.Id), subpageContentType.Key, 0, subpageContentType.Alias),
new(new Lazy<int>(() => simpleSubpageContentType.Id), simpleSubpageContentType.Key, 1, simpleSubpageContentType.Alias)
};
repository.Save(contentType);

View File

@@ -70,18 +70,18 @@ public class ContentServicePerformanceTest : UmbracoIntegrationTest
ContentTypeService.Save(new[] { contentType1, contentType2, contentType3 });
contentType1.AllowedContentTypes = new[]
{
new ContentTypeSort(new Lazy<int>(() => contentType2.Id), 0, contentType2.Alias),
new ContentTypeSort(new Lazy<int>(() => contentType3.Id), 1, contentType3.Alias)
new ContentTypeSort(new Lazy<int>(() => contentType2.Id), contentType2.Key, 0, contentType2.Alias),
new ContentTypeSort(new Lazy<int>(() => contentType3.Id), contentType3.Key, 1, contentType3.Alias)
};
contentType2.AllowedContentTypes = new[]
{
new ContentTypeSort(new Lazy<int>(() => contentType1.Id), 0, contentType1.Alias),
new ContentTypeSort(new Lazy<int>(() => contentType3.Id), 1, contentType3.Alias)
new ContentTypeSort(new Lazy<int>(() => contentType1.Id), contentType1.Key, 0, contentType1.Alias),
new ContentTypeSort(new Lazy<int>(() => contentType3.Id), contentType3.Key, 1, contentType3.Alias)
};
contentType3.AllowedContentTypes = new[]
{
new ContentTypeSort(new Lazy<int>(() => contentType1.Id), 0, contentType1.Alias),
new ContentTypeSort(new Lazy<int>(() => contentType2.Id), 1, contentType2.Alias)
new ContentTypeSort(new Lazy<int>(() => contentType1.Id), contentType1.Key, 0, contentType1.Alias),
new ContentTypeSort(new Lazy<int>(() => contentType2.Id), contentType2.Key, 1, contentType2.Alias)
};
ContentTypeService.Save(new[] { contentType1, contentType2, contentType3 });

View File

@@ -652,7 +652,7 @@ public class ContentServiceTagsTests : UmbracoIntegrationTest
CreateAndAddTagsPropertyType(contentType);
ContentTypeService.Save(contentType);
contentType.AllowedContentTypes =
new[] { new ContentTypeSort(new Lazy<int>(() => contentType.Id), 0, contentType.Alias) };
new[] { new ContentTypeSort(new Lazy<int>(() => contentType.Id), contentType.Key, 0, contentType.Alias) };
var content = ContentBuilder.CreateSimpleContent(contentType, "Tagged content");
content.AssignTags(PropertyEditorCollection, DataTypeService, Serializer, "tags",

View File

@@ -1843,7 +1843,7 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent
ContentTypeBuilder.CreateSimpleContentType("umbTextpage1", "Textpage", defaultTemplateId: template.Id);
contentType.AllowedContentTypes = new List<ContentTypeSort>
{
new(new Lazy<int>(() => contentType.Id), 0, contentType.Alias)
new(new Lazy<int>(() => contentType.Id), contentType.Key, 0, contentType.Alias)
};
ContentTypeService.Save(contentType);
@@ -1883,7 +1883,7 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent
ContentTypeBuilder.CreateSimpleContentType("umbTextpage1", "Textpage", defaultTemplateId: template.Id);
contentType.AllowedContentTypes = new List<ContentTypeSort>
{
new(new Lazy<int>(() => contentType.Id), 0, contentType.Alias)
new(new Lazy<int>(() => contentType.Id), contentType.Key, 0, contentType.Alias)
};
ContentTypeService.Save(contentType);

View File

@@ -139,7 +139,7 @@ public class DomainAndUrlsTests : UmbracoIntegrationTest
root.GetContentUrlsAsync(
GetRequiredService<IPublishedRouter>(),
GetRequiredService<IUmbracoContextAccessor>().GetRequiredUmbracoContext(),
GetRequiredService<ILocalizationService>(),
GetRequiredService<ILanguageService>(),
GetRequiredService<ILocalizedTextService>(),
ContentService,
GetRequiredService<IVariationContextAccessor>(),

View File

@@ -1,7 +1,4 @@
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Moq;
using NUnit.Framework;
@@ -58,16 +55,16 @@ public class GetContentUrlsTests : PublishedSnapshotServiceTestBase
return textService.Object;
}
private ILocalizationService GetLangService(params string[] isoCodes)
private ILanguageService GetLangService(params string[] isoCodes)
{
var allLangs = isoCodes
.Select(CultureInfo.GetCultureInfo)
.Select(culture => new Language(culture.Name, culture.EnglishName) { IsDefault = true, IsMandatory = true })
.ToArray();
var langServiceMock = new Mock<ILocalizationService>();
langServiceMock.Setup(x => x.GetAllLanguages()).Returns(allLangs);
langServiceMock.Setup(x => x.GetDefaultLanguageIsoCode()).Returns(allLangs.First(x => x.IsDefault).IsoCode);
var langServiceMock = new Mock<ILanguageService>();
langServiceMock.Setup(x => x.GetAllAsync()).ReturnsAsync(allLangs);
langServiceMock.Setup(x => x.GetDefaultIsoCodeAsync()).ReturnsAsync(allLangs.First(x => x.IsDefault).IsoCode);
return langServiceMock.Object;
}