Forward merge #19720 to V16 (#19735)

* Add support for programmatic creation of property types providing the data type key (#19720)

* Add support for programmatic creation of property types providing the data type key.

* Add integration tests

---------

Co-authored-by: kjac <kja@umbraco.dk>

* Don't use Lazy

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
This commit is contained in:
Kenn Jacobsen
2025-07-18 10:47:34 +02:00
committed by GitHub
parent 698d566b76
commit 5660c6c369
11 changed files with 135 additions and 37 deletions

View File

@@ -5,6 +5,7 @@ using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Persistence.Querying;
using Umbraco.Cms.Core.Persistence.Repositories;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Strings;
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
using Umbraco.Cms.Infrastructure.Persistence.Querying;
@@ -25,8 +26,9 @@ internal class ContentTypeRepository : ContentTypeRepositoryBase<IContentType>,
ILogger<ContentTypeRepository> logger,
IContentTypeCommonRepository commonRepository,
ILanguageRepository languageRepository,
IShortStringHelper shortStringHelper)
: base(scopeAccessor, cache, logger, commonRepository, languageRepository, shortStringHelper)
IShortStringHelper shortStringHelper,
IIdKeyMap idKeyMap)
: base(scopeAccessor, cache, logger, commonRepository, languageRepository, shortStringHelper, idKeyMap)
{
}

View File

@@ -29,15 +29,22 @@ internal abstract class ContentTypeRepositoryBase<TEntity> : EntityRepositoryBas
where TEntity : class, IContentTypeComposition
{
private readonly IShortStringHelper _shortStringHelper;
private readonly IIdKeyMap _idKeyMap;
protected ContentTypeRepositoryBase(IScopeAccessor scopeAccessor, AppCaches cache,
ILogger<ContentTypeRepositoryBase<TEntity>> logger, IContentTypeCommonRepository commonRepository,
ILanguageRepository languageRepository, IShortStringHelper shortStringHelper)
protected ContentTypeRepositoryBase(
IScopeAccessor scopeAccessor,
AppCaches cache,
ILogger<ContentTypeRepositoryBase<TEntity>> logger,
IContentTypeCommonRepository commonRepository,
ILanguageRepository languageRepository,
IShortStringHelper shortStringHelper,
IIdKeyMap idKeyMap)
: base(scopeAccessor, cache, logger)
{
_shortStringHelper = shortStringHelper;
CommonRepository = commonRepository;
LanguageRepository = languageRepository;
_idKeyMap = idKeyMap;
}
protected IContentTypeCommonRepository CommonRepository { get; }
@@ -291,7 +298,7 @@ AND umbracoNode.nodeObjectType = @objectType",
// If the Id of the DataType is not set, we resolve it from the db by its PropertyEditorAlias
if (propertyType.DataTypeId == 0 || propertyType.DataTypeId == default)
{
AssignDataTypeFromPropertyEditor(propertyType);
AssignDataTypeIdFromProvidedKeyOrPropertyEditor(propertyType);
}
PropertyTypeDto propertyTypeDto =
@@ -592,7 +599,7 @@ AND umbracoNode.id <> @id",
// if the Id of the DataType is not set, we resolve it from the db by its PropertyEditorAlias
if (propertyType.DataTypeId == 0 || propertyType.DataTypeId == default)
{
AssignDataTypeFromPropertyEditor(propertyType);
AssignDataTypeIdFromProvidedKeyOrPropertyEditor(propertyType);
}
// validate the alias
@@ -1434,37 +1441,59 @@ AND umbracoNode.id <> @id",
protected abstract TEntity? PerformGet(Guid id);
/// <summary>
/// Try to set the data type id based on its ControlId
/// Try to set the data type Id based on the provided key or property editor alias.
/// </summary>
/// <param name="propertyType"></param>
private void AssignDataTypeFromPropertyEditor(IPropertyType propertyType)
private void AssignDataTypeIdFromProvidedKeyOrPropertyEditor(IPropertyType propertyType)
{
// we cannot try to assign a data type of it's empty
if (propertyType.PropertyEditorAlias.IsNullOrWhiteSpace() == false)
// If a key is provided, use that.
if (propertyType.DataTypeKey != Guid.Empty)
{
Sql<ISqlContext> sql = Sql()
.Select<DataTypeDto>(dt => dt.Select(x => x.NodeDto))
.From<DataTypeDto>()
.InnerJoin<NodeDto>().On<DataTypeDto, NodeDto>((dt, n) => dt.NodeId == n.NodeId)
.Where(
"propertyEditorAlias = @propertyEditorAlias",
new { propertyEditorAlias = propertyType.PropertyEditorAlias })
.OrderBy<DataTypeDto>(typeDto => typeDto.NodeId);
DataTypeDto? datatype = Database.FirstOrDefault<DataTypeDto>(sql);
// we cannot assign a data type if one was not found
if (datatype != null)
Attempt<int> dataTypeIdAttempt = _idKeyMap.GetIdForKey(propertyType.DataTypeKey, UmbracoObjectTypes.DataType);
if (dataTypeIdAttempt.Success)
{
propertyType.DataTypeId = datatype.NodeId;
propertyType.DataTypeKey = datatype.NodeDto.UniqueId;
propertyType.DataTypeId = dataTypeIdAttempt.Result;
return;
}
else
{
Logger.LogWarning(
"Could not assign a data type for the property type {PropertyTypeAlias} since no data type was found with a property editor {PropertyEditorAlias}",
propertyType.Alias, propertyType.PropertyEditorAlias);
"Could not assign a data type for the property type {PropertyTypeAlias} since no integer Id was found matching the key {DataTypeKey}. Falling back to look up via the property editor alias.",
propertyType.Alias,
propertyType.DataTypeKey);
}
}
// Otherwise if a property editor alias is provided, try to find a data type that uses that alias.
if (propertyType.PropertyEditorAlias.IsNullOrWhiteSpace())
{
// We cannot try to assign a data type if it's empty.
return;
}
Sql<ISqlContext> sql = Sql()
.Select<DataTypeDto>(dt => dt.Select(x => x.NodeDto))
.From<DataTypeDto>()
.InnerJoin<NodeDto>().On<DataTypeDto, NodeDto>((dt, n) => dt.NodeId == n.NodeId)
.Where(
"propertyEditorAlias = @propertyEditorAlias",
new { propertyEditorAlias = propertyType.PropertyEditorAlias })
.OrderBy<DataTypeDto>(typeDto => typeDto.NodeId);
DataTypeDto? datatype = Database.FirstOrDefault<DataTypeDto>(sql);
// we cannot assign a data type if one was not found
if (datatype != null)
{
propertyType.DataTypeId = datatype.NodeId;
propertyType.DataTypeKey = datatype.NodeDto.UniqueId;
}
else
{
Logger.LogWarning(
"Could not assign a data type for the property type {PropertyTypeAlias} since no data type was found with a property editor {PropertyEditorAlias}",
propertyType.Alias,
propertyType.PropertyEditorAlias);
}
}
protected abstract TEntity? PerformGet(string alias);

View File

@@ -5,6 +5,7 @@ using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Persistence.Querying;
using Umbraco.Cms.Core.Persistence.Repositories;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Strings;
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
using Umbraco.Cms.Infrastructure.Persistence.Querying;
@@ -24,8 +25,9 @@ internal class MediaTypeRepository : ContentTypeRepositoryBase<IMediaType>, IMed
ILogger<MediaTypeRepository> logger,
IContentTypeCommonRepository commonRepository,
ILanguageRepository languageRepository,
IShortStringHelper shortStringHelper)
: base(scopeAccessor, cache, logger, commonRepository, languageRepository, shortStringHelper)
IShortStringHelper shortStringHelper,
IIdKeyMap idKeyMap)
: base(scopeAccessor, cache, logger, commonRepository, languageRepository, shortStringHelper, idKeyMap)
{
}

View File

@@ -5,6 +5,7 @@ using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Persistence.Querying;
using Umbraco.Cms.Core.Persistence.Repositories;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Strings;
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
using Umbraco.Cms.Infrastructure.Persistence.Factories;
@@ -27,9 +28,10 @@ internal class MemberTypeRepository : ContentTypeRepositoryBase<IMemberType>, IM
ILogger<MemberTypeRepository> logger,
IContentTypeCommonRepository commonRepository,
ILanguageRepository languageRepository,
IShortStringHelper shortStringHelper)
: base(scopeAccessor, cache, logger, commonRepository, languageRepository, shortStringHelper) =>
_shortStringHelper = shortStringHelper;
IShortStringHelper shortStringHelper,
IIdKeyMap idKeyMap)
: base(scopeAccessor, cache, logger, commonRepository, languageRepository, shortStringHelper, idKeyMap)
=> _shortStringHelper = shortStringHelper;
protected override bool SupportsPublishing => MemberType.SupportsPublishingConst;

View File

@@ -60,6 +60,8 @@ public abstract class UmbracoIntegrationTest : UmbracoIntegrationTestBase
protected IShortStringHelper ShortStringHelper => Services.GetRequiredService<IShortStringHelper>();
protected IIdKeyMap IdKeyMap => Services.GetRequiredService<IIdKeyMap>();
protected GlobalSettings GlobalSettings => Services.GetRequiredService<IOptions<GlobalSettings>>().Value;
protected IMapperCollection Mappers => Services.GetRequiredService<IMapperCollection>();

View File

@@ -123,7 +123,7 @@ internal sealed class DocumentRepositoryTest : UmbracoIntegrationTest
new ContentTypeCommonRepository(scopeAccessor, templateRepository, appCaches, ShortStringHelper);
var languageRepository =
new LanguageRepository(scopeAccessor, appCaches, LoggerFactory.CreateLogger<LanguageRepository>());
contentTypeRepository = new ContentTypeRepository(scopeAccessor, appCaches, LoggerFactory.CreateLogger<ContentTypeRepository>(), commonRepository, languageRepository, ShortStringHelper);
contentTypeRepository = new ContentTypeRepository(scopeAccessor, appCaches, LoggerFactory.CreateLogger<ContentTypeRepository>(), commonRepository, languageRepository, ShortStringHelper, IdKeyMap);
var relationTypeRepository = new RelationTypeRepository(scopeAccessor, AppCaches.Disabled, LoggerFactory.CreateLogger<RelationTypeRepository>());
var entityRepository = new EntityRepository(scopeAccessor, AppCaches.Disabled);
var relationRepository = new RelationRepository(scopeAccessor, LoggerFactory.CreateLogger<RelationRepository>(), relationTypeRepository, entityRepository);

View File

@@ -56,7 +56,7 @@ internal sealed class MediaRepositoryTest : UmbracoIntegrationTest
new ContentTypeCommonRepository(scopeAccessor, TemplateRepository, appCaches, ShortStringHelper);
var languageRepository =
new LanguageRepository(scopeAccessor, appCaches, LoggerFactory.CreateLogger<LanguageRepository>());
mediaTypeRepository = new MediaTypeRepository(scopeAccessor, appCaches, LoggerFactory.CreateLogger<MediaTypeRepository>(), commonRepository, languageRepository, ShortStringHelper);
mediaTypeRepository = new MediaTypeRepository(scopeAccessor, appCaches, LoggerFactory.CreateLogger<MediaTypeRepository>(), commonRepository, languageRepository, ShortStringHelper, IdKeyMap);
var tagRepository = new TagRepository(scopeAccessor, appCaches, LoggerFactory.CreateLogger<TagRepository>());
var relationTypeRepository = new RelationTypeRepository(scopeAccessor, AppCaches.Disabled, LoggerFactory.CreateLogger<RelationTypeRepository>());
var entityRepository = new EntityRepository(scopeAccessor, AppCaches.Disabled);

View File

@@ -9,6 +9,7 @@ using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Persistence;
using Umbraco.Cms.Core.Persistence.Repositories;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement;
using Umbraco.Cms.Infrastructure.Scoping;
using Umbraco.Cms.Tests.Common.Builders;
@@ -411,7 +412,7 @@ internal sealed class MediaTypeRepositoryTest : UmbracoIntegrationTest
}
private MediaTypeRepository CreateRepository(IScopeProvider provider) =>
new((IScopeAccessor)provider, AppCaches.Disabled, LoggerFactory.CreateLogger<MediaTypeRepository>(), CommonRepository, LanguageRepository, ShortStringHelper);
new((IScopeAccessor)provider, AppCaches.Disabled, LoggerFactory.CreateLogger<MediaTypeRepository>(), CommonRepository, LanguageRepository, ShortStringHelper, IdKeyMap);
private EntityContainerRepository CreateContainerRepository(IScopeProvider provider) =>
new((IScopeAccessor)provider, AppCaches.Disabled, LoggerFactory.CreateLogger<EntityContainerRepository>(), Constants.ObjectTypes.MediaTypeContainer);

View File

@@ -10,6 +10,7 @@ using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Persistence;
using Umbraco.Cms.Core.Persistence.Repositories;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement;
using Umbraco.Cms.Infrastructure.Scoping;
using Umbraco.Cms.Tests.Common.Builders;
@@ -26,7 +27,7 @@ internal sealed class MemberTypeRepositoryTest : UmbracoIntegrationTest
{
var commonRepository = GetRequiredService<IContentTypeCommonRepository>();
var languageRepository = GetRequiredService<ILanguageRepository>();
return new MemberTypeRepository((IScopeAccessor)provider, AppCaches.Disabled, Mock.Of<ILogger<MemberTypeRepository>>(), commonRepository, languageRepository, ShortStringHelper);
return new MemberTypeRepository((IScopeAccessor)provider, AppCaches.Disabled, Mock.Of<ILogger<MemberTypeRepository>>(), commonRepository, languageRepository, ShortStringHelper, IdKeyMap);
}
[Test]

View File

@@ -266,7 +266,7 @@ internal sealed class TemplateRepositoryTest : UmbracoIntegrationTest
var commonRepository =
new ContentTypeCommonRepository(scopeAccessor, templateRepository, AppCaches, ShortStringHelper);
var languageRepository = new LanguageRepository(scopeAccessor, AppCaches.Disabled, LoggerFactory.CreateLogger<LanguageRepository>());
var contentTypeRepository = new ContentTypeRepository(scopeAccessor, AppCaches.Disabled, LoggerFactory.CreateLogger<ContentTypeRepository>(), commonRepository, languageRepository, ShortStringHelper);
var contentTypeRepository = new ContentTypeRepository(scopeAccessor, AppCaches.Disabled, LoggerFactory.CreateLogger<ContentTypeRepository>(), commonRepository, languageRepository, ShortStringHelper, IdKeyMap);
var relationTypeRepository = new RelationTypeRepository(scopeAccessor, AppCaches.Disabled, LoggerFactory.CreateLogger<RelationTypeRepository>());
var entityRepository = new EntityRepository(scopeAccessor, AppCaches.Disabled);
var relationRepository = new RelationRepository(scopeAccessor, LoggerFactory.CreateLogger<RelationRepository>(), relationTypeRepository, entityRepository);

View File

@@ -1971,6 +1971,65 @@ internal sealed class ContentTypeServiceTests : UmbracoIntegrationTest
.Variations);
}
[Test]
public void Can_Create_Property_Type_Based_On_DataTypeKey()
{
// Arrange
var cts = ContentTypeService;
var dtdYesNo = DataTypeService.GetDataType(-49);
IContentType ctBase = new ContentType(ShortStringHelper, -1)
{
Name = "Base",
Alias = "Base",
Icon = "folder.gif",
Thumbnail = "folder.png"
};
ctBase.AddPropertyType(new PropertyType(ShortStringHelper, "ShouldNotMatter", ValueStorageType.Nvarchar)
{
Name = "Hide From Navigation",
Alias = Constants.Conventions.Content.NaviHide,
DataTypeKey = dtdYesNo.Key
});
cts.Save(ctBase);
// Assert
ctBase = cts.Get(ctBase.Key);
Assert.That(ctBase, Is.Not.Null);
Assert.That(ctBase.HasIdentity, Is.True);
Assert.That(ctBase.PropertyTypes.Count(), Is.EqualTo(1));
Assert.That(ctBase.PropertyTypes.First().DataTypeId, Is.EqualTo(dtdYesNo.Id));
Assert.That(ctBase.PropertyTypes.First().PropertyEditorAlias, Is.EqualTo(dtdYesNo.EditorAlias));
}
[Test]
public void Can_Create_Property_Type_Based_On_PropertyEditorAlias()
{
// Arrange
var cts = ContentTypeService;
var dtdYesNo = DataTypeService.GetDataType(-49);
IContentType ctBase = new ContentType(ShortStringHelper, -1)
{
Name = "Base",
Alias = "Base",
Icon = "folder.gif",
Thumbnail = "folder.png"
};
ctBase.AddPropertyType(new PropertyType(ShortStringHelper, "Umbraco.TrueFalse", ValueStorageType.Nvarchar)
{
Name = "Hide From Navigation",
Alias = Constants.Conventions.Content.NaviHide,
});
cts.Save(ctBase);
// Assert
ctBase = cts.Get(ctBase.Key);
Assert.That(ctBase, Is.Not.Null);
Assert.That(ctBase.HasIdentity, Is.True);
Assert.That(ctBase.PropertyTypes.Count(), Is.EqualTo(1));
Assert.That(ctBase.PropertyTypes.First().DataTypeId, Is.EqualTo(dtdYesNo.Id));
Assert.That(ctBase.PropertyTypes.First().PropertyEditorAlias, Is.EqualTo(dtdYesNo.EditorAlias));
}
private ContentType CreateComponent()
{
var component = new ContentType(ShortStringHelper, -1)