Align the data type API (#13760)

* Make data type CRUD operations async using Attempt pattern

* Refactor data type container management to its own service + add unit tests for it

* Add compatability suppression for new interface methods and unit test changes
This commit is contained in:
Kenn Jacobsen
2023-02-01 08:38:36 +01:00
committed by GitHub
parent ac8cfcf634
commit 641cae7fb5
24 changed files with 1172 additions and 342 deletions

View File

@@ -24,12 +24,12 @@ public class ByKeyDataTypeController : DataTypeControllerBase
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<DataTypeViewModel>> ByKey(Guid key)
{
IDataType? dataType = _dataTypeService.GetDataType(key);
IDataType? dataType = await _dataTypeService.GetAsync(key);
if (dataType == null)
{
return NotFound();
}
return await Task.FromResult(Ok(_umbracoMapper.Map<DataTypeViewModel>(dataType)));
return Ok(_umbracoMapper.Map<DataTypeViewModel>(dataType));
}
}

View File

@@ -1,10 +1,12 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.DataType;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Api.Management.Controllers.DataType;
@@ -24,21 +26,15 @@ public class CreateDataTypeController : DataTypeControllerBase
[HttpPost]
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> Create(DataTypeCreateModel dataTypeCreateModel)
public async Task<IActionResult> Create(DataTypeCreateModel dataTypeCreateModel)
{
IDataType? created = _umbracoMapper.Map<IDataType>(dataTypeCreateModel);
if (created == null)
{
return BadRequest("Could not map the POSTed model to a data type");
}
IDataType? created = _umbracoMapper.Map<IDataType>(dataTypeCreateModel)!;
Attempt<IDataType, DataTypeOperationStatus> result = await _dataTypeService.CreateAsync(created, CurrentUserId(_backOfficeSecurityAccessor));
ProblemDetails? validationIssues = Save(created, _dataTypeService, _backOfficeSecurityAccessor);
if (validationIssues != null)
{
return BadRequest(validationIssues);
}
return await Task.FromResult(CreatedAtAction<ByKeyDataTypeController>(controller => nameof(controller.ByKey), created.Key));
return result.Success
? CreatedAtAction<ByKeyDataTypeController>(controller => nameof(controller.ByKey), created.Key)
: DataTypeOperationStatusResult(result.Status);
}
}

View File

@@ -1,12 +1,9 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Common.Builders;
using Umbraco.Cms.Api.Management.Routing;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Api.Management.Controllers.DataType;
@@ -16,19 +13,22 @@ namespace Umbraco.Cms.Api.Management.Controllers.DataType;
[ApiVersion("1.0")]
public abstract class DataTypeControllerBase : ManagementApiControllerBase
{
protected static ProblemDetails? Save(IDataType dataType, IDataTypeService dataTypeService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
{
ValidationResult[] validationResults = dataTypeService.ValidateConfigurationData(dataType).ToArray();
if (validationResults.Any())
protected IActionResult DataTypeOperationStatusResult(DataTypeOperationStatus status) =>
status switch
{
return new ProblemDetailsBuilder()
DataTypeOperationStatus.InvalidConfiguration => BadRequest(new ProblemDetailsBuilder()
.WithTitle("Invalid data type configuration")
.WithDetail(string.Join(Environment.NewLine, validationResults.Select(r => r.ErrorMessage)))
.Build();
}
dataTypeService.Save(dataType, CurrentUserId(backOfficeSecurityAccessor));
return null;
}
.WithDetail("The supplied data type configuration was not valid. Please see the log for more details.")
.Build()),
DataTypeOperationStatus.NotFound => NotFound("The data type could not be found"),
DataTypeOperationStatus.InvalidName => BadRequest(new ProblemDetailsBuilder()
.WithTitle("Invalid data type name")
.WithDetail("The data type name must be non-empty and no longer than 255 characters.")
.Build()),
DataTypeOperationStatus.CancelledByNotification => BadRequest(new ProblemDetailsBuilder()
.WithTitle("Cancelled by notification")
.WithDetail("A notification handler prevented the data type operation.")
.Build()),
_ => StatusCode(StatusCodes.Status500InternalServerError, "Unknown data type operation status")
};
}

View File

@@ -1,9 +1,10 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.DataType;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Api.Management.Controllers.DataType;
@@ -21,18 +22,14 @@ public class DeleteDataTypeController : DataTypeControllerBase
[HttpDelete("{key:guid}")]
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> Delete(Guid key)
public async Task<IActionResult> Delete(Guid key)
{
IDataType? dataType = _dataTypeService.GetDataType(key);
if (dataType == null)
{
return NotFound();
}
Attempt<IDataType?, DataTypeOperationStatus> result = await _dataTypeService.DeleteAsync(key, CurrentUserId(_backOfficeSecurityAccessor));
// one might expect this method to have an Attempt return value, but no - it has no
// return value, we'll just have to assume it succeeds
_dataTypeService.Delete(dataType, CurrentUserId(_backOfficeSecurityAccessor));
return await Task.FromResult(Ok());
return result.Success
? Ok()
: DataTypeOperationStatusResult(result.Status);
}
}

View File

@@ -8,8 +8,8 @@ namespace Umbraco.Cms.Api.Management.Controllers.DataType.Folder;
public class ByKeyDataTypeFolderController : DataTypeFolderControllerBase
{
public ByKeyDataTypeFolderController(IBackOfficeSecurityAccessor backOfficeSecurityAccessor, IDataTypeService dataTypeService)
: base(backOfficeSecurityAccessor, dataTypeService)
public ByKeyDataTypeFolderController(IBackOfficeSecurityAccessor backOfficeSecurityAccessor, IDataTypeContainerService dataTypeContainerService)
: base(backOfficeSecurityAccessor, dataTypeContainerService)
{
}
@@ -17,6 +17,5 @@ public class ByKeyDataTypeFolderController : DataTypeFolderControllerBase
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(FolderViewModel), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<FolderViewModel>> ByKey(Guid key)
=> await Task.FromResult(GetFolder(key));
public async Task<IActionResult> ByKey(Guid key) => await GetFolderAsync(key);
}

View File

@@ -8,14 +8,14 @@ namespace Umbraco.Cms.Api.Management.Controllers.DataType.Folder;
public class CreateDataTypeFolderController : DataTypeFolderControllerBase
{
public CreateDataTypeFolderController(IBackOfficeSecurityAccessor backOfficeSecurityAccessor, IDataTypeService dataTypeService)
: base(backOfficeSecurityAccessor, dataTypeService)
public CreateDataTypeFolderController(IBackOfficeSecurityAccessor backOfficeSecurityAccessor, IDataTypeContainerService dataTypeContainerService)
: base(backOfficeSecurityAccessor, dataTypeContainerService)
{
}
[HttpPost]
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status201Created)]
public async Task<ActionResult> Create(FolderCreateModel folderCreateModel)
=> await Task.FromResult(CreateFolder<ByKeyDataTypeFolderController>(folderCreateModel, controller => nameof(controller.ByKey)));
public async Task<IActionResult> Create(FolderCreateModel folderCreateModel)
=> await CreateFolderAsync<ByKeyDataTypeFolderController>(folderCreateModel, controller => nameof(controller.ByKey));
}

View File

@@ -1,9 +1,12 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Common.Builders;
using Umbraco.Cms.Api.Management.Routing;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Api.Management.Controllers.DataType.Folder;
@@ -11,26 +14,44 @@ namespace Umbraco.Cms.Api.Management.Controllers.DataType.Folder;
[ApiController]
[VersionedApiBackOfficeRoute($"{Constants.UdiEntityType.DataType}/folder")]
[ApiExplorerSettings(GroupName = "Data Type")]
public abstract class DataTypeFolderControllerBase : FolderManagementControllerBase
public abstract class DataTypeFolderControllerBase : FolderManagementControllerBase<DataTypeContainerOperationStatus>
{
private readonly IDataTypeService _dataTypeService;
private readonly IDataTypeContainerService _dataTypeContainerService;
protected DataTypeFolderControllerBase(IBackOfficeSecurityAccessor backOfficeSecurityAccessor, IDataTypeService dataTypeService)
protected DataTypeFolderControllerBase(IBackOfficeSecurityAccessor backOfficeSecurityAccessor, IDataTypeContainerService dataTypeContainerService)
: base(backOfficeSecurityAccessor) =>
_dataTypeService = dataTypeService;
_dataTypeContainerService = dataTypeContainerService;
protected override EntityContainer? GetContainer(Guid key)
=> _dataTypeService.GetContainer(key);
protected override Guid ContainerObjectType => Constants.ObjectTypes.DataType;
protected override EntityContainer? GetContainer(int containerId)
=> _dataTypeService.GetContainer(containerId);
protected override async Task<EntityContainer?> GetContainerAsync(Guid key)
=> await _dataTypeContainerService.GetAsync(key);
protected override Attempt<OperationResult<OperationResultType, EntityContainer>?> CreateContainer(int parentId, string name, int userId)
=> _dataTypeService.CreateContainer(parentId, Guid.NewGuid(), name, userId);
protected override async Task<EntityContainer?> GetParentContainerAsync(EntityContainer container)
=> await _dataTypeContainerService.GetParentAsync(container);
protected override Attempt<OperationResult?> SaveContainer(EntityContainer container, int userId)
=> _dataTypeService.SaveContainer(container, userId);
protected override async Task<Attempt<EntityContainer, DataTypeContainerOperationStatus>> CreateContainerAsync(EntityContainer container, Guid? parentId, int userId)
=> await _dataTypeContainerService.CreateAsync(container, parentId, userId);
protected override Attempt<OperationResult?> DeleteContainer(int containerId, int userId)
=> _dataTypeService.DeleteContainer(containerId, userId);
protected override async Task<Attempt<EntityContainer, DataTypeContainerOperationStatus>> UpdateContainerAsync(EntityContainer container, int userId)
=> await _dataTypeContainerService.UpdateAsync(container, userId);
protected override async Task<Attempt<EntityContainer?, DataTypeContainerOperationStatus>> DeleteContainerAsync(Guid id, int userId)
=> await _dataTypeContainerService.DeleteAsync(id, userId);
protected override IActionResult OperationStatusResult(DataTypeContainerOperationStatus status)
=> status switch
{
DataTypeContainerOperationStatus.NotFound => NotFound("The data type folder could not be found"),
DataTypeContainerOperationStatus.ParentNotFound => NotFound("The data type parent folder could not be found"),
DataTypeContainerOperationStatus.NotEmpty => BadRequest(new ProblemDetailsBuilder()
.WithTitle("The folder is not empty")
.WithDetail("The data type folder must be empty to perform this action.")
.Build()),
DataTypeContainerOperationStatus.CancelledByNotification => BadRequest(new ProblemDetailsBuilder()
.WithTitle("Cancelled by notification")
.WithDetail("A notification handler prevented the data type folder operation.")
.Build()),
_ => StatusCode(StatusCodes.Status500InternalServerError, "Unknown data type folder operation status")
};
}

View File

@@ -8,8 +8,8 @@ namespace Umbraco.Cms.Api.Management.Controllers.DataType.Folder;
public class DeleteDataTypeFolderController : DataTypeFolderControllerBase
{
public DeleteDataTypeFolderController(IBackOfficeSecurityAccessor backOfficeSecurityAccessor, IDataTypeService dataTypeService)
: base(backOfficeSecurityAccessor, dataTypeService)
public DeleteDataTypeFolderController(IBackOfficeSecurityAccessor backOfficeSecurityAccessor, IDataTypeContainerService dataTypeContainerService)
: base(backOfficeSecurityAccessor, dataTypeContainerService)
{
}
@@ -17,6 +17,5 @@ public class DeleteDataTypeFolderController : DataTypeFolderControllerBase
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> Delete(Guid key)
=> await Task.FromResult(DeleteFolder(key));
public async Task<IActionResult> Delete(Guid key) => await DeleteFolderAsync(key);
}

View File

@@ -8,8 +8,8 @@ namespace Umbraco.Cms.Api.Management.Controllers.DataType.Folder;
public class UpdateDataTypeFolderController : DataTypeFolderControllerBase
{
public UpdateDataTypeFolderController(IBackOfficeSecurityAccessor backOfficeSecurityAccessor, IDataTypeService dataTypeService)
: base(backOfficeSecurityAccessor, dataTypeService)
public UpdateDataTypeFolderController(IBackOfficeSecurityAccessor backOfficeSecurityAccessor, IDataTypeContainerService dataTypeContainerService)
: base(backOfficeSecurityAccessor, dataTypeContainerService)
{
}
@@ -17,6 +17,5 @@ public class UpdateDataTypeFolderController : DataTypeFolderControllerBase
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> Update(Guid key, FolderUpdateModel folderUpdateModel)
=> await Task.FromResult(UpdateFolder(key, folderUpdateModel));
public async Task<IActionResult> Update(Guid key, FolderUpdateModel folderUpdateModel) => await UpdateFolderAsync(key, folderUpdateModel);
}

View File

@@ -3,8 +3,8 @@ using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.ViewModels.DataType;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Api.Management.Controllers.DataType;
@@ -23,17 +23,15 @@ public class ReferencesDataTypeController : DataTypeControllerBase
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(DataTypeReferenceViewModel[]), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<DataTypeReferenceViewModel[]>> References(Guid key)
public async Task<IActionResult> References(Guid key)
{
IDataType? dataType = _dataTypeService.GetDataType(key);
if (dataType == null)
Attempt<IReadOnlyDictionary<Udi, IEnumerable<string>>, DataTypeOperationStatus> result = await _dataTypeService.GetReferencesAsync(key);
if (result.Success == false)
{
return NotFound();
return DataTypeOperationStatusResult(result.Status);
}
IReadOnlyDictionary<Udi, IEnumerable<string>> usages = _dataTypeService.GetReferences(dataType.Id);
DataTypeReferenceViewModel[] viewModels = _dataTypeReferenceViewModelFactory.CreateDataTypeReferenceViewModels(usages).ToArray();
return await Task.FromResult(Ok(viewModels));
DataTypeReferenceViewModel[] viewModels = _dataTypeReferenceViewModelFactory.CreateDataTypeReferenceViewModels(result.Result).ToArray();
return Ok(viewModels);
}
}

View File

@@ -1,11 +1,12 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.DataType;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Api.Management.Controllers.DataType;
@@ -25,23 +26,21 @@ public class UpdateDataTypeController : DataTypeControllerBase
[HttpPut("{key:guid}")]
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> Update(Guid key, DataTypeUpdateModel dataTypeViewModel)
public async Task<IActionResult> Update(Guid key, DataTypeUpdateModel dataTypeViewModel)
{
IDataType? current = _dataTypeService.GetDataType(key);
IDataType? current = await _dataTypeService.GetAsync(key);
if (current == null)
{
return NotFound();
}
IDataType updated = _umbracoMapper.Map(dataTypeViewModel, current);
Attempt<IDataType, DataTypeOperationStatus> result = await _dataTypeService.UpdateAsync(updated, CurrentUserId(_backOfficeSecurityAccessor));
ProblemDetails? validationIssues = Save(updated, _dataTypeService, _backOfficeSecurityAccessor);
if (validationIssues != null)
{
return BadRequest(validationIssues);
}
return await Task.FromResult(Ok());
return result.Success
? Ok()
: DataTypeOperationStatusResult(result.Status);
}
}

View File

@@ -1,32 +1,29 @@
using System.Linq.Expressions;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Common.Builders;
using Umbraco.Cms.Api.Management.ViewModels.Folder;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Management.Controllers;
public abstract class FolderManagementControllerBase : ManagementApiControllerBase
public abstract class FolderManagementControllerBase<TStatus> : ManagementApiControllerBase
where TStatus : Enum
{
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
protected FolderManagementControllerBase(IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
=> _backOfficeSecurityAccessor = backOfficeSecurityAccessor;
protected ActionResult GetFolder(Guid key)
protected async Task<IActionResult> GetFolderAsync(Guid key)
{
EntityContainer? container = GetContainer(key);
EntityContainer? container = await GetContainerAsync(key);
if (container == null)
{
return NotFound($"Could not find the folder with key: {key}");
}
EntityContainer? parentContainer = container.ParentId > 0
? GetContainer(container.ParentId)
: null;
EntityContainer? parentContainer = await GetParentContainerAsync(container);
// we could implement a mapper for this but it seems rather overkill at this point
return Ok(new FolderViewModel
@@ -37,35 +34,25 @@ public abstract class FolderManagementControllerBase : ManagementApiControllerBa
});
}
protected ActionResult CreateFolder<TCreatedActionController>(
protected async Task<IActionResult> CreateFolderAsync<TCreatedActionController>(
FolderCreateModel folderCreateModel,
Expression<Func<TCreatedActionController, string>> createdAction)
{
EntityContainer? parentContainer = folderCreateModel.ParentKey.HasValue
? GetContainer(folderCreateModel.ParentKey.Value)
: null;
var container = new EntityContainer(ContainerObjectType) { Name = folderCreateModel.Name };
Attempt<OperationResult<OperationResultType, EntityContainer>?> result = CreateContainer(
parentContainer?.Id ?? Constants.System.Root,
folderCreateModel.Name,
Attempt<EntityContainer, TStatus> result = await CreateContainerAsync(
container,
folderCreateModel.ParentKey,
CurrentUserId(_backOfficeSecurityAccessor));
if (result.Success == false)
{
ProblemDetails problemDetails = new ProblemDetailsBuilder()
.WithTitle("Unable to create the folder")
.WithDetail(result.Exception?.Message ?? FallbackProblemDetail(result.Result))
.Build();
return BadRequest(problemDetails);
}
EntityContainer container = result.Result!.Entity!;
return CreatedAtAction(createdAction, container.Key);
return result.Success
? CreatedAtAction(createdAction, result.Result.Key)
: OperationStatusResult(result.Status);
}
protected ActionResult UpdateFolder(Guid key, FolderUpdateModel folderUpdateModel)
protected async Task<IActionResult> UpdateFolderAsync(Guid key, FolderUpdateModel folderUpdateModel)
{
EntityContainer? container = GetContainer(key);
EntityContainer? container = await GetContainerAsync(key);
if (container == null)
{
return NotFound($"Could not find the folder with key: {key}");
@@ -73,50 +60,31 @@ public abstract class FolderManagementControllerBase : ManagementApiControllerBa
container.Name = folderUpdateModel.Name;
Attempt<OperationResult?> result = SaveContainer(container, CurrentUserId(_backOfficeSecurityAccessor));
if (result.Success == false)
{
ProblemDetails problemDetails = new ProblemDetailsBuilder()
.WithTitle("Unable to update the folder")
.WithDetail(result.Exception?.Message ?? FallbackProblemDetail(result.Result))
.Build();
return BadRequest(problemDetails);
}
return Ok();
Attempt<EntityContainer, TStatus> result = await UpdateContainerAsync(container, CurrentUserId(_backOfficeSecurityAccessor));
return result.Success
? Ok()
: OperationStatusResult(result.Status);
}
protected ActionResult DeleteFolder(Guid key)
protected async Task<IActionResult> DeleteFolderAsync(Guid key)
{
EntityContainer? container = GetContainer(key);
if (container == null)
{
return NotFound($"Could not find the folder with key: {key}");
}
Attempt<OperationResult?> result = DeleteContainer(container.Id, CurrentUserId(_backOfficeSecurityAccessor));
if (result.Success == false)
{
ProblemDetails problemDetails = new ProblemDetailsBuilder()
.WithTitle("Unable to delete the folder")
.WithDetail(result.Exception?.Message ?? FallbackProblemDetail(result.Result))
.Build();
return BadRequest(problemDetails);
}
return Ok();
Attempt<EntityContainer?, TStatus> result = await DeleteContainerAsync(key, CurrentUserId(_backOfficeSecurityAccessor));
return result.Success
? Ok()
: OperationStatusResult(result.Status);
}
private static string FallbackProblemDetail(OperationResult<OperationResultType>? result)
=> result != null ? $"The reported operation result was: {result.Result}" : "Check the log for additional details";
protected abstract Guid ContainerObjectType { get; }
protected abstract EntityContainer? GetContainer(Guid key);
protected abstract Task<EntityContainer?> GetContainerAsync(Guid key);
protected abstract EntityContainer? GetContainer(int containerId);
protected abstract Task<EntityContainer?> GetParentContainerAsync(EntityContainer container);
protected abstract Attempt<OperationResult?> SaveContainer(EntityContainer container, int userId);
protected abstract Task<Attempt<EntityContainer, TStatus>> CreateContainerAsync(EntityContainer container, Guid? parentId, int userId);
protected abstract Attempt<OperationResult<OperationResultType, EntityContainer>?> CreateContainer(int parentId, string name, int userId);
protected abstract Task<Attempt<EntityContainer, TStatus>> UpdateContainerAsync(EntityContainer container, int userId);
protected abstract Attempt<OperationResult?> DeleteContainer(int containerId, int userId);
protected abstract Task<Attempt<EntityContainer?, TStatus>> DeleteContainerAsync(Guid id, int userId);
protected abstract IActionResult OperationStatusResult(TStatus status);
}

View File

@@ -36,6 +36,6 @@ public abstract class LanguageControllerBase : ManagementApiControllerBase
.WithTitle("Cancelled by notification")
.WithDetail("A notification handler prevented the language operation.")
.Build()),
_ => StatusCode(StatusCodes.Status500InternalServerError, "Unknown dictionary operation status")
_ => StatusCode(StatusCodes.Status500InternalServerError, "Unknown language operation status")
};
}

View File

@@ -203,6 +203,13 @@
<Right>lib/net7.0/Umbraco.Core.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Umbraco.Cms.Core.Services.Implement.DataTypeService.#ctor(Umbraco.Cms.Core.PropertyEditors.IDataValueEditorFactory,Umbraco.Cms.Core.Scoping.ICoreScopeProvider,Microsoft.Extensions.Logging.ILoggerFactory,Umbraco.Cms.Core.Events.IEventMessagesFactory,Umbraco.Cms.Core.Persistence.Repositories.IDataTypeRepository,Umbraco.Cms.Core.Persistence.Repositories.IDataTypeContainerRepository,Umbraco.Cms.Core.Persistence.Repositories.IAuditRepository,Umbraco.Cms.Core.Persistence.Repositories.IEntityRepository,Umbraco.Cms.Core.Persistence.Repositories.IContentTypeRepository,Umbraco.Cms.Core.IO.IIOHelper,Umbraco.Cms.Core.Services.ILocalizedTextService,Umbraco.Cms.Core.Services.ILocalizationService,Umbraco.Cms.Core.Strings.IShortStringHelper,Umbraco.Cms.Core.Serialization.IJsonSerializer)</Target>
<Left>lib/net7.0/Umbraco.Core.dll</Left>
<Right>lib/net7.0/Umbraco.Core.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Umbraco.Cms.Core.Deploy.IDataTypeConfigurationConnector.FromArtifact(Umbraco.Cms.Core.Models.IDataType,System.String,Umbraco.Cms.Core.Deploy.IContextCache)</Target>
@@ -294,6 +301,55 @@
<Right>lib/net7.0/Umbraco.Core.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Umbraco.Cms.Core.Services.IDataTypeService.CreateAsync(Umbraco.Cms.Core.Models.IDataType,System.Int32)</Target>
<Left>lib/net7.0/Umbraco.Core.dll</Left>
<Right>lib/net7.0/Umbraco.Core.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Umbraco.Cms.Core.Services.IDataTypeService.DeleteAsync(System.Guid,System.Int32)</Target>
<Left>lib/net7.0/Umbraco.Core.dll</Left>
<Right>lib/net7.0/Umbraco.Core.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Umbraco.Cms.Core.Services.IDataTypeService.GetAsync(System.Guid)</Target>
<Left>lib/net7.0/Umbraco.Core.dll</Left>
<Right>lib/net7.0/Umbraco.Core.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Umbraco.Cms.Core.Services.IDataTypeService.GetAsync(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>CP0006</DiagnosticId>
<Target>M:Umbraco.Cms.Core.Services.IDataTypeService.GetByEditorAliasAsync(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>CP0006</DiagnosticId>
<Target>M:Umbraco.Cms.Core.Services.IDataTypeService.GetReferencesAsync(System.Guid)</Target>
<Left>lib/net7.0/Umbraco.Core.dll</Left>
<Right>lib/net7.0/Umbraco.Core.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Umbraco.Cms.Core.Services.IDataTypeService.UpdateAsync(Umbraco.Cms.Core.Models.IDataType,System.Int32)</Target>
<Left>lib/net7.0/Umbraco.Core.dll</Left>
<Right>lib/net7.0/Umbraco.Core.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Umbraco.Cms.Core.Services.IDataTypeService.ValidateConfigurationData(Umbraco.Cms.Core.Models.IDataType)</Target>

View File

@@ -282,6 +282,7 @@ namespace Umbraco.Cms.Core.DependencyInjection
Services.AddUnique<IUserService, UserService>();
Services.AddUnique<ILocalizationService, LocalizationService>();
Services.AddUnique<IDictionaryItemService, DictionaryItemService>();
Services.AddUnique<IDataTypeContainerService, DataTypeContainerService>();
Services.AddUnique<ILanguageService, LanguageService>();
Services.AddUnique<IMacroService, MacroService>();
Services.AddUnique<IMemberGroupService, MemberGroupService>();

View File

@@ -0,0 +1,180 @@
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Entities;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.Persistence.Repositories;
using Umbraco.Cms.Core.Scoping;
using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Core.Services;
internal sealed class DataTypeContainerService : RepositoryService, IDataTypeContainerService
{
private readonly IDataTypeContainerRepository _dataTypeContainerRepository;
private readonly IAuditRepository _auditRepository;
private readonly IEntityRepository _entityRepository;
public DataTypeContainerService(
ICoreScopeProvider provider,
ILoggerFactory loggerFactory,
IEventMessagesFactory eventMessagesFactory,
IDataTypeContainerRepository dataTypeContainerRepository,
IAuditRepository auditRepository,
IEntityRepository entityRepository)
: base(provider, loggerFactory, eventMessagesFactory)
{
_dataTypeContainerRepository = dataTypeContainerRepository;
_auditRepository = auditRepository;
_entityRepository = entityRepository;
}
/// <inheritdoc />
public async Task<EntityContainer?> GetAsync(Guid id)
{
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
return await Task.FromResult(_dataTypeContainerRepository.Get(id));
}
/// <inheritdoc />
public async Task<EntityContainer?> GetParentAsync(EntityContainer container)
=> await Task.FromResult(GetParent(container));
/// <inheritdoc />
public async Task<EntityContainer?> GetParentAsync(IDataType dataType)
=> await Task.FromResult(GetParent(dataType));
/// <inheritdoc />
public async Task<Attempt<EntityContainer, DataTypeContainerOperationStatus>> CreateAsync(EntityContainer container, Guid? parentId = null, int userId = Constants.Security.SuperUserId)
=> await SaveAsync(
container,
userId,
() =>
{
if (container.Id > 0)
{
return DataTypeContainerOperationStatus.InvalidId;
}
EntityContainer? parentContainer = parentId.HasValue
? _dataTypeContainerRepository.Get(parentId.Value)
: null;
if (parentId.HasValue && parentContainer == null)
{
return DataTypeContainerOperationStatus.ParentNotFound;
}
container.ParentId = parentContainer?.Id ?? Constants.System.Root;
return DataTypeContainerOperationStatus.Success;
},
AuditType.New);
/// <inheritdoc />
public async Task<Attempt<EntityContainer, DataTypeContainerOperationStatus>> UpdateAsync(EntityContainer container, int userId = Constants.Security.SuperUserId)
=> await SaveAsync(
container,
userId,
() =>
{
if (container.Id == 0)
{
return DataTypeContainerOperationStatus.InvalidId;
}
if (container.IsPropertyDirty(nameof(EntityContainer.ParentId)))
{
LoggerFactory.CreateLogger<DataTypeContainerService>().LogWarning($"Cannot use {nameof(UpdateAsync)} to change the container parent. Move the container instead.");
return DataTypeContainerOperationStatus.ParentNotFound;
}
return DataTypeContainerOperationStatus.Success;
},
AuditType.New);
/// <inheritdoc />
public async Task<Attempt<EntityContainer?, DataTypeContainerOperationStatus>> DeleteAsync(Guid id, int userId = Constants.Security.SuperUserId)
{
using ICoreScope scope = ScopeProvider.CreateCoreScope();
EntityContainer? container = _dataTypeContainerRepository.Get(id);
if (container == null)
{
return Attempt.FailWithStatus<EntityContainer?, DataTypeContainerOperationStatus>(DataTypeContainerOperationStatus.NotFound, null);
}
// 'container' here does not know about its children, so we need
// to get it again from the entity repository, as a light entity
IEntitySlim? entity = _entityRepository.Get(container.Id);
if (entity?.HasChildren is true)
{
scope.Complete();
return Attempt.FailWithStatus<EntityContainer?, DataTypeContainerOperationStatus>(DataTypeContainerOperationStatus.NotEmpty, container);
}
EventMessages eventMessages = EventMessagesFactory.Get();
var deletingEntityContainerNotification = new EntityContainerDeletingNotification(container, eventMessages);
if (await scope.Notifications.PublishCancelableAsync(deletingEntityContainerNotification))
{
scope.Complete();
return Attempt.FailWithStatus<EntityContainer?, DataTypeContainerOperationStatus>(DataTypeContainerOperationStatus.CancelledByNotification, container);
}
_dataTypeContainerRepository.Delete(container);
Audit(AuditType.Delete, userId, container.Id);
scope.Complete();
scope.Notifications.Publish(new EntityContainerDeletedNotification(container, eventMessages).WithStateFrom(deletingEntityContainerNotification));
return Attempt.SucceedWithStatus<EntityContainer?, DataTypeContainerOperationStatus>(DataTypeContainerOperationStatus.Success, container);
}
private async Task<Attempt<EntityContainer, DataTypeContainerOperationStatus>> SaveAsync(EntityContainer container, int userId, Func<DataTypeContainerOperationStatus> operationValidation, AuditType auditType)
{
if (container.ContainedObjectType != Constants.ObjectTypes.DataType)
{
return Attempt.FailWithStatus(DataTypeContainerOperationStatus.InvalidObjectType, container);
}
using ICoreScope scope = ScopeProvider.CreateCoreScope();
DataTypeContainerOperationStatus operationValidationStatus = operationValidation();
if (operationValidationStatus != DataTypeContainerOperationStatus.Success)
{
return Attempt.FailWithStatus(operationValidationStatus, container);
}
EventMessages eventMessages = EventMessagesFactory.Get();
var savingEntityContainerNotification = new EntityContainerSavingNotification(container, eventMessages);
if (await scope.Notifications.PublishCancelableAsync(savingEntityContainerNotification))
{
scope.Complete();
return Attempt.FailWithStatus(DataTypeContainerOperationStatus.CancelledByNotification, container);
}
_dataTypeContainerRepository.Save(container);
Audit(auditType, userId, container.Id);
scope.Complete();
scope.Notifications.Publish(new EntityContainerSavedNotification(container, eventMessages).WithStateFrom(savingEntityContainerNotification));
return Attempt.SucceedWithStatus(DataTypeContainerOperationStatus.Success, container);
}
private EntityContainer? GetParent(ITreeEntity treeEntity)
{
if (treeEntity.ParentId == Constants.System.Root)
{
return null;
}
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
return _dataTypeContainerRepository.Get(treeEntity.ParentId);
}
private void Audit(AuditType type, int userId, int objectId)
=> _auditRepository.Save(new AuditItem(objectId, type, userId, UmbracoObjectTypes.DataTypeContainer.GetName()));
}

View File

@@ -6,13 +6,13 @@ using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Exceptions;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Entities;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.Persistence.Querying;
using Umbraco.Cms.Core.Persistence.Repositories;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.Scoping;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Cms.Core.Strings;
using Umbraco.Extensions;
@@ -28,60 +28,11 @@ namespace Umbraco.Cms.Core.Services.Implement
private readonly IDataTypeContainerRepository _dataTypeContainerRepository;
private readonly IContentTypeRepository _contentTypeRepository;
private readonly IAuditRepository _auditRepository;
private readonly IEntityRepository _entityRepository;
private readonly IIOHelper _ioHelper;
private readonly ILocalizedTextService _localizedTextService;
private readonly ILocalizationService _localizationService;
private readonly IShortStringHelper _shortStringHelper;
private readonly IJsonSerializer _jsonSerializer;
private readonly IEditorConfigurationParser _editorConfigurationParser;
private readonly IDataTypeContainerService _dataTypeContainerService;
[Obsolete("Please use constructor that takes an ")]
public DataTypeService(
IDataValueEditorFactory dataValueEditorFactory,
ICoreScopeProvider provider,
ILoggerFactory loggerFactory,
IEventMessagesFactory eventMessagesFactory,
IDataTypeRepository dataTypeRepository,
IDataTypeContainerRepository dataTypeContainerRepository,
IAuditRepository auditRepository,
IEntityRepository entityRepository,
IContentTypeRepository contentTypeRepository,
IIOHelper ioHelper,
ILocalizedTextService localizedTextService,
ILocalizationService localizationService,
IShortStringHelper shortStringHelper,
IJsonSerializer jsonSerializer)
: this(
dataValueEditorFactory,
provider,
loggerFactory,
eventMessagesFactory,
dataTypeRepository,
dataTypeContainerRepository,
auditRepository,
entityRepository,
contentTypeRepository,
ioHelper,
localizedTextService,
localizationService,
shortStringHelper,
jsonSerializer,
StaticServiceProvider.Instance.GetRequiredService<IEditorConfigurationParser>())
{
_dataValueEditorFactory = dataValueEditorFactory;
_dataTypeRepository = dataTypeRepository;
_dataTypeContainerRepository = dataTypeContainerRepository;
_auditRepository = auditRepository;
_entityRepository = entityRepository;
_contentTypeRepository = contentTypeRepository;
_ioHelper = ioHelper;
_localizedTextService = localizedTextService;
_localizationService = localizationService;
_shortStringHelper = shortStringHelper;
_jsonSerializer = jsonSerializer;
}
[Obsolete("Please use the constructor that takes less parameters. Will be removed in V15.")]
public DataTypeService(
IDataValueEditorFactory dataValueEditorFactory,
ICoreScopeProvider provider,
@@ -98,31 +49,55 @@ namespace Umbraco.Cms.Core.Services.Implement
IShortStringHelper shortStringHelper,
IJsonSerializer jsonSerializer,
IEditorConfigurationParser editorConfigurationParser)
: this(
provider,
loggerFactory,
eventMessagesFactory,
dataTypeRepository,
dataValueEditorFactory,
auditRepository,
contentTypeRepository,
ioHelper,
editorConfigurationParser)
{
}
public DataTypeService(
ICoreScopeProvider provider,
ILoggerFactory loggerFactory,
IEventMessagesFactory eventMessagesFactory,
IDataTypeRepository dataTypeRepository,
IDataValueEditorFactory dataValueEditorFactory,
IAuditRepository auditRepository,
IContentTypeRepository contentTypeRepository,
IIOHelper ioHelper,
IEditorConfigurationParser editorConfigurationParser)
: base(provider, loggerFactory, eventMessagesFactory)
{
_dataValueEditorFactory = dataValueEditorFactory;
_dataTypeRepository = dataTypeRepository;
_dataTypeContainerRepository = dataTypeContainerRepository;
_auditRepository = auditRepository;
_entityRepository = entityRepository;
_contentTypeRepository = contentTypeRepository;
_ioHelper = ioHelper;
_localizedTextService = localizedTextService;
_localizationService = localizationService;
_shortStringHelper = shortStringHelper;
_jsonSerializer = jsonSerializer;
_editorConfigurationParser = editorConfigurationParser;
// resolve dependencies for obsolete methods through the static service provider, so they don't pollute the constructor signature
_dataTypeContainerService = StaticServiceProvider.Instance.GetRequiredService<IDataTypeContainerService>();
_dataTypeContainerRepository = StaticServiceProvider.Instance.GetRequiredService<IDataTypeContainerRepository>();
}
#region Containers
[Obsolete("Please use IDataTypeContainerService for all data type container operations. Will be removed in V15.")]
public Attempt<OperationResult<OperationResultType, EntityContainer>?> CreateContainer(int parentId, Guid key, string name, int userId = Constants.Security.SuperUserId)
{
EventMessages evtMsgs = EventMessagesFactory.Get();
using (ICoreScope scope = ScopeProvider.CreateCoreScope())
using (ScopeProvider.CreateCoreScope(autoComplete:true))
{
try
{
Guid? parentKey = parentId > 0 ? _dataTypeContainerRepository.Get(parentId)?.Key : null;
var container = new EntityContainer(Constants.ObjectTypes.DataType)
{
Name = name,
@@ -131,21 +106,15 @@ namespace Umbraco.Cms.Core.Services.Implement
Key = key
};
var savingEntityContainerNotification = new EntityContainerSavingNotification(container, evtMsgs);
if (scope.Notifications.PublishCancelable(savingEntityContainerNotification))
Attempt<EntityContainer, DataTypeContainerOperationStatus> result = _dataTypeContainerService.CreateAsync(container, parentKey, userId).GetAwaiter().GetResult();
// mimic old service behavior
return result.Status switch
{
scope.Complete();
return OperationResult.Attempt.Cancel(evtMsgs, container);
}
_dataTypeContainerRepository.Save(container);
scope.Complete();
scope.Notifications.Publish(new EntityContainerSavedNotification(container, evtMsgs).WithStateFrom(savingEntityContainerNotification));
// TODO: Audit trail ?
return OperationResult.Attempt.Succeed(evtMsgs, container);
DataTypeContainerOperationStatus.CancelledByNotification => OperationResult.Attempt.Cancel(evtMsgs, container),
DataTypeContainerOperationStatus.Success => OperationResult.Attempt.Succeed(evtMsgs, container),
_ => throw new InvalidOperationException($"Invalid operation status: {result.Status}")
};
}
catch (Exception ex)
{
@@ -154,24 +123,25 @@ namespace Umbraco.Cms.Core.Services.Implement
}
}
[Obsolete("Please use IDataTypeContainerService for all data type container operations. Will be removed in V15.")]
public EntityContainer? GetContainer(int containerId)
{
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
return _dataTypeContainerRepository.Get(containerId);
}
[Obsolete("Please use IDataTypeContainerService for all data type container operations. Will be removed in V15.")]
public EntityContainer? GetContainer(Guid containerId)
{
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
return _dataTypeContainerRepository.Get(containerId);
}
=> _dataTypeContainerService.GetAsync(containerId).GetAwaiter().GetResult();
[Obsolete("Please use IDataTypeContainerService for all data type container operations. Will be removed in V15.")]
public IEnumerable<EntityContainer> GetContainers(string name, int level)
{
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
return _dataTypeContainerRepository.Get(name, level);
}
[Obsolete("Please use IDataTypeContainerService for all data type container operations. Will be removed in V15.")]
public IEnumerable<EntityContainer> GetContainers(IDataType dataType)
{
var ancestorIds = dataType.Path.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries)
@@ -186,51 +156,43 @@ namespace Umbraco.Cms.Core.Services.Implement
return GetContainers(ancestorIds);
}
[Obsolete("Please use IDataTypeContainerService for all data type container operations. Will be removed in V15.")]
public IEnumerable<EntityContainer> GetContainers(int[] containerIds)
{
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
return _dataTypeContainerRepository.GetMany(containerIds);
}
[Obsolete("Please use IDataTypeContainerService for all data type container operations. Will be removed in V15.")]
public Attempt<OperationResult?> SaveContainer(EntityContainer container, int userId = Constants.Security.SuperUserId)
{
EventMessages evtMsgs = EventMessagesFactory.Get();
if (container.ContainedObjectType != Constants.ObjectTypes.DataType)
using (ScopeProvider.CreateCoreScope(autoComplete:true))
{
var ex = new InvalidOperationException("Not a " + Constants.ObjectTypes.DataType + " container.");
return OperationResult.Attempt.Fail(evtMsgs, ex);
}
var isNew = container.Id == 0;
Guid? parentKey = isNew && container.ParentId > 0 ? _dataTypeContainerRepository.Get(container.ParentId)?.Key : null;
if (container.HasIdentity && container.IsPropertyDirty("ParentId"))
{
var ex = new InvalidOperationException("Cannot save a container with a modified parent, move the container instead.");
return OperationResult.Attempt.Fail(evtMsgs, ex);
}
Attempt<EntityContainer, DataTypeContainerOperationStatus> result = isNew
? _dataTypeContainerService.CreateAsync(container, parentKey, userId).GetAwaiter().GetResult()
: _dataTypeContainerService.UpdateAsync(container, userId).GetAwaiter().GetResult();
using (ICoreScope scope = ScopeProvider.CreateCoreScope())
{
var savingEntityContainerNotification = new EntityContainerSavingNotification(container, evtMsgs);
if (scope.Notifications.PublishCancelable(savingEntityContainerNotification))
// mimic old service behavior
return result.Status switch
{
scope.Complete();
return OperationResult.Attempt.Cancel(evtMsgs);
}
_dataTypeContainerRepository.Save(container);
scope.Notifications.Publish(new EntityContainerSavedNotification(container, evtMsgs).WithStateFrom(savingEntityContainerNotification));
scope.Complete();
DataTypeContainerOperationStatus.Success => OperationResult.Attempt.Succeed(evtMsgs),
DataTypeContainerOperationStatus.CancelledByNotification => OperationResult.Attempt.Cancel(evtMsgs),
DataTypeContainerOperationStatus.ParentNotFound => OperationResult.Attempt.Fail(evtMsgs, new InvalidOperationException("Cannot save a container with a modified parent, move the container instead.")),
DataTypeContainerOperationStatus.InvalidObjectType => OperationResult.Attempt.Fail(evtMsgs, new InvalidOperationException("Not a " + Constants.ObjectTypes.DataType + " container.")),
_ => OperationResult.Attempt.Fail(evtMsgs, new InvalidOperationException($"Invalid operation status: {result.Status}"))
};
}
// TODO: Audit trail ?
return OperationResult.Attempt.Succeed(evtMsgs);
}
[Obsolete("Please use IDataTypeContainerService for all data type container operations. Will be removed in V15.")]
public Attempt<OperationResult?> DeleteContainer(int containerId, int userId = Constants.Security.SuperUserId)
{
EventMessages evtMsgs = EventMessagesFactory.Get();
using (ICoreScope scope = ScopeProvider.CreateCoreScope())
using (ScopeProvider.CreateCoreScope(autoComplete:true))
{
EntityContainer? container = _dataTypeContainerRepository.Get(containerId);
if (container == null)
@@ -238,36 +200,23 @@ namespace Umbraco.Cms.Core.Services.Implement
return OperationResult.Attempt.NoOperation(evtMsgs);
}
// 'container' here does not know about its children, so we need
// to get it again from the entity repository, as a light entity
IEntitySlim? entity = _entityRepository.Get(container.Id);
if (entity?.HasChildren ?? false)
Attempt<EntityContainer?, DataTypeContainerOperationStatus> result = _dataTypeContainerService.DeleteAsync(container.Key, userId).GetAwaiter().GetResult();
// mimic old service behavior
return result.Status switch
{
scope.Complete();
return Attempt.Fail(new OperationResult(OperationResultType.FailedCannot, evtMsgs));
}
var deletingEntityContainerNotification = new EntityContainerDeletingNotification(container, evtMsgs);
if (scope.Notifications.PublishCancelable(deletingEntityContainerNotification))
{
scope.Complete();
return Attempt.Fail(new OperationResult(OperationResultType.FailedCancelledByEvent, evtMsgs));
}
_dataTypeContainerRepository.Delete(container);
scope.Notifications.Publish(new EntityContainerDeletedNotification(container, evtMsgs).WithStateFrom(deletingEntityContainerNotification));
scope.Complete();
DataTypeContainerOperationStatus.Success => OperationResult.Attempt.Succeed(evtMsgs),
DataTypeContainerOperationStatus.NotEmpty => Attempt.Fail(new OperationResult(OperationResultType.FailedCannot, evtMsgs)),
DataTypeContainerOperationStatus.CancelledByNotification => Attempt.Fail(new OperationResult(OperationResultType.FailedCancelledByEvent, evtMsgs)),
_ => OperationResult.Attempt.Fail(evtMsgs, new InvalidOperationException($"Invalid operation status: {result.Status}"))
};
}
// TODO: Audit trail ?
return OperationResult.Attempt.Succeed(evtMsgs);
}
[Obsolete("Please use IDataTypeContainerService for all data type container operations. Will be removed in V15.")]
public Attempt<OperationResult<OperationResultType, EntityContainer>?> RenameContainer(int id, string name, int userId = Constants.Security.SuperUserId)
{
EventMessages evtMsgs = EventMessagesFactory.Get();
using (ICoreScope scope = ScopeProvider.CreateCoreScope())
using (ScopeProvider.CreateCoreScope(autoComplete:true))
{
try
{
@@ -281,19 +230,14 @@ namespace Umbraco.Cms.Core.Services.Implement
container.Name = name;
var renamingEntityContainerNotification = new EntityContainerRenamingNotification(container, evtMsgs);
if (scope.Notifications.PublishCancelable(renamingEntityContainerNotification))
Attempt<EntityContainer, DataTypeContainerOperationStatus> result = _dataTypeContainerService.UpdateAsync(container, userId).GetAwaiter().GetResult();
// mimic old service behavior
return result.Status switch
{
scope.Complete();
return OperationResult.Attempt.Cancel(evtMsgs, container);
}
_dataTypeContainerRepository.Save(container);
scope.Complete();
scope.Notifications.Publish(new EntityContainerRenamedNotification(container, evtMsgs).WithStateFrom(renamingEntityContainerNotification));
return OperationResult.Attempt.Succeed(OperationResultType.Success, evtMsgs, container);
DataTypeContainerOperationStatus.Success => OperationResult.Attempt.Succeed(OperationResultType.Success, evtMsgs, container),
DataTypeContainerOperationStatus.CancelledByNotification => OperationResult.Attempt.Cancel(evtMsgs, container),
_ => OperationResult.Attempt.Fail<EntityContainer>(evtMsgs, new InvalidOperationException($"Invalid operation status: {result.Status}"))
};
}
catch (Exception ex)
{
@@ -309,12 +253,17 @@ namespace Umbraco.Cms.Core.Services.Implement
/// </summary>
/// <param name="name">Name of the <see cref="IDataType"/></param>
/// <returns><see cref="IDataType"/></returns>
[Obsolete("Please use GetAsync. Will be removed in V15.")]
public IDataType? GetDataType(string name)
=> GetAsync(name).GetAwaiter().GetResult();
/// <inheritdoc />
public async Task<IDataType?> GetAsync(string name)
{
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
IDataType? dataType = _dataTypeRepository.Get(Query<IDataType>().Where(x => x.Name == name))?.FirstOrDefault();
ConvertMissingEditorOfDataTypeToLabel(dataType);
return dataType;
return await Task.FromResult(dataType);
}
/// <summary>
@@ -322,6 +271,7 @@ namespace Umbraco.Cms.Core.Services.Implement
/// </summary>
/// <param name="id">Id of the <see cref="IDataType"/></param>
/// <returns><see cref="IDataType"/></returns>
[Obsolete("Please use GetAsync. Will be removed in V15.")]
public IDataType? GetDataType(int id)
{
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
@@ -335,13 +285,17 @@ namespace Umbraco.Cms.Core.Services.Implement
/// </summary>
/// <param name="id">Unique guid Id of the DataType</param>
/// <returns><see cref="IDataType"/></returns>
[Obsolete("Please use GetAsync. Will be removed in V15.")]
public IDataType? GetDataType(Guid id)
=> GetAsync(id).GetAwaiter().GetResult();
/// <inheritdoc />
public async Task<IDataType?> GetAsync(Guid id)
{
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
IQuery<IDataType> query = Query<IDataType>().Where(x => x.Key == id);
IDataType? dataType = _dataTypeRepository.Get(query).FirstOrDefault();
IDataType? dataType = GetDataTypeFromRepository(id);
ConvertMissingEditorOfDataTypeToLabel(dataType);
return dataType;
return await Task.FromResult(dataType);
}
/// <summary>
@@ -349,13 +303,18 @@ namespace Umbraco.Cms.Core.Services.Implement
/// </summary>
/// <param name="propertyEditorAlias">Alias of the property editor</param>
/// <returns>Collection of <see cref="IDataType"/> objects with a matching control id</returns>
[Obsolete("Please use GetByEditorAliasAsync. Will be removed in V15.")]
public IEnumerable<IDataType> GetByEditorAlias(string propertyEditorAlias)
=> GetByEditorAliasAsync(propertyEditorAlias).GetAwaiter().GetResult();
/// <inheritdoc />
public async Task<IEnumerable<IDataType>> GetByEditorAliasAsync(string propertyEditorAlias)
{
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
IQuery<IDataType> query = Query<IDataType>().Where(x => x.EditorAlias == propertyEditorAlias);
IEnumerable<IDataType> dataType = _dataTypeRepository.Get(query).ToArray();
ConvertMissingEditorsOfDataTypesToLabels(dataType);
return dataType;
IEnumerable<IDataType> dataTypes = _dataTypeRepository.Get(query).ToArray();
ConvertMissingEditorsOfDataTypesToLabels(dataTypes);
return await Task.FromResult(dataTypes);
}
/// <summary>
@@ -485,19 +444,7 @@ namespace Umbraco.Cms.Core.Services.Implement
/// <param name="userId">Id of the user issuing the save</param>
public void Save(IDataType dataType, int userId = Constants.Security.SuperUserId)
{
EventMessages evtMsgs = EventMessagesFactory.Get();
dataType.CreatorId = userId;
using ICoreScope scope = ScopeProvider.CreateCoreScope();
var saveEventArgs = new SaveEventArgs<IDataType>(dataType);
var savingDataTypeNotification = new DataTypeSavingNotification(dataType, evtMsgs);
if (scope.Notifications.PublishCancelable(savingDataTypeNotification))
{
scope.Complete();
return;
}
// mimic old service behavior
if (string.IsNullOrWhiteSpace(dataType.Name))
{
throw new ArgumentException("Cannot save datatype with empty name.");
@@ -508,19 +455,44 @@ namespace Umbraco.Cms.Core.Services.Implement
throw new InvalidOperationException("Name cannot be more than 255 characters in length.");
}
_dataTypeRepository.Save(dataType);
scope.Notifications.Publish(new DataTypeSavedNotification(dataType, evtMsgs).WithStateFrom(savingDataTypeNotification));
Audit(AuditType.Save, userId, dataType.Id);
scope.Complete();
Attempt<IDataType, DataTypeOperationStatus> result = SaveAsync(
dataType,
() => DataTypeOperationStatus.Success,
userId,
AuditType.Sort).GetAwaiter().GetResult();
}
/// <inheritdoc />
public async Task<Attempt<IDataType, DataTypeOperationStatus>> CreateAsync(IDataType dataType, int userId = Constants.Security.SuperUserId)
{
if (dataType.Id != 0)
{
return Attempt.FailWithStatus(DataTypeOperationStatus.InvalidId, dataType);
}
return await SaveAsync(dataType, () => DataTypeOperationStatus.Success, userId, AuditType.New);
}
/// <inheritdoc />
public async Task<Attempt<IDataType, DataTypeOperationStatus>> UpdateAsync(IDataType dataType, int userId = Constants.Security.SuperUserId)
=> await SaveAsync(
dataType,
() =>
{
IDataType? current = _dataTypeRepository.Get(dataType.Id);
return current == null
? DataTypeOperationStatus.NotFound
: DataTypeOperationStatus.Success;
},
userId,
AuditType.New);
/// <summary>
/// Saves a collection of <see cref="IDataType"/>
/// </summary>
/// <param name="dataTypeDefinitions"><see cref="IDataType"/> to save</param>
/// <param name="userId">Id of the user issuing the save</param>
[Obsolete("Please use CreateAsync or UpdateAsync. Will be removed in V15.")]
public void Save(IEnumerable<IDataType> dataTypeDefinitions, int userId)
{
EventMessages evtMsgs = EventMessagesFactory.Get();
@@ -557,14 +529,25 @@ namespace Umbraco.Cms.Core.Services.Implement
/// <param name="dataType"><see cref="IDataType"/> to delete</param>
/// <param name="userId">Optional Id of the user issuing the deletion</param>
public void Delete(IDataType dataType, int userId = Constants.Security.SuperUserId)
=> DeleteAsync(dataType.Key, userId).GetAwaiter().GetResult();
/// <inheritdoc />
public async Task<Attempt<IDataType?, DataTypeOperationStatus>> DeleteAsync(Guid id, int userId = Constants.Security.SuperUserId)
{
EventMessages evtMsgs = EventMessagesFactory.Get();
EventMessages eventMessages = EventMessagesFactory.Get();
using ICoreScope scope = ScopeProvider.CreateCoreScope();
var deletingDataTypeNotification = new DataTypeDeletingNotification(dataType, evtMsgs);
if (scope.Notifications.PublishCancelable(deletingDataTypeNotification))
IDataType? dataType = GetDataTypeFromRepository(id);
if (dataType == null)
{
return Attempt.FailWithStatus(DataTypeOperationStatus.NotFound, dataType);
}
var deletingDataTypeNotification = new DataTypeDeletingNotification(dataType, eventMessages);
if (await scope.Notifications.PublishCancelableAsync(deletingDataTypeNotification))
{
scope.Complete();
return;
return Attempt.FailWithStatus<IDataType?, DataTypeOperationStatus>(DataTypeOperationStatus.CancelledByNotification, dataType);
}
// find ContentTypes using this IDataTypeDefinition on a PropertyType, and delete
@@ -599,19 +582,37 @@ namespace Umbraco.Cms.Core.Services.Implement
_dataTypeRepository.Delete(dataType);
scope.Notifications.Publish(new DataTypeDeletedNotification(dataType, evtMsgs).WithStateFrom(deletingDataTypeNotification));
scope.Notifications.Publish(new DataTypeDeletedNotification(dataType, eventMessages).WithStateFrom(deletingDataTypeNotification));
Audit(AuditType.Delete, userId, dataType.Id);
scope.Complete();
return Attempt.SucceedWithStatus<IDataType?, DataTypeOperationStatus>(DataTypeOperationStatus.Success, dataType);
}
/// <inheritdoc />
[Obsolete("Please use GetReferencesAsync. Will be deleted in V15.")]
public IReadOnlyDictionary<Udi, IEnumerable<string>> GetReferences(int id)
{
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete:true);
return _dataTypeRepository.FindUsages(id);
}
/// <inheritdoc />
public async Task<Attempt<IReadOnlyDictionary<Udi, IEnumerable<string>>, DataTypeOperationStatus>> GetReferencesAsync(Guid id)
{
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete:true);
IDataType? dataType = GetDataTypeFromRepository(id);
if (dataType == null)
{
return Attempt.FailWithStatus<IReadOnlyDictionary<Udi, IEnumerable<string>>, DataTypeOperationStatus>(DataTypeOperationStatus.NotFound, new Dictionary<Udi, IEnumerable<string>>());
}
IReadOnlyDictionary<Udi, IEnumerable<string>> usages = _dataTypeRepository.FindUsages(dataType.Id);
return await Task.FromResult(Attempt.SucceedWithStatus(DataTypeOperationStatus.Success, usages));
}
/// <inheritdoc />
public IEnumerable<ValidationResult> ValidateConfigurationData(IDataType dataType)
{
@@ -624,10 +625,67 @@ namespace Umbraco.Cms.Core.Services.Implement
: configurationEditor.Validate(dataType.ConfigurationData);
}
private void Audit(AuditType type, int userId, int objectId)
private async Task<Attempt<IDataType, DataTypeOperationStatus>> SaveAsync(
IDataType dataType,
Func<DataTypeOperationStatus> operationValidation,
int userId,
AuditType auditType)
{
_auditRepository.Save(new AuditItem(objectId, type, userId, ObjectTypes.GetName(UmbracoObjectTypes.DataType)));
IEnumerable<ValidationResult> validationResults = ValidateConfigurationData(dataType);
if (validationResults.Any())
{
LoggerFactory
.CreateLogger<DataTypeService>()
.LogError($"Invalid data type configuration for data type: {dataType.Name} - validation error messages: {string.Join(Environment.NewLine, validationResults.Select(r => r.ErrorMessage))}");
return Attempt.FailWithStatus(DataTypeOperationStatus.InvalidConfiguration, dataType);
}
EventMessages eventMessages = EventMessagesFactory.Get();
dataType.CreatorId = userId;
using ICoreScope scope = ScopeProvider.CreateCoreScope();
DataTypeOperationStatus status = operationValidation();
if (status != DataTypeOperationStatus.Success)
{
return Attempt.FailWithStatus(status, dataType);
}
var savingDataTypeNotification = new DataTypeSavingNotification(dataType, eventMessages);
if (await scope.Notifications.PublishCancelableAsync(savingDataTypeNotification))
{
scope.Complete();
return Attempt.FailWithStatus(DataTypeOperationStatus.CancelledByNotification, dataType);
}
if (string.IsNullOrWhiteSpace(dataType.Name))
{
return Attempt.FailWithStatus(DataTypeOperationStatus.InvalidName, dataType);
}
if (dataType.Name is { Length: > 255 })
{
return Attempt.FailWithStatus(DataTypeOperationStatus.InvalidName, dataType);
}
_dataTypeRepository.Save(dataType);
scope.Notifications.Publish(new DataTypeSavedNotification(dataType, eventMessages).WithStateFrom(savingDataTypeNotification));
Audit(auditType, userId, dataType.Id);
scope.Complete();
return Attempt.SucceedWithStatus(DataTypeOperationStatus.Success, dataType);
}
private IDataType? GetDataTypeFromRepository(Guid id)
{
IQuery<IDataType> query = Query<IDataType>().Where(x => x.Key == id);
IDataType? dataType = _dataTypeRepository.Get(query).FirstOrDefault();
return dataType;
}
private void Audit(AuditType type, int userId, int objectId)
=> _auditRepository.Save(new AuditItem(objectId, type, userId, ObjectTypes.GetName(UmbracoObjectTypes.DataType)));
}
}

View File

@@ -0,0 +1,47 @@
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Core.Services;
public interface IDataTypeContainerService
{
/// <summary>
/// Gets a data type container
/// </summary>
/// <param name="id">The ID of the data type container to get.</param>
/// <returns></returns>
Task<EntityContainer?> GetAsync(Guid id);
/// <summary>
/// Gets the parent container of a data type container
/// </summary>
/// <param name="container">The container whose parent container to get.</param>
/// <returns></returns>
Task<EntityContainer?> GetParentAsync(EntityContainer container);
/// <summary>
/// Creates a new data type container
/// </summary>
/// <param name="container">The container to create.</param>
/// <param name="parentId">The ID of the parent container to create the new container under.</param>
/// <param name="userId">The ID of the user issuing the creation.</param>
/// <returns></returns>
/// <remarks>If no parent ID is supplied, the container will be created at the data type tree root.</remarks>
Task<Attempt<EntityContainer, DataTypeContainerOperationStatus>> CreateAsync(EntityContainer container, Guid? parentId = null, int userId = Constants.Security.SuperUserId);
/// <summary>
/// Updates an existing data type container
/// </summary>
/// <param name="container">The container to create.</param>
/// <param name="userId">The ID of the user issuing the update.</param>
/// <returns></returns>
Task<Attempt<EntityContainer, DataTypeContainerOperationStatus>> UpdateAsync(EntityContainer container, int userId = Constants.Security.SuperUserId);
/// <summary>
/// Deletes a data type container
/// </summary>
/// <param name="id">The ID of the container to delete.</param>
/// <param name="userId">The ID of the user issuing the deletion.</param>
/// <returns></returns>
Task<Attempt<EntityContainer?, DataTypeContainerOperationStatus>> DeleteAsync(Guid id, int userId = Constants.Security.SuperUserId);
}

View File

@@ -1,5 +1,6 @@
using System.ComponentModel.DataAnnotations;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Core.Services;
@@ -14,24 +15,41 @@ public interface IDataTypeService : IService
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
[Obsolete("Please use GetReferencesAsync. Will be deleted in V15.")]
IReadOnlyDictionary<Udi, IEnumerable<string>> GetReferences(int id);
/// <summary>
/// Returns a dictionary of content type <see cref="Udi" />s and the property type aliases that use a <see cref="IDataType" />
/// </summary>
/// <param name="id">The guid Id of the <see cref="IDataType" /></param>
/// <returns></returns>
Task<Attempt<IReadOnlyDictionary<Udi, IEnumerable<string>>, DataTypeOperationStatus>> GetReferencesAsync(Guid id);
[Obsolete("Please use IDataTypeContainerService for all data type container operations. Will be removed in V15.")]
Attempt<OperationResult<OperationResultType, EntityContainer>?> CreateContainer(int parentId, Guid key, string name, int userId = Constants.Security.SuperUserId);
[Obsolete("Please use IDataTypeContainerService for all data type container operations. Will be removed in V15.")]
Attempt<OperationResult?> SaveContainer(EntityContainer container, int userId = Constants.Security.SuperUserId);
[Obsolete("Please use IDataTypeContainerService for all data type container operations. Will be removed in V15.")]
EntityContainer? GetContainer(int containerId);
[Obsolete("Please use IDataTypeContainerService for all data type container operations. Will be removed in V15.")]
EntityContainer? GetContainer(Guid containerId);
[Obsolete("Please use IDataTypeContainerService for all data type container operations. Will be removed in V15.")]
IEnumerable<EntityContainer> GetContainers(string folderName, int level);
[Obsolete("Please use IDataTypeContainerService for all data type container operations. Will be removed in V15.")]
IEnumerable<EntityContainer> GetContainers(IDataType dataType);
[Obsolete("Please use IDataTypeContainerService for all data type container operations. Will be removed in V15.")]
IEnumerable<EntityContainer> GetContainers(int[] containerIds);
[Obsolete("Please use IDataTypeContainerService for all data type container operations. Will be removed in V15.")]
Attempt<OperationResult?> DeleteContainer(int containerId, int userId = Constants.Security.SuperUserId);
[Obsolete("Please use IDataTypeContainerService for all data type container operations. Will be removed in V15.")]
Attempt<OperationResult<OperationResultType, EntityContainer>?> RenameContainer(int id, string name, int userId = Constants.Security.SuperUserId);
/// <summary>
@@ -41,6 +59,7 @@ public interface IDataTypeService : IService
/// <returns>
/// <see cref="IDataType" />
/// </returns>
[Obsolete("Please use GetAsync. Will be removed in V15.")]
IDataType? GetDataType(string name);
/// <summary>
@@ -50,6 +69,7 @@ public interface IDataTypeService : IService
/// <returns>
/// <see cref="IDataType" />
/// </returns>
[Obsolete("Please use GetAsync. Will be removed in V15.")]
IDataType? GetDataType(int id);
/// <summary>
@@ -59,8 +79,27 @@ public interface IDataTypeService : IService
/// <returns>
/// <see cref="IDataType" />
/// </returns>
[Obsolete("Please use GetAsync. Will be removed in V15.")]
IDataType? GetDataType(Guid id);
/// <summary>
/// Gets an <see cref="IDataType" /> by its Name
/// </summary>
/// <param name="name">Name of the <see cref="IDataType" /></param>
/// <returns>
/// <see cref="IDataType" />
/// </returns>
Task<IDataType?> GetAsync(string name);
/// <summary>
/// Gets an <see cref="IDataType" /> by its unique guid Id
/// </summary>
/// <param name="id">Unique guid Id of the DataType</param>
/// <returns>
/// <see cref="IDataType" />
/// </returns>
Task<IDataType?> GetAsync(Guid id);
/// <summary>
/// Gets all <see cref="IDataType" /> objects or those with the ids passed in
/// </summary>
@@ -73,6 +112,7 @@ public interface IDataTypeService : IService
/// </summary>
/// <param name="dataType"><see cref="IDataType" /> to save</param>
/// <param name="userId">Id of the user issuing the save</param>
[Obsolete("Please use CreateAsync or UpdateAsync. Will be removed in V15.")]
void Save(IDataType dataType, int userId = Constants.Security.SuperUserId);
/// <summary>
@@ -80,8 +120,23 @@ public interface IDataTypeService : IService
/// </summary>
/// <param name="dataTypeDefinitions"><see cref="IDataType" /> to save</param>
/// <param name="userId">Id of the user issuing the save</param>
[Obsolete("Please use CreateAsync or UpdateAsync. Will be removed in V15.")]
void Save(IEnumerable<IDataType> dataTypeDefinitions, int userId = Constants.Security.SuperUserId);
/// <summary>
/// Creates a new <see cref="IDataType" />
/// </summary>
/// <param name="dataType"><see cref="IDataType" /> to create</param>
/// <param name="userId">Id of the user issuing the creation</param>
Task<Attempt<IDataType, DataTypeOperationStatus>> CreateAsync(IDataType dataType, int userId = Constants.Security.SuperUserId);
/// <summary>
/// Updates an existing <see cref="IDataType" />
/// </summary>
/// <param name="dataType"><see cref="IDataType" /> to update</param>
/// <param name="userId">Id of the user issuing the update</param>
Task<Attempt<IDataType, DataTypeOperationStatus>> UpdateAsync(IDataType dataType, int userId = Constants.Security.SuperUserId);
/// <summary>
/// Deletes an <see cref="IDataType" />
/// </summary>
@@ -91,15 +146,35 @@ public interface IDataTypeService : IService
/// </remarks>
/// <param name="dataType"><see cref="IDataType" /> to delete</param>
/// <param name="userId">Id of the user issuing the deletion</param>
[Obsolete("Please use DeleteAsync. Will be removed in V15.")]
void Delete(IDataType dataType, int userId = Constants.Security.SuperUserId);
/// <summary>
/// Deletes an <see cref="IDataType" />
/// </summary>
/// <remarks>
/// Please note that deleting a <see cref="IDataType" /> will remove
/// all the <see cref="IPropertyType" /> data that references this <see cref="IDataType" />.
/// </remarks>
/// <param name="id">The guid Id of the <see cref="IDataType" /> to delete</param>
/// <param name="userId">Id of the user issuing the deletion</param>
Task<Attempt<IDataType?, DataTypeOperationStatus>> DeleteAsync(Guid id, int userId = Constants.Security.SuperUserId);
/// <summary>
/// Gets a <see cref="IDataType" /> by its control Id
/// </summary>
/// <param name="propertyEditorAlias">Alias of the property editor</param>
/// <returns>Collection of <see cref="IDataType" /> objects with a matching control id</returns>
[Obsolete("Please use GetByEditorAliasAsync. Will be removed in V15.")]
IEnumerable<IDataType> GetByEditorAlias(string propertyEditorAlias);
/// <summary>
/// Gets all <see cref="IDataType" /> for a given property editor
/// </summary>
/// <param name="propertyEditorAlias">Alias of the property editor</param>
/// <returns>Collection of <see cref="IDataType" /> configured for the property editor</returns>
Task<IEnumerable<IDataType>> GetByEditorAliasAsync(string propertyEditorAlias);
Attempt<OperationResult<MoveOperationStatusType>?> Move(IDataType toMove, int parentId);
[Obsolete("Use the method which specifies the userId parameter")]

View File

@@ -0,0 +1,12 @@
namespace Umbraco.Cms.Core.Services.OperationStatus;
public enum DataTypeContainerOperationStatus
{
Success,
CancelledByNotification,
InvalidObjectType,
InvalidId,
NotFound,
ParentNotFound,
NotEmpty
}

View File

@@ -0,0 +1,11 @@
namespace Umbraco.Cms.Core.Services.OperationStatus;
public enum DataTypeOperationStatus
{
Success,
CancelledByNotification,
InvalidConfiguration,
InvalidName,
InvalidId,
NotFound
}

View File

@@ -1,5 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<Suppressions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services.DataTypeServiceTests.DataTypeService_Can_Persist_New_DataTypeDefinition</Target>
<Left>lib/net7.0/Umbraco.Tests.Integration.dll</Left>
<Right>lib/net7.0/Umbraco.Tests.Integration.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services.LocalizationServiceTests.Can_Create_DictionaryItem_At_Root_With_Identity</Target>

View File

@@ -0,0 +1,281 @@
using NUnit.Framework;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Cms.Infrastructure.Services;
using Umbraco.Cms.Tests.Common.Testing;
using Umbraco.Cms.Tests.Integration.Testing;
namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services;
/// <summary>
/// Tests covering the DataTypeContainerService
/// </summary>
[TestFixture]
[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)]
public class DataTypeContainerServiceTests : UmbracoIntegrationTest
{
private IDataTypeContainerService DataTypeContainerService => GetRequiredService<IDataTypeContainerService>();
private IDataTypeService DataTypeService => GetRequiredService<IDataTypeService>();
private IDataValueEditorFactory DataValueEditorFactory => GetRequiredService<IDataValueEditorFactory>();
private IConfigurationEditorJsonSerializer ConfigurationEditorJsonSerializer => GetRequiredService<IConfigurationEditorJsonSerializer>();
private IEditorConfigurationParser EditorConfigurationParser => GetRequiredService<IEditorConfigurationParser>();
[Test]
public async Task Can_Create_Container_At_Root()
{
EntityContainer toCreate = new EntityContainer(Constants.ObjectTypes.DataType) { Name = "Root Container" };
var result = await DataTypeContainerService.CreateAsync(toCreate);
Assert.IsTrue(result.Success);
Assert.AreEqual(DataTypeContainerOperationStatus.Success, result.Status);
var created = await DataTypeContainerService.GetAsync(toCreate.Key);
Assert.NotNull(created);
Assert.AreEqual("Root Container", created.Name);
Assert.AreEqual(Constants.System.Root, created.ParentId);
}
[Test]
public async Task Can_Create_Child_Container()
{
EntityContainer root = new EntityContainer(Constants.ObjectTypes.DataType) { Name = "Root Container" };
await DataTypeContainerService.CreateAsync(root);
EntityContainer child = new EntityContainer(Constants.ObjectTypes.DataType) { Name = "Child Container" };
var result = await DataTypeContainerService.CreateAsync(child, root.Key);
Assert.IsTrue(result.Success);
Assert.AreEqual(DataTypeContainerOperationStatus.Success, result.Status);
var created = await DataTypeContainerService.GetAsync(child.Key);
Assert.NotNull(created);
Assert.AreEqual("Child Container", created.Name);
Assert.AreEqual(root.Id, child.ParentId);
}
[Test]
public async Task Can_Update_Container_At_Root()
{
EntityContainer root = new EntityContainer(Constants.ObjectTypes.DataType) { Name = "Root Container" };
await DataTypeContainerService.CreateAsync(root);
EntityContainer toUpdate = await DataTypeContainerService.GetAsync(root.Key);
Assert.NotNull(toUpdate);
toUpdate.Name += " UPDATED";
var result = await DataTypeContainerService.UpdateAsync(toUpdate);
Assert.IsTrue(result.Success);
Assert.AreEqual(DataTypeContainerOperationStatus.Success, result.Status);
var updated = await DataTypeContainerService.GetAsync(toUpdate.Key);
Assert.NotNull(updated);
Assert.AreEqual("Root Container UPDATED", updated.Name);
Assert.AreEqual(Constants.System.Root, updated.ParentId);
}
[Test]
public async Task Can_Update_Child_Container()
{
EntityContainer root = new EntityContainer(Constants.ObjectTypes.DataType) { Name = "Root Container" };
await DataTypeContainerService.CreateAsync(root);
EntityContainer child = new EntityContainer(Constants.ObjectTypes.DataType) { Name = "Child Container" };
await DataTypeContainerService.CreateAsync(child, root.Key);
EntityContainer toUpdate = await DataTypeContainerService.GetAsync(child.Key);
Assert.NotNull(toUpdate);
toUpdate.Name += " UPDATED";
var result = await DataTypeContainerService.UpdateAsync(toUpdate);
Assert.IsTrue(result.Success);
Assert.AreEqual(DataTypeContainerOperationStatus.Success, result.Status);
var updated = await DataTypeContainerService.GetAsync(toUpdate.Key);
Assert.NotNull(updated);
Assert.AreEqual("Child Container UPDATED", updated.Name);
Assert.AreEqual(root.Id, updated.ParentId);
}
[Test]
public async Task Can_Get_Container_At_Root()
{
EntityContainer root = new EntityContainer(Constants.ObjectTypes.DataType) { Name = "Root Container" };
await DataTypeContainerService.CreateAsync(root);
var created = await DataTypeContainerService.GetAsync(root.Key);
Assert.NotNull(created);
Assert.AreEqual("Root Container", created.Name);
Assert.AreEqual(Constants.System.Root, created.ParentId);
}
[Test]
public async Task Can_Get_Child_Container()
{
EntityContainer root = new EntityContainer(Constants.ObjectTypes.DataType) { Name = "Root Container" };
await DataTypeContainerService.CreateAsync(root);
EntityContainer child = new EntityContainer(Constants.ObjectTypes.DataType) { Name = "Child Container" };
await DataTypeContainerService.CreateAsync(child, root.Key);
var created = await DataTypeContainerService.GetAsync(child.Key);
Assert.IsNotNull(created);
Assert.AreEqual("Child Container", created.Name);
Assert.AreEqual(root.Id, child.ParentId);
}
[Test]
public async Task Can_Delete_Container_At_Root()
{
EntityContainer root = new EntityContainer(Constants.ObjectTypes.DataType) { Name = "Root Container" };
await DataTypeContainerService.CreateAsync(root);
var result = await DataTypeContainerService.DeleteAsync(root.Key);
Assert.IsTrue(result.Success);
Assert.AreEqual(DataTypeContainerOperationStatus.Success, result.Status);
var current = await DataTypeContainerService.GetAsync(root.Key);
Assert.IsNull(current);
}
[Test]
public async Task Can_Delete_Child_Container()
{
EntityContainer root = new EntityContainer(Constants.ObjectTypes.DataType) { Name = "Root Container" };
await DataTypeContainerService.CreateAsync(root);
EntityContainer child = new EntityContainer(Constants.ObjectTypes.DataType) { Name = "Child Container" };
await DataTypeContainerService.CreateAsync(child, root.Key);
var result = await DataTypeContainerService.DeleteAsync(child.Key);
Assert.IsTrue(result.Success);
Assert.AreEqual(DataTypeContainerOperationStatus.Success, result.Status);
var current = await DataTypeContainerService.GetAsync(child.Key);
Assert.IsNull(current);
current = await DataTypeContainerService.GetAsync(root.Key);
Assert.IsNotNull(current);
}
[Test]
public async Task Cannot_Create_Child_Container_Below_Invalid_Parent()
{
EntityContainer child = new EntityContainer(Constants.ObjectTypes.DataType) { Name = "Child Container" };
var result = await DataTypeContainerService.CreateAsync(child, Guid.NewGuid());
Assert.IsFalse(result.Success);
Assert.AreEqual(DataTypeContainerOperationStatus.ParentNotFound, result.Status);
var created = await DataTypeContainerService.GetAsync(child.Key);
Assert.IsNull(created);
}
[Test]
public async Task Cannot_Create_Child_Container_With_Explicit_Id()
{
EntityContainer toCreate = new EntityContainer(Constants.ObjectTypes.DataType) { Name = "Root Container", Id = 1234 };
var result = await DataTypeContainerService.CreateAsync(toCreate);
Assert.IsFalse(result.Success);
Assert.AreEqual(DataTypeContainerOperationStatus.InvalidId, result.Status);
var created = await DataTypeContainerService.GetAsync(toCreate.Key);
Assert.IsNull(created);
}
[TestCase(Constants.ObjectTypes.Strings.DocumentType)]
[TestCase(Constants.ObjectTypes.Strings.MediaType)]
public async Task Cannot_Create_Container_With_Invalid_Contained_Type(string containedObjectType)
{
EntityContainer toCreate = new EntityContainer(new Guid(containedObjectType)) { Name = "Root Container" };
var result = await DataTypeContainerService.CreateAsync(toCreate);
Assert.IsFalse(result.Success);
Assert.AreEqual(DataTypeContainerOperationStatus.InvalidObjectType, result.Status);
var created = await DataTypeContainerService.GetAsync(toCreate.Key);
Assert.IsNull(created);
}
[Test]
public async Task Cannot_Update_Container_Parent()
{
EntityContainer root = new EntityContainer(Constants.ObjectTypes.DataType) { Name = "Root Container" };
await DataTypeContainerService.CreateAsync(root);
EntityContainer root2 = new EntityContainer(Constants.ObjectTypes.DataType) { Name = "Root Container 2" };
await DataTypeContainerService.CreateAsync(root2);
EntityContainer child = new EntityContainer(Constants.ObjectTypes.DataType) { Name = "Child Container" };
await DataTypeContainerService.CreateAsync(child, root.Key);
EntityContainer toUpdate = await DataTypeContainerService.GetAsync(child.Key);
Assert.IsNotNull(toUpdate);
toUpdate.ParentId = root2.Id;
var result = await DataTypeContainerService.UpdateAsync(toUpdate);
Assert.IsFalse(result.Success);
Assert.AreEqual(DataTypeContainerOperationStatus.ParentNotFound, result.Status);
var current = await DataTypeContainerService.GetAsync(child.Key);
Assert.IsNotNull(current);
Assert.AreEqual(root.Id, child.ParentId);
}
[Test]
public async Task Cannot_Delete_Container_With_Child_Container()
{
EntityContainer root = new EntityContainer(Constants.ObjectTypes.DataType) { Name = "Root Container" };
await DataTypeContainerService.CreateAsync(root);
EntityContainer child = new EntityContainer(Constants.ObjectTypes.DataType) { Name = "Child Container" };
await DataTypeContainerService.CreateAsync(child, root.Key);
var result = await DataTypeContainerService.DeleteAsync(root.Key);
Assert.IsFalse(result.Success);
Assert.AreEqual(DataTypeContainerOperationStatus.NotEmpty, result.Status);
var current = await DataTypeContainerService.GetAsync(root.Key);
Assert.IsNotNull(current);
}
[Test]
public async Task Cannot_Delete_Container_With_Child_DataType()
{
EntityContainer container = new EntityContainer(Constants.ObjectTypes.DataType) { Name = "Root Container" };
await DataTypeContainerService.CreateAsync(container);
IDataType dataType =
new DataType(new TextboxPropertyEditor(DataValueEditorFactory, IOHelper, EditorConfigurationParser), ConfigurationEditorJsonSerializer)
{
Name = Guid.NewGuid().ToString(),
DatabaseType = ValueStorageType.Nvarchar,
ParentId = container.Id
};
await DataTypeService.CreateAsync(dataType);
var result = await DataTypeContainerService.DeleteAsync(container.Key);
Assert.IsFalse(result.Success);
Assert.AreEqual(DataTypeContainerOperationStatus.NotEmpty, result.Status);
var currentContainer = await DataTypeContainerService.GetAsync(container.Key);
Assert.IsNotNull(currentContainer);
var currentDataType = await DataTypeService.GetAsync(dataType.Key);
Assert.IsNotNull(currentDataType);
}
[Test]
public async Task Cannot_Delete_Non_Existing_Container()
{
var result = await DataTypeContainerService.DeleteAsync(Guid.NewGuid());
Assert.IsFalse(result.Success);
Assert.AreEqual(DataTypeContainerOperationStatus.NotFound, result.Status);
}
}

View File

@@ -1,13 +1,13 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using System;
using System.Linq;
using NUnit.Framework;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Cms.Tests.Common.Builders;
using Umbraco.Cms.Tests.Common.Testing;
using Umbraco.Cms.Tests.Integration.Testing;
@@ -31,8 +31,10 @@ public class DataTypeServiceTests : UmbracoIntegrationTest
private IConfigurationEditorJsonSerializer ConfigurationEditorJsonSerializer =>
GetRequiredService<IConfigurationEditorJsonSerializer>();
private IEditorConfigurationParser EditorConfigurationParser => GetRequiredService<IEditorConfigurationParser>();
[Test]
public void DataTypeService_Can_Persist_New_DataTypeDefinition()
public async Task Can_Create_New_DataTypeDefinition()
{
// Act
IDataType dataType =
@@ -41,22 +43,104 @@ public class DataTypeServiceTests : UmbracoIntegrationTest
Name = "Testing Textfield",
DatabaseType = ValueStorageType.Ntext
};
DataTypeService.Save(dataType);
var result = await DataTypeService.CreateAsync(dataType);
Assert.True(result.Success);
Assert.AreEqual(DataTypeOperationStatus.Success, result.Status);
// Assert
Assert.That(dataType, Is.Not.Null);
Assert.That(dataType.HasIdentity, Is.True);
dataType = DataTypeService.GetDataType(dataType.Id);
dataType = await DataTypeService.GetAsync(dataType.Key);
Assert.That(dataType, Is.Not.Null);
}
[Test]
public void DataTypeService_Can_Delete_Textfield_DataType_And_Clear_Usages()
public async Task Can_Update_Existing_DataTypeDefinition()
{
// Arrange
var textfieldId = "Umbraco.Textbox";
var dataTypeDefinitions = DataTypeService.GetByEditorAlias(textfieldId);
IDataType? dataType = (await DataTypeService.GetByEditorAliasAsync(Constants.PropertyEditors.Aliases.TextBox)).FirstOrDefault();
Assert.NotNull(dataType);
// Act
dataType.Name += " UPDATED";
var result = await DataTypeService.UpdateAsync(dataType);
Assert.True(result.Success);
Assert.AreEqual(DataTypeOperationStatus.Success, result.Status);
// Assert
dataType = await DataTypeService.GetAsync(dataType.Key);
Assert.NotNull(dataType);
Assert.True(dataType.Name.EndsWith(" UPDATED"));
}
[Test]
public async Task Can_Get_All_By_Editor_Alias()
{
// Arrange
async Task<IDataType> CreateTextBoxDataType()
{
IDataType dataType =
new DataType(new TextboxPropertyEditor(DataValueEditorFactory, IOHelper, EditorConfigurationParser), ConfigurationEditorJsonSerializer)
{
Name = Guid.NewGuid().ToString(),
DatabaseType = ValueStorageType.Nvarchar
};
var result = await DataTypeService.CreateAsync(dataType);
Assert.True(result.Success);
return result.Result;
}
IDataType dataType1 = await CreateTextBoxDataType();
IDataType dataType2 = await CreateTextBoxDataType();
IDataType dataType3 = await CreateTextBoxDataType();
// Act
IEnumerable<IDataType> dataTypes = await DataTypeService.GetByEditorAliasAsync(Constants.PropertyEditors.Aliases.TextBox);
// Assert
Assert.True(dataTypes.Count() >= 3);
Assert.True(dataTypes.All(dataType => dataType.EditorAlias == Constants.PropertyEditors.Aliases.TextBox));
Assert.NotNull(dataTypes.FirstOrDefault(dataType => dataType.Key == dataType1.Key));
Assert.NotNull(dataTypes.FirstOrDefault(dataType => dataType.Key == dataType2.Key));
Assert.NotNull(dataTypes.FirstOrDefault(dataType => dataType.Key == dataType3.Key));
}
[Test]
public async Task Can_Get_By_Id()
{
// Arrange
IDataType? dataType = (await DataTypeService.GetByEditorAliasAsync(Constants.PropertyEditors.Aliases.TextBox)).FirstOrDefault();
Assert.NotNull(dataType);
// Act
IDataType? actual = await DataTypeService.GetAsync(dataType.Key);
// Assert
Assert.NotNull(actual);
Assert.AreEqual(dataType.Key, actual.Key);
}
[Test]
public async Task Can_Get_By_Name()
{
// Arrange
IDataType? dataType = (await DataTypeService.GetByEditorAliasAsync(Constants.PropertyEditors.Aliases.TextBox)).FirstOrDefault();
Assert.NotNull(dataType);
// Act
IDataType? actual = await DataTypeService.GetAsync(dataType.Name);
// Assert
Assert.NotNull(actual);
Assert.AreEqual(dataType.Key, actual.Key);
}
[Test]
public async Task DataTypeService_Can_Delete_Textfield_DataType_And_Clear_Usages()
{
// Arrange
var dataTypeDefinitions = await DataTypeService.GetByEditorAliasAsync(Constants.PropertyEditors.Aliases.TextBox);
var template = TemplateBuilder.CreateTextPageTemplate();
FileService.SaveTemplate(template);
var doctype =
@@ -65,10 +149,14 @@ public class DataTypeServiceTests : UmbracoIntegrationTest
// Act
var definition = dataTypeDefinitions.First();
var definitionId = definition.Id;
DataTypeService.Delete(definition);
var definitionKey = definition.Key;
var result = await DataTypeService.DeleteAsync(definitionKey);
Assert.True(result.Success);
Assert.AreEqual(DataTypeOperationStatus.Success, result.Status);
Assert.NotNull(result.Result);
Assert.AreEqual(definitionKey, result.Result.Key);
var deletedDefinition = DataTypeService.GetDataType(definitionId);
var deletedDefinition = await DataTypeService.GetAsync(definitionKey);
// Assert
Assert.That(deletedDefinition, Is.Null);
@@ -79,6 +167,44 @@ public class DataTypeServiceTests : UmbracoIntegrationTest
Assert.That(contentType.PropertyTypes.Count(), Is.EqualTo(1));
}
[Test]
public async Task Cannot_Create_DataType_With_Empty_Name()
{
// Act
var dataTypeDefinition =
new DataType(new LabelPropertyEditor(DataValueEditorFactory, IOHelper), ConfigurationEditorJsonSerializer)
{
Name = string.Empty,
DatabaseType = ValueStorageType.Ntext
};
// Act
var result = await DataTypeService.CreateAsync(dataTypeDefinition);
// Assert
Assert.IsFalse(result.Success);
Assert.AreEqual(DataTypeOperationStatus.InvalidName, result.Status);
}
[Test]
public async Task Cannot_Create_DataType_With_Too_Long_Name()
{
// Act
var dataTypeDefinition =
new DataType(new LabelPropertyEditor(DataValueEditorFactory, IOHelper), ConfigurationEditorJsonSerializer)
{
Name = new string('a', 256),
DatabaseType = ValueStorageType.Ntext
};
// Act
var result = await DataTypeService.CreateAsync(dataTypeDefinition);
// Assert
Assert.IsFalse(result.Success);
Assert.AreEqual(DataTypeOperationStatus.InvalidName, result.Status);
}
[Test]
public void Cannot_Save_DataType_With_Empty_Name()
{