V14: Member group controller (#15669)

* Add models & mapping

* Add controller

* Add create async to service

* Add auth policy

* Implement delete

* Rename response model

* Implement updateAsync

* Refactor update to use own model

* Implement all async counterparts for IMemberService

* Add tests

* Implement update member group mapping

* Dont fail if updating the current user group

* Return not found if not found

* Add missing OperationResults to MemberGroupOperationStatusResult

* Add 404 to response type

* Update openapi

* Update OpenApi

* Update OpenApi.json
This commit is contained in:
Nikolaj Geisle
2024-02-29 09:16:27 +01:00
committed by GitHub
parent 3d5bb07ab1
commit 1e043cbcfb
17 changed files with 1170 additions and 48 deletions

View File

@@ -0,0 +1,38 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Api.Management.ViewModels.MemberGroup;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Management.Controllers.MemberGroup;
[ApiVersion("1.0")]
public class AllMemberGroupController : MemberGroupControllerBase
{
private readonly IMemberGroupService _memberGroupService;
private readonly IUmbracoMapper _mapper;
public AllMemberGroupController(IMemberGroupService memberGroupService, IUmbracoMapper mapper)
{
_memberGroupService = memberGroupService;
_mapper = mapper;
}
[HttpGet]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedViewModel<MemberGroupResponseModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<MemberGroupResponseModel>>> All(int skip = 0, int take = 100)
{
IMemberGroup[] memberGroups = (await _memberGroupService.GetAllAsync()).ToArray();
var viewModel = new PagedViewModel<MemberGroupResponseModel>
{
Total = memberGroups.Length,
Items = _mapper.MapEnumerable<IMemberGroup, MemberGroupResponseModel>(memberGroups.Skip(skip).Take(take)),
};
return await Task.FromResult(Ok(viewModel));
}
}

View File

@@ -0,0 +1,37 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.MemberGroup;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Api.Management.Controllers.MemberGroup;
[ApiVersion("1.0")]
public class CreateMemberGroupController : MemberGroupControllerBase
{
private readonly IMemberGroupService _memberGroupService;
private readonly IUmbracoMapper _mapper;
public CreateMemberGroupController(IMemberGroupService memberGroupService, IUmbracoMapper mapper)
{
_memberGroupService = memberGroupService;
_mapper = mapper;
}
[HttpPost]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(MemberGroupResponseModel), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
public async Task<IActionResult> Create(CreateMemberGroupRequestModel model)
{
IMemberGroup? memberGroup = _mapper.Map<IMemberGroup>(model);
Attempt<IMemberGroup?, MemberGroupOperationStatus> result = await _memberGroupService.CreateAsync(memberGroup!);
return result.Success
? Ok(_mapper.Map<MemberGroupResponseModel>(result.Result))
: MemberGroupOperationStatusResult(result.Status);
}
}

View File

@@ -0,0 +1,30 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
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.MemberGroup;
[ApiVersion("1.0")]
public class DeleteMemberGroupController : MemberGroupControllerBase
{
private readonly IMemberGroupService _memberGroupService;
public DeleteMemberGroupController(IMemberGroupService memberGroupService) => _memberGroupService = memberGroupService;
[HttpDelete("{key:guid}")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<IActionResult> Delete(Guid key)
{
Attempt<IMemberGroup?, MemberGroupOperationStatus> result = await _memberGroupService.DeleteAsync(key);
return result.Success
? Ok()
: MemberGroupOperationStatusResult(result.Status);
}
}

View File

@@ -0,0 +1,48 @@
using Microsoft.AspNetCore.Authorization;
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.Services.OperationStatus;
using Umbraco.Cms.Web.Common.Authorization;
namespace Umbraco.Cms.Api.Management.Controllers.MemberGroup;
[ApiController]
[VersionedApiBackOfficeRoute($"{Constants.UdiEntityType.MemberGroup}")]
[ApiExplorerSettings(GroupName = "Member Group")]
[Authorize(Policy = "New" + AuthorizationPolicies.SectionAccessMembers)]
public class MemberGroupControllerBase : ManagementApiControllerBase
{
protected IActionResult MemberGroupOperationStatusResult(MemberGroupOperationStatus status) =>
status switch
{
MemberGroupOperationStatus.Success => Ok(),
MemberGroupOperationStatus.NotFound => MemberGroupNotFound(),
MemberGroupOperationStatus.CancelledByNotification => BadRequest(new ProblemDetailsBuilder()
.WithTitle("Cancelled by notification")
.WithDetail("A notification handler prevented the member group operation.")
.Build()),
MemberGroupOperationStatus.CannotHaveEmptyName => BadRequest(new ProblemDetailsBuilder()
.WithTitle("Name was empty or null")
.WithDetail("The provided member group name cannot be null or empty.")
.Build()),
MemberGroupOperationStatus.DuplicateName => BadRequest(new ProblemDetailsBuilder()
.WithTitle("Duplicate name")
.WithDetail("Another group with the same name already exists.")
.Build()),
MemberGroupOperationStatus.DuplicateKey => BadRequest(new ProblemDetailsBuilder()
.WithTitle("Duplicate key")
.WithDetail("Another group with the same key already exists.")
.Build()),
_ => StatusCode(StatusCodes.Status500InternalServerError, new ProblemDetailsBuilder()
.WithTitle("Unknown member group operation status.")
.Build()),
};
protected IActionResult MemberGroupNotFound() => OperationStatusResult(MemberGroupOperationStatus.NotFound, problemDetailsBuilder
=> NotFound(problemDetailsBuilder
.WithTitle("The requested member group could not be found")
.Build()));
}

View File

@@ -0,0 +1,45 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.MemberGroup;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Api.Management.Controllers.MemberGroup;
[ApiVersion("1.0")]
public class UpdateMemberGroupController : MemberGroupControllerBase
{
private readonly IUmbracoMapper _mapper;
private readonly IMemberGroupService _memberGroupService;
public UpdateMemberGroupController(IUmbracoMapper mapper, IMemberGroupService memberGroupService)
{
_mapper = mapper;
_memberGroupService = memberGroupService;
}
[HttpPut($"{{{nameof(id)}:guid}}")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(MemberGroupResponseModel), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> Update(Guid id, UpdateMemberGroupRequestModel model)
{
IMemberGroup? current = await _memberGroupService.GetAsync(id);
if (current is null)
{
return MemberGroupNotFound();
}
IMemberGroup updated = _mapper.Map(model, current);
Attempt<IMemberGroup?, MemberGroupOperationStatus> result = await _memberGroupService.UpdateAsync(updated);
return result.Success
? Ok(_mapper.Map<MemberGroupResponseModel>(result.Result))
: MemberGroupOperationStatusResult(result.Status);
}
}

View File

@@ -0,0 +1,16 @@
using Umbraco.Cms.Api.Management.Mapping.MemberGroup;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Mapping;
namespace Umbraco.Cms.Api.Management.DependencyInjection;
public static class MemberGroupsBuilderExtensions
{
internal static IUmbracoBuilder AddMemberGroups(this IUmbracoBuilder builder)
{
builder.WithCollectionBuilder<MapDefinitionCollectionBuilder>()
.Add<MemberGroupMapDefinition>();
return builder;
}
}

View File

@@ -37,6 +37,7 @@ public static partial class UmbracoBuilderExtensions
.AddDocumentTypes()
.AddMedia()
.AddMediaTypes()
.AddMemberGroups()
.AddMember()
.AddMemberTypes()
.AddLanguages()

View File

@@ -0,0 +1,32 @@
using Umbraco.Cms.Api.Management.ViewModels.MemberGroup;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
namespace Umbraco.Cms.Api.Management.Mapping.MemberGroup;
public class MemberGroupMapDefinition : IMapDefinition
{
public void DefineMaps(IUmbracoMapper mapper)
{
mapper.Define<CreateMemberGroupRequestModel, IMemberGroup>((_, _) => new Core.Models.MemberGroup(), Map);
mapper.Define<UpdateMemberGroupRequestModel, IMemberGroup>((_, _) => new Core.Models.MemberGroup(), Map);
mapper.Define<IMemberGroup, MemberGroupResponseModel>((_, _) => new MemberGroupResponseModel { Name = string.Empty }, Map);
}
// Umbraco.Code.MapAll -Id -CreateDate -CreatorId -DeleteDate -UpdateDate
private static void Map(CreateMemberGroupRequestModel source, IMemberGroup target, MapperContext context)
{
target.Name = source.Name;
target.Key = source.Id ?? Guid.NewGuid();
}
// Umbraco.Code.MapAll -Id -CreateDate -CreatorId -DeleteDate -UpdateDate -Key
private static void Map(UpdateMemberGroupRequestModel source, IMemberGroup target, MapperContext context) => target.Name = source.Name;
// Umbraco.Code.MapAll
private static void Map(IMemberGroup source, MemberGroupResponseModel target, MapperContext context)
{
target.Name = source.Name ?? string.Empty;
target.Id = source.Key;
}
}

View File

@@ -15125,6 +15125,463 @@
]
}
},
"/umbraco/management/api/v1/member-group": {
"get": {
"tags": [
"Member Group"
],
"operationId": "GetMemberGroup",
"parameters": [
{
"name": "skip",
"in": "query",
"schema": {
"type": "integer",
"format": "int32",
"default": 0
}
},
{
"name": "take",
"in": "query",
"schema": {
"type": "integer",
"format": "int32",
"default": 100
}
}
],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PagedMemberGroupResponseModel"
}
},
"text/json": {
"schema": {
"$ref": "#/components/schemas/PagedMemberGroupResponseModel"
}
},
"text/plain": {
"schema": {
"$ref": "#/components/schemas/PagedMemberGroupResponseModel"
}
}
}
},
"401": {
"description": "The resource is protected and requires an authentication token"
}
},
"security": [
{
"Backoffice User": [ ]
}
]
},
"post": {
"tags": [
"Member Group"
],
"operationId": "PostMemberGroup",
"requestBody": {
"content": {
"application/json": {
"schema": {
"oneOf": [
{
"$ref": "#/components/schemas/CreateMemberGroupRequestModel"
}
]
}
},
"text/json": {
"schema": {
"oneOf": [
{
"$ref": "#/components/schemas/CreateMemberGroupRequestModel"
}
]
}
},
"application/*+json": {
"schema": {
"oneOf": [
{
"$ref": "#/components/schemas/CreateMemberGroupRequestModel"
}
]
}
}
}
},
"responses": {
"200": {
"description": "Success",
"headers": {
"Umb-Notifications": {
"description": "The list of notifications produced during the request.",
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/NotificationHeaderModel"
},
"nullable": true
}
}
},
"content": {
"application/json": {
"schema": {
"oneOf": [
{
"$ref": "#/components/schemas/MemberGroupResponseModel"
}
]
}
},
"text/json": {
"schema": {
"oneOf": [
{
"$ref": "#/components/schemas/MemberGroupResponseModel"
}
]
}
},
"text/plain": {
"schema": {
"oneOf": [
{
"$ref": "#/components/schemas/MemberGroupResponseModel"
}
]
}
}
}
},
"400": {
"description": "Bad Request",
"headers": {
"Umb-Notifications": {
"description": "The list of notifications produced during the request.",
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/NotificationHeaderModel"
},
"nullable": true
}
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
},
"text/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
},
"text/plain": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
}
}
},
"401": {
"description": "The resource is protected and requires an authentication token"
}
},
"security": [
{
"Backoffice User": [ ]
}
]
}
},
"/umbraco/management/api/v1/member-group/{id}": {
"put": {
"tags": [
"Member Group"
],
"operationId": "PutMemberGroupById",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"oneOf": [
{
"$ref": "#/components/schemas/UpdateMemberGroupRequestModel"
}
]
}
},
"text/json": {
"schema": {
"oneOf": [
{
"$ref": "#/components/schemas/UpdateMemberGroupRequestModel"
}
]
}
},
"application/*+json": {
"schema": {
"oneOf": [
{
"$ref": "#/components/schemas/UpdateMemberGroupRequestModel"
}
]
}
}
}
},
"responses": {
"200": {
"description": "Success",
"headers": {
"Umb-Notifications": {
"description": "The list of notifications produced during the request.",
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/NotificationHeaderModel"
},
"nullable": true
}
}
},
"content": {
"application/json": {
"schema": {
"oneOf": [
{
"$ref": "#/components/schemas/MemberGroupResponseModel"
}
]
}
},
"text/json": {
"schema": {
"oneOf": [
{
"$ref": "#/components/schemas/MemberGroupResponseModel"
}
]
}
},
"text/plain": {
"schema": {
"oneOf": [
{
"$ref": "#/components/schemas/MemberGroupResponseModel"
}
]
}
}
}
},
"400": {
"description": "Bad Request",
"headers": {
"Umb-Notifications": {
"description": "The list of notifications produced during the request.",
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/NotificationHeaderModel"
},
"nullable": true
}
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
},
"text/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
},
"text/plain": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
}
}
},
"404": {
"description": "Not Found",
"headers": {
"Umb-Notifications": {
"description": "The list of notifications produced during the request.",
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/NotificationHeaderModel"
},
"nullable": true
}
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
},
"text/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
},
"text/plain": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
}
}
},
"401": {
"description": "The resource is protected and requires an authentication token"
}
},
"security": [
{
"Backoffice User": [ ]
}
]
}
},
"/umbraco/management/api/v1/member-group/{key}": {
"delete": {
"tags": [
"Member Group"
],
"operationId": "DeleteMemberGroupByKey",
"parameters": [
{
"name": "key",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
"400": {
"description": "Bad Request",
"headers": {
"Umb-Notifications": {
"description": "The list of notifications produced during the request.",
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/NotificationHeaderModel"
},
"nullable": true
}
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
},
"text/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
},
"text/plain": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
}
}
},
"404": {
"description": "Not Found",
"headers": {
"Umb-Notifications": {
"description": "The list of notifications produced during the request.",
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/NotificationHeaderModel"
},
"nullable": true
}
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
},
"text/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
},
"text/plain": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
}
}
},
"200": {
"description": "Success",
"headers": {
"Umb-Notifications": {
"description": "The list of notifications produced during the request.",
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/NotificationHeaderModel"
},
"nullable": true
}
}
}
},
"401": {
"description": "The resource is protected and requires an authentication token"
}
},
"security": [
{
"Backoffice User": [ ]
}
]
}
},
"/umbraco/management/api/v1/tree/member-group/root": {
"get": {
"tags": [
@@ -32000,6 +32457,22 @@
},
"additionalProperties": false
},
"CreateMemberGroupRequestModel": {
"type": "object",
"allOf": [
{
"$ref": "#/components/schemas/MemberGroupPresentationBaseModel"
}
],
"properties": {
"id": {
"type": "string",
"format": "uuid",
"nullable": true
}
},
"additionalProperties": false
},
"CreateMemberRequestModel": {
"required": [
"email",
@@ -34994,6 +35467,36 @@
],
"additionalProperties": false
},
"MemberGroupPresentationBaseModel": {
"required": [
"name"
],
"type": "object",
"properties": {
"name": {
"type": "string"
}
},
"additionalProperties": false
},
"MemberGroupResponseModel": {
"required": [
"id"
],
"type": "object",
"allOf": [
{
"$ref": "#/components/schemas/MemberGroupPresentationBaseModel"
}
],
"properties": {
"id": {
"type": "string",
"format": "uuid"
}
},
"additionalProperties": false
},
"MemberItemResponseModel": {
"required": [
"memberType",
@@ -36218,6 +36721,30 @@
},
"additionalProperties": false
},
"PagedMemberGroupResponseModel": {
"required": [
"items",
"total"
],
"type": "object",
"properties": {
"total": {
"type": "integer",
"format": "int64"
},
"items": {
"type": "array",
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/MemberGroupResponseModel"
}
]
}
}
},
"additionalProperties": false
},
"PagedMemberResponseModel": {
"required": [
"items",
@@ -38777,6 +39304,15 @@
},
"additionalProperties": false
},
"UpdateMemberGroupRequestModel": {
"type": "object",
"allOf": [
{
"$ref": "#/components/schemas/MemberGroupPresentationBaseModel"
}
],
"additionalProperties": false
},
"UpdateMemberRequestModel": {
"required": [
"email",

View File

@@ -0,0 +1,6 @@
namespace Umbraco.Cms.Api.Management.ViewModels.MemberGroup;
public class CreateMemberGroupRequestModel : MemberGroupPresentationBase
{
public Guid? Id { get; set; }
}

View File

@@ -0,0 +1,6 @@
namespace Umbraco.Cms.Api.Management.ViewModels.MemberGroup;
public class MemberGroupPresentationBase
{
public required string Name { get; set; }
}

View File

@@ -0,0 +1,6 @@
namespace Umbraco.Cms.Api.Management.ViewModels.MemberGroup;
public class MemberGroupResponseModel : MemberGroupPresentationBase
{
public Guid Id { get; set; }
}

View File

@@ -0,0 +1,5 @@
namespace Umbraco.Cms.Api.Management.ViewModels.MemberGroup;
public class UpdateMemberGroupRequestModel : MemberGroupPresentationBase
{
}

View File

@@ -1,20 +1,74 @@
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Core.Services;
public interface IMemberGroupService : IService
{
[Obsolete("Please use the asynchronous counterpart. Scheduled for removal in v15.")]
IEnumerable<IMemberGroup> GetAll();
[Obsolete("Please use Guid instead of Int id. Scheduled for removal in v15.")]
IMemberGroup? GetById(int id);
[Obsolete("Please use the asynchronous counterpart. Scheduled for removal in v15.")]
IMemberGroup? GetById(Guid id);
[Obsolete("Please use the asynchronous counterpart. Scheduled for removal in v15.")]
IEnumerable<IMemberGroup> GetByIds(IEnumerable<int> ids);
IMemberGroup? GetByName(string? name);
[Obsolete("Please use the respective CreateAsync/UpdateAsync for you save operations. Scheduled for removal in v15.")]
void Save(IMemberGroup memberGroup);
[Obsolete("Please use the asynchronous counterpart. Scheduled for removal in v15.")]
void Delete(IMemberGroup memberGroup);
/// <summary>
/// Get a member group by name.
/// </summary>
/// <param name="name">Name of the member group to get.</param>
/// <returns>A <see cref="IMemberGroup" /> object.</returns>
Task<IMemberGroup?> GetByNameAsync(string name);
/// <summary>
/// Get a member group by key.
/// </summary>
/// <param name="key"><see cref="Guid" /> of the member group to get.</param>
/// <returns>A <see cref="IMemberGroup" /> object.</returns>
Task<IMemberGroup?> GetAsync(Guid key);
/// <summary>
/// Gets all member groups
/// </summary>
/// <returns>An enumerable list of <see cref="IMemberGroup" /> objects.</returns>
Task<IEnumerable<IMemberGroup>> GetAllAsync();
/// <summary>
/// Gets a list of member groups with the given ids.
/// </summary>
/// <param name="ids">An enumerable list of <see cref="int" /> ids, to get the member groups by.</param>
/// <returns>An enumerable list of <see cref="IMemberGroup" /> objects.</returns>
Task<IEnumerable<IMemberGroup>> GetByIdsAsync(IEnumerable<int> ids);
/// <summary>
/// Creates a new <see cref="IMemberGroup" /> object
/// </summary>
/// <param name="memberGroup"><see cref="IMemberGroup" /> to create</param>
/// <returns>An attempt with a status of whether the operation was successful or not, and the created object if it succeeded.</returns>
Task<Attempt<IMemberGroup?, MemberGroupOperationStatus>> CreateAsync(IMemberGroup memberGroup);
/// <summary>
/// Deletes a <see cref="IMemberGroup" /> by removing it and its usages from the db
/// </summary>
/// <param name="key">The key of the <see cref="IMemberGroup" /> to delete</param>
Task<Attempt<IMemberGroup?, MemberGroupOperationStatus>> DeleteAsync(Guid key);
/// <summary>
/// Updates <see cref="IMemberGroup" /> object
/// </summary>
/// <param name="memberGroup"><see cref="IMemberGroup" /> to create</param>
/// <returns>An attempt with a status of whether the operation was successful or not, and the object.</returns>
Task<Attempt<IMemberGroup?, MemberGroupOperationStatus>> UpdateAsync(IMemberGroup memberGroup);
}

View File

@@ -4,6 +4,7 @@ using Umbraco.Cms.Core.Models;
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;
@@ -15,26 +16,9 @@ internal class MemberGroupService : RepositoryService, IMemberGroupService
: base(provider, loggerFactory, eventMessagesFactory) =>
_memberGroupRepository = memberGroupRepository;
public IEnumerable<IMemberGroup> GetAll()
{
using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
{
return _memberGroupRepository.GetMany();
}
}
public IEnumerable<IMemberGroup> GetAll() => GetAllAsync().GetAwaiter().GetResult();
public IEnumerable<IMemberGroup> GetByIds(IEnumerable<int> ids)
{
if (ids == null || ids.Any() == false)
{
return new IMemberGroup[0];
}
using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
{
return _memberGroupRepository.GetMany(ids.ToArray());
}
}
public IEnumerable<IMemberGroup> GetByIds(IEnumerable<int> ids) => GetByIdsAsync(ids).GetAwaiter().GetResult();
public IMemberGroup? GetById(int id)
{
@@ -44,21 +28,9 @@ internal class MemberGroupService : RepositoryService, IMemberGroupService
}
}
public IMemberGroup? GetById(Guid id)
{
using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
{
return _memberGroupRepository.Get(id);
}
}
public IMemberGroup? GetById(Guid id) => GetAsync(id).GetAwaiter().GetResult();
public IMemberGroup? GetByName(string? name)
{
using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
{
return _memberGroupRepository.GetByName(name);
}
}
public IMemberGroup? GetByName(string? name) => name is null ? null : GetByNameAsync(name).GetAwaiter().GetResult();
public void Save(IMemberGroup memberGroup)
{
@@ -86,24 +58,139 @@ internal class MemberGroupService : RepositoryService, IMemberGroupService
}
}
public void Delete(IMemberGroup memberGroup)
public void Delete(IMemberGroup memberGroup) => DeleteAsync(memberGroup.Key).GetAwaiter().GetResult();
/// <inheritdoc/>
public Task<IMemberGroup?> GetByNameAsync(string name)
{
EventMessages evtMsgs = EventMessagesFactory.Get();
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
return Task.FromResult(_memberGroupRepository.GetByName(name));
}
using (ICoreScope scope = ScopeProvider.CreateCoreScope())
/// <inheritdoc/>
public Task<IMemberGroup?> GetAsync(Guid key)
{
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
return Task.FromResult(_memberGroupRepository.Get(key));
}
/// <inheritdoc/>
public Task<IEnumerable<IMemberGroup>> GetAllAsync()
{
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
return Task.FromResult(_memberGroupRepository.GetMany());
}
public Task<IEnumerable<IMemberGroup>> GetByIdsAsync(IEnumerable<int> ids)
{
if (ids.Any() == false)
{
var deletingNotification = new MemberGroupDeletingNotification(memberGroup, evtMsgs);
if (scope.Notifications.PublishCancelable(deletingNotification))
{
scope.Complete();
return;
}
_memberGroupRepository.Delete(memberGroup);
scope.Complete();
scope.Notifications.Publish(
new MemberGroupDeletedNotification(memberGroup, evtMsgs).WithStateFrom(deletingNotification));
return Task.FromResult<IEnumerable<IMemberGroup>>(Array.Empty<IMemberGroup>());
}
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
return Task.FromResult(_memberGroupRepository.GetMany(ids.ToArray()));
}
/// <inheritdoc/>
public async Task<Attempt<IMemberGroup?, MemberGroupOperationStatus>> CreateAsync(IMemberGroup memberGroup)
{
if (string.IsNullOrWhiteSpace(memberGroup.Name))
{
return Attempt.FailWithStatus<IMemberGroup?, MemberGroupOperationStatus>(MemberGroupOperationStatus.CannotHaveEmptyName, null);
}
EventMessages eventMessages = EventMessagesFactory.Get();
using ICoreScope scope = ScopeProvider.CreateCoreScope();
IMemberGroup? existingMemberGroup = await GetAsync(memberGroup.Key);
if (existingMemberGroup is not null)
{
return Attempt.FailWithStatus<IMemberGroup?, MemberGroupOperationStatus>(MemberGroupOperationStatus.DuplicateKey, null);
}
if (await NameAlreadyExistsAsync(memberGroup))
{
return Attempt.FailWithStatus<IMemberGroup?, MemberGroupOperationStatus>(MemberGroupOperationStatus.DuplicateName, null);
}
var savingNotification = new MemberGroupSavingNotification(memberGroup, eventMessages);
if (await scope.Notifications.PublishCancelableAsync(savingNotification))
{
scope.Complete();
return Attempt.FailWithStatus<IMemberGroup?, MemberGroupOperationStatus>(MemberGroupOperationStatus.CancelledByNotification, null);
}
_memberGroupRepository.Save(memberGroup);
scope.Complete();
scope.Notifications.Publish(new MemberGroupSavedNotification(memberGroup, eventMessages).WithStateFrom(savingNotification));
return Attempt.SucceedWithStatus<IMemberGroup?, MemberGroupOperationStatus>(MemberGroupOperationStatus.Success, memberGroup);
}
/// <inheritdoc/>
public async Task<Attempt<IMemberGroup?, MemberGroupOperationStatus>> DeleteAsync(Guid key)
{
EventMessages eventMessages = EventMessagesFactory.Get();
using ICoreScope scope = ScopeProvider.CreateCoreScope();
IMemberGroup? memberGroup = _memberGroupRepository.Get(key);
if (memberGroup is null)
{
return Attempt.FailWithStatus<IMemberGroup?, MemberGroupOperationStatus>(MemberGroupOperationStatus.NotFound, null);
}
var deletingNotification = new MemberGroupDeletingNotification(memberGroup, eventMessages);
if (await scope.Notifications.PublishCancelableAsync(deletingNotification))
{
scope.Complete();
return Attempt.FailWithStatus<IMemberGroup?, MemberGroupOperationStatus>(MemberGroupOperationStatus.CancelledByNotification, null);
}
_memberGroupRepository.Delete(memberGroup);
scope.Complete();
scope.Notifications.Publish(new MemberGroupDeletedNotification(memberGroup, eventMessages).WithStateFrom(deletingNotification));
return Attempt.SucceedWithStatus<IMemberGroup?, MemberGroupOperationStatus>(MemberGroupOperationStatus.Success, memberGroup);
}
public async Task<Attempt<IMemberGroup?, MemberGroupOperationStatus>> UpdateAsync(IMemberGroup memberGroup)
{
if (string.IsNullOrWhiteSpace(memberGroup.Name))
{
return Attempt.FailWithStatus<IMemberGroup?, MemberGroupOperationStatus>(MemberGroupOperationStatus.CannotHaveEmptyName, null);
}
EventMessages eventMessages = EventMessagesFactory.Get();
using ICoreScope scope = ScopeProvider.CreateCoreScope();
IMemberGroup? existingMemberGroup = await GetByNameAsync(memberGroup.Name!);
if (existingMemberGroup is not null && existingMemberGroup.Key != memberGroup.Key)
{
return Attempt.FailWithStatus<IMemberGroup?, MemberGroupOperationStatus>(MemberGroupOperationStatus.DuplicateName, null);
}
var savingNotification = new MemberGroupSavingNotification(memberGroup, eventMessages);
if (await scope.Notifications.PublishCancelableAsync(savingNotification))
{
scope.Complete();
return Attempt.FailWithStatus<IMemberGroup?, MemberGroupOperationStatus>(MemberGroupOperationStatus.CancelledByNotification, null);
}
_memberGroupRepository.Save(memberGroup);
scope.Complete();
scope.Notifications.Publish(new MemberGroupSavedNotification(memberGroup, eventMessages).WithStateFrom(savingNotification));
return Attempt.SucceedWithStatus<IMemberGroup?, MemberGroupOperationStatus>(MemberGroupOperationStatus.Success, memberGroup);
}
private async Task<bool> NameAlreadyExistsAsync(IMemberGroup memberGroup)
{
IMemberGroup? existingMemberGroup = await GetByNameAsync(memberGroup.Name!);
return existingMemberGroup is not null;
}
}

View File

@@ -0,0 +1,11 @@
namespace Umbraco.Cms.Core.Services.OperationStatus;
public enum MemberGroupOperationStatus
{
Success,
NotFound,
CannotHaveEmptyName,
CancelledByNotification,
DuplicateName,
DuplicateKey,
}

View File

@@ -0,0 +1,164 @@
using NUnit.Framework;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Cms.Tests.Common.Testing;
using Umbraco.Cms.Tests.Integration.Testing;
namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services;
/// <summary>
/// Tests covering the MemberGroupService
/// </summary>
[TestFixture]
[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)]
public class MemberGroupServiceTests : UmbracoIntegrationTest
{
private IMemberGroupService MemberGroupService => GetRequiredService<IMemberGroupService>();
[Test]
public async Task Can_Create_MemberGroup()
{
var memberGroup = new MemberGroup
{
Name = "TestGroup",
};
await MemberGroupService.CreateAsync(memberGroup);
var fetchedGroup = await MemberGroupService.GetAsync(memberGroup.Key);
Assert.Multiple(() =>
{
Assert.IsNotNull(fetchedGroup);
Assert.AreEqual(fetchedGroup, memberGroup);
});
}
[Test]
public async Task Can_Create_MemberGroup_With_Key()
{
Guid key = Guid.NewGuid();
var memberGroup = new MemberGroup
{
Name = "TestGroup",
Key = key,
};
await MemberGroupService.CreateAsync(memberGroup);
var fetchedGroup = await MemberGroupService.GetAsync(memberGroup.Key);
Assert.Multiple(() =>
{
Assert.IsNotNull(fetchedGroup);
Assert.AreEqual(key, fetchedGroup.Key);
Assert.AreEqual(fetchedGroup, memberGroup);
});
}
[Test]
public async Task Can_Update_MemberGroup()
{
const string updatedName = "UpdatedName";
var memberGroup = new MemberGroup
{
Name = "TestGroup",
};
await MemberGroupService.CreateAsync(memberGroup);
memberGroup.Name = updatedName;
await MemberGroupService.UpdateAsync(memberGroup);
var fetchedGroup = await MemberGroupService.GetAsync(memberGroup.Key);
Assert.Multiple(() =>
{
Assert.IsNotNull(fetchedGroup);
Assert.AreEqual(fetchedGroup.Name, updatedName);
Assert.AreEqual(fetchedGroup, memberGroup);
});
}
[Test]
public async Task Can_Delete_MemberGroup()
{
var memberGroup = new MemberGroup
{
Name = "TestGroup",
};
await MemberGroupService.CreateAsync(memberGroup);
var fetchedGroup = await MemberGroupService.GetAsync(memberGroup.Key);
Assert.IsNotNull(fetchedGroup);
await MemberGroupService.DeleteAsync(memberGroup.Key);
// re-get
fetchedGroup = await MemberGroupService.GetAsync(memberGroup.Key);
Assert.IsNull(fetchedGroup);
}
[Test]
public async Task Cannot_Create_MemberGroup_With_Duplicate_Name()
{
const string name = "TestGroup";
var memberGroupOne = new MemberGroup
{
Name = name,
};
var memberGroupTwo = new MemberGroup
{
Name = name,
};
var attemptOne = await MemberGroupService.CreateAsync(memberGroupOne);
Assert.Multiple(() =>
{
Assert.IsTrue(attemptOne.Success);
Assert.AreEqual(MemberGroupOperationStatus.Success, attemptOne.Status);
});
var attemptTwo = await MemberGroupService.CreateAsync(memberGroupTwo);
Assert.Multiple(() =>
{
Assert.IsFalse(attemptTwo.Success);
Assert.AreEqual(MemberGroupOperationStatus.DuplicateName, attemptTwo.Status);
});
}
[Test]
public async Task Cannot_Update_MemberGroup_With_Duplicate_Name()
{
const string name = "TestGroup";
var memberGroupOne = new MemberGroup
{
Name = name,
};
var memberGroupTwo = new MemberGroup
{
Name = "TestGroupTwo",
};
var attemptOne = await MemberGroupService.CreateAsync(memberGroupOne);
var attemptTwo = await MemberGroupService.CreateAsync(memberGroupTwo);
Assert.Multiple(() =>
{
Assert.IsTrue(attemptOne.Success);
Assert.AreEqual(MemberGroupOperationStatus.Success, attemptOne.Status);
Assert.IsTrue(attemptTwo.Success);
Assert.AreEqual(MemberGroupOperationStatus.Success, attemptTwo.Status);
});
// Update to already existing name.
memberGroupTwo.Name = name;
var updateAttempt = await MemberGroupService.UpdateAsync(memberGroupTwo);
Assert.Multiple(() =>
{
Assert.IsFalse(updateAttempt.Success);
Assert.AreEqual(MemberGroupOperationStatus.DuplicateName, updateAttempt.Status);
});
}
}