Browse Source

Domain objects refactoring (#1012)

* Refactor domain objects and add deleted flag to asset content.

* Fix command flow.

* Fix archive.
pull/1016/head
Sebastian Stehle 3 years ago
committed by GitHub
parent
commit
f16ea0b7bd
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/DefaultValueFactory.cs
  2. 34
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs
  3. 69
      backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppDomainObject.cs
  4. 4
      backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetCommandMiddleware.cs
  5. 39
      backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetDomainObject.cs
  6. 28
      backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetFolderDomainObject.cs
  7. 4
      backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetQueryService.cs
  8. 1
      backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetEnricher.cs
  9. 16
      backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryService.cs
  10. 1
      backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/Steps/CalculateTokens.cs
  11. 4
      backend/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs
  12. 7
      backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObject.State.cs
  13. 50
      backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObject.cs
  14. 1
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/CalculateTokens.cs
  15. 32
      backend/src/Squidex.Domain.Apps.Entities/Rules/DomainObject/RuleDomainObject.cs
  16. 63
      backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/SchemaDomainObject.cs
  17. 28
      backend/src/Squidex.Domain.Apps.Entities/Teams/DomainObject/TeamDomainObject.cs
  18. 215
      backend/src/Squidex.Infrastructure/Commands/DomainObject.Execute.cs
  19. 114
      backend/src/Squidex.Infrastructure/Commands/DomainObject.cs
  20. 16
      backend/src/Squidex.Infrastructure/Commands/DomainObjectState.cs
  21. 2
      backend/src/Squidex.Infrastructure/DomainId.cs
  22. 3
      backend/src/Squidex.Infrastructure/RandomHash.cs
  23. 1
      backend/src/Squidex.Web/ETagExtensions.cs
  24. 6
      backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs
  25. 6
      backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetContentQueryDto.cs
  26. 6
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsFluidExtensionTests.cs
  27. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetsQueryIntegrationTests.cs
  28. 16
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryServiceTests.cs
  29. 65
      backend/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectTests.cs
  30. 62
      backend/tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainObject.cs
  31. 158
      tools/TestSuite/TestSuite.ApiTests/AssetTests.cs
  32. 2
      tools/TestSuite/TestSuite.ApiTests/TestSuite.ApiTests.csproj
  33. 39
      tools/TestSuite/TestSuite.Shared/ClientExtensions.cs
  34. 5
      tools/TestSuite/TestSuite.Shared/TestSuite.Shared.csproj
  35. 75
      tools/TestSuite/TestSuite.Shared/Utils/PauseStream.cs
  36. 66
      tools/TestSuite/TestSuite.Shared/Utils/ProgressHandler.cs

1
backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/DefaultValueFactory.cs

@ -8,7 +8,6 @@
using System.Globalization; using System.Globalization;
using NodaTime; using NodaTime;
using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Json.Objects;
#pragma warning disable SA1313 // Parameter names should begin with lower-case letter #pragma warning disable SA1313 // Parameter names should begin with lower-case letter

34
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs

@ -206,28 +206,26 @@ public sealed partial class MongoAssetRepository : MongoRepositoryBase<MongoAsse
} }
} }
public async Task<IAssetEntity?> FindAssetBySlugAsync(DomainId appId, string slug, public async Task<IAssetEntity?> FindAssetBySlugAsync(DomainId appId, string slug, bool allowDeleted,
CancellationToken ct = default) CancellationToken ct = default)
{ {
using (Telemetry.Activities.StartActivity("MongoAssetRepository/FindAssetBySlugAsync")) using (Telemetry.Activities.StartActivity("MongoAssetRepository/FindAssetBySlugAsync"))
{ {
var assetEntity = var assetEntity =
await Collection.Find(x => x.IndexedAppId == appId && x.Slug == slug && !x.IsDeleted) await Collection.Find(BuildFilter(appId, slug, allowDeleted))
.FirstOrDefaultAsync(ct); .FirstOrDefaultAsync(ct);
return assetEntity; return assetEntity;
} }
} }
public async Task<IAssetEntity?> FindAssetAsync(DomainId appId, DomainId id, public async Task<IAssetEntity?> FindAssetAsync(DomainId appId, DomainId id, bool allowDeleted,
CancellationToken ct = default) CancellationToken ct = default)
{ {
using (Telemetry.Activities.StartActivity("MongoAssetRepository/FindAssetAsync")) using (Telemetry.Activities.StartActivity("MongoAssetRepository/FindAssetAsync"))
{ {
var documentId = DomainId.Combine(appId, id);
var assetEntity = var assetEntity =
await Collection.Find(x => x.DocumentId == documentId && !x.IsDeleted) await Collection.Find(BuildFilter(appId, id, allowDeleted))
.FirstOrDefaultAsync(ct); .FirstOrDefaultAsync(ct);
return assetEntity; return assetEntity;
@ -256,6 +254,30 @@ public sealed partial class MongoAssetRepository : MongoRepositoryBase<MongoAsse
Filter.Ne(x => x.IsDeleted, true)); Filter.Ne(x => x.IsDeleted, true));
} }
private static FilterDefinition<MongoAssetEntity> BuildFilter(DomainId appId, string slug, bool allowDeleted)
{
var filter = Filter.And(Filter.Eq(x => x.IndexedAppId, appId), Filter.Eq(x => x.Slug, slug));
if (!allowDeleted)
{
filter = Filter.And(filter, Filter.Ne(x => x.IsDeleted, true));
}
return filter;
}
private static FilterDefinition<MongoAssetEntity> BuildFilter(DomainId appId, DomainId id, bool allowDeleted)
{
var filter = Filter.Eq(x => x.DocumentId, DomainId.Combine(appId, id));
if (!allowDeleted)
{
filter = Filter.And(filter, Filter.Ne(x => x.IsDeleted, true));
}
return filter;
}
private static FilterDefinition<MongoAssetEntity> BuildFilter(DomainId appId, DomainId parentId) private static FilterDefinition<MongoAssetEntity> BuildFilter(DomainId appId, DomainId parentId)
{ {
return Filter.And( return Filter.And(

69
backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppDomainObject.cs

@ -40,14 +40,29 @@ public partial class AppDomainObject : DomainObject<AppDomainObject.State>
return snapshot.IsDeleted; return snapshot.IsDeleted;
} }
protected override bool CanAcceptCreation(ICommand command) protected override bool CanAccept(ICommand command)
{ {
return command is AppCommandBase; if (Snapshot.Id == default)
{
return true;
}
return command is AppCommandBase c && c.AggregateId == Snapshot.Id;
} }
protected override bool CanAccept(ICommand command) protected override bool CanAccept(ICommand command, DomainObjectState state)
{ {
return command is AppCommand update && Equals(update?.AppId?.Id, Snapshot.Id); switch (state)
{
case DomainObjectState.Undefined:
return command is CreateApp;
case DomainObjectState.Empty:
return command is CreateApp;
case DomainObjectState.Created:
return command is not CreateApp;
default:
return false;
}
} }
public override Task<CommandResult> ExecuteAsync(IAggregateCommand command, public override Task<CommandResult> ExecuteAsync(IAggregateCommand command,
@ -56,7 +71,7 @@ public partial class AppDomainObject : DomainObject<AppDomainObject.State>
switch (command) switch (command)
{ {
case CreateApp create: case CreateApp create:
return CreateReturn(create, c => return ApplyReturn(create, c =>
{ {
GuardApp.CanCreate(c); GuardApp.CanCreate(c);
@ -66,7 +81,7 @@ public partial class AppDomainObject : DomainObject<AppDomainObject.State>
}, ct); }, ct);
case UpdateApp update: case UpdateApp update:
return UpdateReturn(update, c => return ApplyReturn(update, c =>
{ {
GuardApp.CanUpdate(c); GuardApp.CanUpdate(c);
@ -76,7 +91,7 @@ public partial class AppDomainObject : DomainObject<AppDomainObject.State>
}, ct); }, ct);
case TransferToTeam transfer: case TransferToTeam transfer:
return UpdateReturnAsync(transfer, async (c, ct) => return ApplyReturnAsync(transfer, async (c, ct) =>
{ {
await GuardApp.CanTransfer(c, Snapshot, AppProvider, ct); await GuardApp.CanTransfer(c, Snapshot, AppProvider, ct);
@ -86,7 +101,7 @@ public partial class AppDomainObject : DomainObject<AppDomainObject.State>
}, ct); }, ct);
case UpdateAppSettings updateSettings: case UpdateAppSettings updateSettings:
return UpdateReturn(updateSettings, c => return ApplyReturn(updateSettings, c =>
{ {
GuardApp.CanUpdateSettings(c); GuardApp.CanUpdateSettings(c);
@ -96,7 +111,7 @@ public partial class AppDomainObject : DomainObject<AppDomainObject.State>
}, ct); }, ct);
case UploadAppImage uploadImage: case UploadAppImage uploadImage:
return UpdateReturn(uploadImage, c => return ApplyReturn(uploadImage, c =>
{ {
GuardApp.CanUploadImage(c); GuardApp.CanUploadImage(c);
@ -106,7 +121,7 @@ public partial class AppDomainObject : DomainObject<AppDomainObject.State>
}, ct); }, ct);
case RemoveAppImage removeImage: case RemoveAppImage removeImage:
return UpdateReturn(removeImage, c => return ApplyReturn(removeImage, c =>
{ {
GuardApp.CanRemoveImage(c); GuardApp.CanRemoveImage(c);
@ -116,7 +131,7 @@ public partial class AppDomainObject : DomainObject<AppDomainObject.State>
}, ct); }, ct);
case ConfigureAssetScripts configureAssetScripts: case ConfigureAssetScripts configureAssetScripts:
return UpdateReturn(configureAssetScripts, c => return ApplyReturn(configureAssetScripts, c =>
{ {
GuardApp.CanUpdateAssetScripts(c); GuardApp.CanUpdateAssetScripts(c);
@ -126,7 +141,7 @@ public partial class AppDomainObject : DomainObject<AppDomainObject.State>
}, ct); }, ct);
case AssignContributor assignContributor: case AssignContributor assignContributor:
return UpdateReturnAsync(assignContributor, async (c, ct) => return ApplyReturnAsync(assignContributor, async (c, ct) =>
{ {
var (plan, _, _) = await UsageGate.GetPlanForAppAsync(Snapshot, false, ct); var (plan, _, _) = await UsageGate.GetPlanForAppAsync(Snapshot, false, ct);
@ -138,7 +153,7 @@ public partial class AppDomainObject : DomainObject<AppDomainObject.State>
}, ct); }, ct);
case RemoveContributor removeContributor: case RemoveContributor removeContributor:
return UpdateReturn(removeContributor, c => return ApplyReturn(removeContributor, c =>
{ {
GuardAppContributors.CanRemove(c, Snapshot); GuardAppContributors.CanRemove(c, Snapshot);
@ -148,7 +163,7 @@ public partial class AppDomainObject : DomainObject<AppDomainObject.State>
}, ct); }, ct);
case AttachClient attachClient: case AttachClient attachClient:
return UpdateReturn(attachClient, c => return ApplyReturn(attachClient, c =>
{ {
GuardAppClients.CanAttach(c, Snapshot); GuardAppClients.CanAttach(c, Snapshot);
@ -158,7 +173,7 @@ public partial class AppDomainObject : DomainObject<AppDomainObject.State>
}, ct); }, ct);
case UpdateClient updateClient: case UpdateClient updateClient:
return UpdateReturn(updateClient, c => return ApplyReturn(updateClient, c =>
{ {
GuardAppClients.CanUpdate(c, Snapshot); GuardAppClients.CanUpdate(c, Snapshot);
@ -168,7 +183,7 @@ public partial class AppDomainObject : DomainObject<AppDomainObject.State>
}, ct); }, ct);
case RevokeClient revokeClient: case RevokeClient revokeClient:
return UpdateReturn(revokeClient, c => return ApplyReturn(revokeClient, c =>
{ {
GuardAppClients.CanRevoke(c, Snapshot); GuardAppClients.CanRevoke(c, Snapshot);
@ -178,7 +193,7 @@ public partial class AppDomainObject : DomainObject<AppDomainObject.State>
}, ct); }, ct);
case AddWorkflow addWorkflow: case AddWorkflow addWorkflow:
return UpdateReturn(addWorkflow, c => return ApplyReturn(addWorkflow, c =>
{ {
GuardAppWorkflows.CanAdd(c); GuardAppWorkflows.CanAdd(c);
@ -188,7 +203,7 @@ public partial class AppDomainObject : DomainObject<AppDomainObject.State>
}, ct); }, ct);
case UpdateWorkflow updateWorkflow: case UpdateWorkflow updateWorkflow:
return UpdateReturn(updateWorkflow, c => return ApplyReturn(updateWorkflow, c =>
{ {
GuardAppWorkflows.CanUpdate(c, Snapshot); GuardAppWorkflows.CanUpdate(c, Snapshot);
@ -198,7 +213,7 @@ public partial class AppDomainObject : DomainObject<AppDomainObject.State>
}, ct); }, ct);
case DeleteWorkflow deleteWorkflow: case DeleteWorkflow deleteWorkflow:
return UpdateReturn(deleteWorkflow, c => return ApplyReturn(deleteWorkflow, c =>
{ {
GuardAppWorkflows.CanDelete(c, Snapshot); GuardAppWorkflows.CanDelete(c, Snapshot);
@ -208,7 +223,7 @@ public partial class AppDomainObject : DomainObject<AppDomainObject.State>
}, ct); }, ct);
case AddLanguage addLanguage: case AddLanguage addLanguage:
return UpdateReturn(addLanguage, c => return ApplyReturn(addLanguage, c =>
{ {
GuardAppLanguages.CanAdd(c, Snapshot); GuardAppLanguages.CanAdd(c, Snapshot);
@ -218,7 +233,7 @@ public partial class AppDomainObject : DomainObject<AppDomainObject.State>
}, ct); }, ct);
case RemoveLanguage removeLanguage: case RemoveLanguage removeLanguage:
return UpdateReturn(removeLanguage, c => return ApplyReturn(removeLanguage, c =>
{ {
GuardAppLanguages.CanRemove(c, Snapshot); GuardAppLanguages.CanRemove(c, Snapshot);
@ -228,7 +243,7 @@ public partial class AppDomainObject : DomainObject<AppDomainObject.State>
}, ct); }, ct);
case UpdateLanguage updateLanguage: case UpdateLanguage updateLanguage:
return UpdateReturn(updateLanguage, c => return ApplyReturn(updateLanguage, c =>
{ {
GuardAppLanguages.CanUpdate(c, Snapshot); GuardAppLanguages.CanUpdate(c, Snapshot);
@ -238,7 +253,7 @@ public partial class AppDomainObject : DomainObject<AppDomainObject.State>
}, ct); }, ct);
case AddRole addRole: case AddRole addRole:
return UpdateReturn(addRole, c => return ApplyReturn(addRole, c =>
{ {
GuardAppRoles.CanAdd(c, Snapshot); GuardAppRoles.CanAdd(c, Snapshot);
@ -248,7 +263,7 @@ public partial class AppDomainObject : DomainObject<AppDomainObject.State>
}, ct); }, ct);
case DeleteRole deleteRole: case DeleteRole deleteRole:
return UpdateReturn(deleteRole, c => return ApplyReturn(deleteRole, c =>
{ {
GuardAppRoles.CanDelete(c, Snapshot); GuardAppRoles.CanDelete(c, Snapshot);
@ -258,7 +273,7 @@ public partial class AppDomainObject : DomainObject<AppDomainObject.State>
}, ct); }, ct);
case UpdateRole updateRole: case UpdateRole updateRole:
return UpdateReturn(updateRole, c => return ApplyReturn(updateRole, c =>
{ {
GuardAppRoles.CanUpdate(c, Snapshot); GuardAppRoles.CanUpdate(c, Snapshot);
@ -268,7 +283,7 @@ public partial class AppDomainObject : DomainObject<AppDomainObject.State>
}, ct); }, ct);
case DeleteApp delete: case DeleteApp delete:
return UpdateAsync(delete, async (c, ct) => return ApplyAsync(delete, async (c, ct) =>
{ {
await BillingManager.UnsubscribeAsync(c.Actor.Identifier, Snapshot, default); await BillingManager.UnsubscribeAsync(c.Actor.Identifier, Snapshot, default);
@ -276,7 +291,7 @@ public partial class AppDomainObject : DomainObject<AppDomainObject.State>
}, ct); }, ct);
case ChangePlan changePlan: case ChangePlan changePlan:
return UpdateReturnAsync(changePlan, async (c, ct) => return ApplyReturnAsync(changePlan, async (c, ct) =>
{ {
GuardApp.CanChangePlan(c, Snapshot, BillingPlans); GuardApp.CanChangePlan(c, Snapshot, BillingPlans);

4
backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetCommandMiddleware.cs

@ -142,10 +142,6 @@ public sealed class AssetCommandMiddleware : CachingDomainObjectMiddleware<Asset
} }
catch (AssetAlreadyExistsException) catch (AssetAlreadyExistsException)
{ {
if (context.Command is not UpsertAsset)
{
throw;
}
} }
} }

39
backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetDomainObject.cs

@ -36,26 +36,29 @@ public partial class AssetDomainObject : DomainObject<AssetDomainObject.State>
return snapshot.IsDeleted; return snapshot.IsDeleted;
} }
protected override bool CanRecreate() protected override bool IsRecreation(IEvent @event)
{
return true;
}
protected override bool CanRecreate(IEvent @event)
{ {
return @event is AssetCreated; return @event is AssetCreated;
} }
protected override bool CanAcceptCreation(ICommand command) protected override bool CanAccept(ICommand command)
{ {
return command is AssetCommandBase; return command is AssetCommand c && c.AppId == Snapshot.AppId && c.AssetId == Snapshot.Id;
} }
protected override bool CanAccept(ICommand command) protected override bool CanAccept(ICommand command, DomainObjectState state)
{ {
return command is AssetCommand assetCommand && switch (state)
Equals(assetCommand.AppId, Snapshot.AppId) && {
Equals(assetCommand.AssetId, Snapshot.Id); case DomainObjectState.Undefined:
return command is CreateAsset;
case DomainObjectState.Empty:
return command is CreateAsset or UpsertAsset;
case DomainObjectState.Deleted:
return command is CreateAsset or UpsertAsset or DeleteAsset { Permanent: true };
default:
return command is not CreateAsset;
}
} }
public override Task<CommandResult> ExecuteAsync(IAggregateCommand command, public override Task<CommandResult> ExecuteAsync(IAggregateCommand command,
@ -64,7 +67,7 @@ public partial class AssetDomainObject : DomainObject<AssetDomainObject.State>
switch (command) switch (command)
{ {
case UpsertAsset upsert: case UpsertAsset upsert:
return UpsertReturnAsync(upsert, async (c, ct) => return ApplyReturnAsync(upsert, async (c, ct) =>
{ {
var operation = await AssetOperation.CreateAsync(serviceProvider, c, () => Snapshot); var operation = await AssetOperation.CreateAsync(serviceProvider, c, () => Snapshot);
@ -86,7 +89,7 @@ public partial class AssetDomainObject : DomainObject<AssetDomainObject.State>
}, ct); }, ct);
case CreateAsset create: case CreateAsset create:
return CreateReturnAsync(create, async (c, ct) => return ApplyReturnAsync(create, async (c, ct) =>
{ {
var operation = await AssetOperation.CreateAsync(serviceProvider, c, () => Snapshot); var operation = await AssetOperation.CreateAsync(serviceProvider, c, () => Snapshot);
@ -101,7 +104,7 @@ public partial class AssetDomainObject : DomainObject<AssetDomainObject.State>
}, ct); }, ct);
case AnnotateAsset annotate: case AnnotateAsset annotate:
return UpdateReturnAsync(annotate, async (c, ct) => return ApplyReturnAsync(annotate, async (c, ct) =>
{ {
var operation = await AssetOperation.CreateAsync(serviceProvider, c, () => Snapshot); var operation = await AssetOperation.CreateAsync(serviceProvider, c, () => Snapshot);
@ -111,7 +114,7 @@ public partial class AssetDomainObject : DomainObject<AssetDomainObject.State>
}, ct); }, ct);
case UpdateAsset update: case UpdateAsset update:
return UpdateReturnAsync(update, async (c, ct) => return ApplyReturnAsync(update, async (c, ct) =>
{ {
var operation = await AssetOperation.CreateAsync(serviceProvider, c, () => Snapshot); var operation = await AssetOperation.CreateAsync(serviceProvider, c, () => Snapshot);
@ -121,7 +124,7 @@ public partial class AssetDomainObject : DomainObject<AssetDomainObject.State>
}, ct); }, ct);
case MoveAsset move: case MoveAsset move:
return UpdateReturnAsync(move, async (c, ct) => return ApplyReturnAsync(move, async (c, ct) =>
{ {
var operation = await AssetOperation.CreateAsync(serviceProvider, c, () => Snapshot); var operation = await AssetOperation.CreateAsync(serviceProvider, c, () => Snapshot);
@ -139,7 +142,7 @@ public partial class AssetDomainObject : DomainObject<AssetDomainObject.State>
}, ct); }, ct);
case DeleteAsset delete: case DeleteAsset delete:
return UpdateAsync(delete, async (c, ct) => return ApplyAsync(delete, async (c, ct) =>
{ {
var operation = await AssetOperation.CreateAsync(serviceProvider, c, () => Snapshot); var operation = await AssetOperation.CreateAsync(serviceProvider, c, () => Snapshot);

28
backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetFolderDomainObject.cs

@ -36,16 +36,24 @@ public sealed partial class AssetFolderDomainObject : DomainObject<AssetFolderDo
return Snapshot.IsDeleted; return Snapshot.IsDeleted;
} }
protected override bool CanAcceptCreation(ICommand command) protected override bool CanAccept(ICommand command)
{ {
return command is AssetFolderCommandBase; return command is AssetFolderCommand c && c.AppId == Snapshot.AppId && c.AssetFolderId == Snapshot.Id;
} }
protected override bool CanAccept(ICommand command) protected override bool CanAccept(ICommand command, DomainObjectState state)
{ {
return command is AssetFolderCommand assetFolderCommand && switch (state)
Equals(assetFolderCommand.AppId, Snapshot.AppId) && {
Equals(assetFolderCommand.AssetFolderId, Snapshot.Id); case DomainObjectState.Undefined:
return command is CreateAssetFolder;
case DomainObjectState.Empty:
return command is CreateAssetFolder;
case DomainObjectState.Created:
return command is not CreateAssetFolder;
default:
return false;
}
} }
public override Task<CommandResult> ExecuteAsync(IAggregateCommand command, public override Task<CommandResult> ExecuteAsync(IAggregateCommand command,
@ -54,7 +62,7 @@ public sealed partial class AssetFolderDomainObject : DomainObject<AssetFolderDo
switch (command) switch (command)
{ {
case CreateAssetFolder create: case CreateAssetFolder create:
return CreateReturnAsync(create, async (c, ct) => return ApplyReturnAsync(create, async (c, ct) =>
{ {
await CreateCore(c, ct); await CreateCore(c, ct);
@ -62,7 +70,7 @@ public sealed partial class AssetFolderDomainObject : DomainObject<AssetFolderDo
}, ct); }, ct);
case MoveAssetFolder move: case MoveAssetFolder move:
return UpdateReturnAsync(move, async (c, ct) => return ApplyReturnAsync(move, async (c, ct) =>
{ {
await MoveCore(c, ct); await MoveCore(c, ct);
@ -70,7 +78,7 @@ public sealed partial class AssetFolderDomainObject : DomainObject<AssetFolderDo
}, ct); }, ct);
case RenameAssetFolder rename: case RenameAssetFolder rename:
return UpdateReturnAsync(rename, async (c, ct) => return ApplyReturnAsync(rename, async (c, ct) =>
{ {
await RenameCore(c); await RenameCore(c);
@ -78,7 +86,7 @@ public sealed partial class AssetFolderDomainObject : DomainObject<AssetFolderDo
}, ct); }, ct);
case DeleteAssetFolder delete: case DeleteAssetFolder delete:
return Update(delete, c => return Apply(delete, c =>
{ {
Delete(c); Delete(c);
}, ct); }, ct);

4
backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetQueryService.cs

@ -23,10 +23,10 @@ public interface IAssetQueryService
Task<IEnrichedAssetEntity?> FindByHashAsync(Context context, string hash, string fileName, long fileSize, Task<IEnrichedAssetEntity?> FindByHashAsync(Context context, string hash, string fileName, long fileSize,
CancellationToken ct = default); CancellationToken ct = default);
Task<IEnrichedAssetEntity?> FindAsync(Context context, DomainId id, long version = EtagVersion.Any, Task<IEnrichedAssetEntity?> FindAsync(Context context, DomainId id, bool allowDeleted = false, long version = EtagVersion.Any,
CancellationToken ct = default); CancellationToken ct = default);
Task<IEnrichedAssetEntity?> FindBySlugAsync(Context context, string slug, Task<IEnrichedAssetEntity?> FindBySlugAsync(Context context, string slug, bool allowDeleted = false,
CancellationToken ct = default); CancellationToken ct = default);
Task<IEnrichedAssetEntity?> FindGlobalAsync(Context context, DomainId id, Task<IEnrichedAssetEntity?> FindGlobalAsync(Context context, DomainId id,

1
backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetEnricher.cs

@ -7,7 +7,6 @@
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
using System.Diagnostics;
namespace Squidex.Domain.Apps.Entities.Assets.Queries; namespace Squidex.Domain.Apps.Entities.Assets.Queries;

16
backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryService.cs

@ -99,7 +99,7 @@ public sealed class AssetQueryService : IAssetQueryService
} }
} }
public async Task<IEnrichedAssetEntity?> FindBySlugAsync(Context context, string slug, public async Task<IEnrichedAssetEntity?> FindBySlugAsync(Context context, string slug, bool allowDeleted = false,
CancellationToken ct = default) CancellationToken ct = default)
{ {
Guard.NotNull(context); Guard.NotNull(context);
@ -108,7 +108,7 @@ public sealed class AssetQueryService : IAssetQueryService
{ {
activity?.SetTag("slug", slug); activity?.SetTag("slug", slug);
var asset = await FindBySlugCoreAsync(context, slug, ct); var asset = await FindBySlugCoreAsync(context, slug, allowDeleted, ct);
if (asset == null) if (asset == null)
{ {
@ -139,7 +139,7 @@ public sealed class AssetQueryService : IAssetQueryService
} }
} }
public async Task<IEnrichedAssetEntity?> FindAsync(Context context, DomainId id, long version = EtagVersion.Any, public async Task<IEnrichedAssetEntity?> FindAsync(Context context, DomainId id, bool allowDeleted = false, long version = EtagVersion.Any,
CancellationToken ct = default) CancellationToken ct = default)
{ {
Guard.NotNull(context); Guard.NotNull(context);
@ -156,7 +156,7 @@ public sealed class AssetQueryService : IAssetQueryService
} }
else else
{ {
asset = await FindCoreAsync(context, id, ct); asset = await FindCoreAsync(context, id, allowDeleted, ct);
} }
if (asset == null) if (asset == null)
@ -278,7 +278,7 @@ public sealed class AssetQueryService : IAssetQueryService
} }
} }
private async Task<IAssetEntity?> FindBySlugCoreAsync(Context context, string slug, private async Task<IAssetEntity?> FindBySlugCoreAsync(Context context, string slug, bool allowDeleted,
CancellationToken ct) CancellationToken ct)
{ {
using (var combined = CancellationTokenSource.CreateLinkedTokenSource(ct)) using (var combined = CancellationTokenSource.CreateLinkedTokenSource(ct))
@ -286,7 +286,7 @@ public sealed class AssetQueryService : IAssetQueryService
// Enforce a hard timeout // Enforce a hard timeout
combined.CancelAfter(options.TimeoutFind); combined.CancelAfter(options.TimeoutFind);
return await assetRepository.FindAssetBySlugAsync(context.App.Id, slug, combined.Token); return await assetRepository.FindAssetBySlugAsync(context.App.Id, slug, allowDeleted, combined.Token);
} }
} }
@ -302,7 +302,7 @@ public sealed class AssetQueryService : IAssetQueryService
} }
} }
private async Task<IAssetEntity?> FindCoreAsync(Context context, DomainId id, private async Task<IAssetEntity?> FindCoreAsync(Context context, DomainId id, bool allowDeleted,
CancellationToken ct) CancellationToken ct)
{ {
using (var combined = CancellationTokenSource.CreateLinkedTokenSource(ct)) using (var combined = CancellationTokenSource.CreateLinkedTokenSource(ct))
@ -310,7 +310,7 @@ public sealed class AssetQueryService : IAssetQueryService
// Enforce a hard timeout // Enforce a hard timeout
combined.CancelAfter(options.TimeoutFind); combined.CancelAfter(options.TimeoutFind);
return await assetRepository.FindAssetAsync(context.App.Id, id, combined.Token); return await assetRepository.FindAssetAsync(context.App.Id, id, allowDeleted, combined.Token);
} }
} }
} }

1
backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/Steps/CalculateTokens.cs

@ -5,7 +5,6 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Text;
using Squidex.Domain.Apps.Core; using Squidex.Domain.Apps.Core;
using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Json;

4
backend/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs

@ -26,12 +26,12 @@ public interface IAssetRepository
Task<IAssetEntity?> FindAssetByHashAsync(DomainId appId, string hash, string fileName, long fileSize, Task<IAssetEntity?> FindAssetByHashAsync(DomainId appId, string hash, string fileName, long fileSize,
CancellationToken ct = default); CancellationToken ct = default);
Task<IAssetEntity?> FindAssetBySlugAsync(DomainId appId, string slug, Task<IAssetEntity?> FindAssetBySlugAsync(DomainId appId, string slug, bool allowDeleted,
CancellationToken ct = default); CancellationToken ct = default);
Task<IAssetEntity?> FindAssetAsync(DomainId id, Task<IAssetEntity?> FindAssetAsync(DomainId id,
CancellationToken ct = default); CancellationToken ct = default);
Task<IAssetEntity?> FindAssetAsync(DomainId appId, DomainId id, Task<IAssetEntity?> FindAssetAsync(DomainId appId, DomainId id, bool allowDeleted,
CancellationToken ct = default); CancellationToken ct = default);
} }

7
backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObject.State.cs

@ -81,8 +81,8 @@ public partial class ContentDomainObject
NewVersion = new ContentVersion(e.Status, newData); NewVersion = new ContentVersion(e.Status, newData);
// Implictely cancels any pending update jobs.
ScheduleJob = null; ScheduleJob = null;
break; break;
} }
@ -90,8 +90,8 @@ public partial class ContentDomainObject
{ {
NewVersion = null; NewVersion = null;
// Implictely cancels any pending update jobs.
ScheduleJob = null; ScheduleJob = null;
break; break;
} }
@ -123,14 +123,12 @@ public partial class ContentDomainObject
case ContentSchedulingCancelled: case ContentSchedulingCancelled:
{ {
ScheduleJob = null; ScheduleJob = null;
break; break;
} }
case ContentStatusScheduled e: case ContentStatusScheduled e:
{ {
ScheduleJob = ScheduleJob.Build(e.Status, e.Actor, e.DueTime); ScheduleJob = ScheduleJob.Build(e.Status, e.Actor, e.DueTime);
break; break;
} }
@ -151,7 +149,6 @@ public partial class ContentDomainObject
case ContentDeleted: case ContentDeleted:
{ {
IsDeleted = true; IsDeleted = true;
break; break;
} }
} }

50
backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObject.cs

@ -40,27 +40,29 @@ public partial class ContentDomainObject : DomainObject<ContentDomainObject.Stat
return snapshot.IsDeleted; return snapshot.IsDeleted;
} }
protected override bool CanAcceptCreation(ICommand command) protected override bool IsRecreation(IEvent @event)
{ {
return command is ContentCommandBase; return @event is ContentCreated;
}
protected override bool CanRecreate()
{
return true;
} }
protected override bool CanRecreate(IEvent @event) protected override bool CanAccept(ICommand command)
{ {
return @event is ContentCreated; return command is ContentCommand c && c.AppId == Snapshot.AppId && c.SchemaId == Snapshot.SchemaId && c.ContentId == Snapshot.Id;
} }
protected override bool CanAccept(ICommand command) protected override bool CanAccept(ICommand command, DomainObjectState state)
{ {
return command is ContentCommand contentCommand && switch (state)
Equals(contentCommand.AppId, Snapshot.AppId) && {
Equals(contentCommand.SchemaId, Snapshot.SchemaId) && case DomainObjectState.Undefined:
Equals(contentCommand.ContentId, Snapshot.Id); return command is CreateContent;
case DomainObjectState.Deleted:
return command is CreateContent or UpsertContent or DeleteContent { Permanent: true };
case DomainObjectState.Empty:
return command is CreateContent or UpsertContent;
default:
return command is not CreateContent;
}
} }
public override Task<CommandResult> ExecuteAsync(IAggregateCommand command, public override Task<CommandResult> ExecuteAsync(IAggregateCommand command,
@ -69,7 +71,7 @@ public partial class ContentDomainObject : DomainObject<ContentDomainObject.Stat
switch (command) switch (command)
{ {
case UpsertContent upsertContent: case UpsertContent upsertContent:
return UpsertReturnAsync(upsertContent, async (c, ct) => return ApplyReturnAsync(upsertContent, async (c, ct) =>
{ {
var operation = await ContentOperation.CreateAsync(serviceProvider, c, () => Snapshot); var operation = await ContentOperation.CreateAsync(serviceProvider, c, () => Snapshot);
@ -95,7 +97,7 @@ public partial class ContentDomainObject : DomainObject<ContentDomainObject.Stat
}, ct); }, ct);
case CreateContent createContent: case CreateContent createContent:
return CreateReturnAsync(createContent, async (c, ct) => return ApplyReturnAsync(createContent, async (c, ct) =>
{ {
var operation = await ContentOperation.CreateAsync(serviceProvider, c, () => Snapshot); var operation = await ContentOperation.CreateAsync(serviceProvider, c, () => Snapshot);
@ -114,7 +116,7 @@ public partial class ContentDomainObject : DomainObject<ContentDomainObject.Stat
}, ct); }, ct);
case ValidateContent validate: case ValidateContent validate:
return UpdateReturnAsync(validate, async (c, ct) => return ApplyReturnAsync(validate, async (c, ct) =>
{ {
var operation = await ContentOperation.CreateAsync(serviceProvider, c, () => Snapshot); var operation = await ContentOperation.CreateAsync(serviceProvider, c, () => Snapshot);
@ -124,7 +126,7 @@ public partial class ContentDomainObject : DomainObject<ContentDomainObject.Stat
}, ct); }, ct);
case CreateContentDraft createDraft: case CreateContentDraft createDraft:
return UpdateReturnAsync(createDraft, async (c, ct) => return ApplyReturnAsync(createDraft, async (c, ct) =>
{ {
var operation = await ContentOperation.CreateAsync(serviceProvider, c, () => Snapshot); var operation = await ContentOperation.CreateAsync(serviceProvider, c, () => Snapshot);
@ -134,7 +136,7 @@ public partial class ContentDomainObject : DomainObject<ContentDomainObject.Stat
}, ct); }, ct);
case DeleteContentDraft deleteDraft: case DeleteContentDraft deleteDraft:
return UpdateReturnAsync(deleteDraft, async (c, ct) => return ApplyReturnAsync(deleteDraft, async (c, ct) =>
{ {
var operation = await ContentOperation.CreateAsync(serviceProvider, c, () => Snapshot); var operation = await ContentOperation.CreateAsync(serviceProvider, c, () => Snapshot);
@ -144,7 +146,7 @@ public partial class ContentDomainObject : DomainObject<ContentDomainObject.Stat
}, ct); }, ct);
case PatchContent patchContent: case PatchContent patchContent:
return UpdateReturnAsync(patchContent, async (c, ct) => return ApplyReturnAsync(patchContent, async (c, ct) =>
{ {
var operation = await ContentOperation.CreateAsync(serviceProvider, c, () => Snapshot); var operation = await ContentOperation.CreateAsync(serviceProvider, c, () => Snapshot);
@ -154,7 +156,7 @@ public partial class ContentDomainObject : DomainObject<ContentDomainObject.Stat
}, ct); }, ct);
case UpdateContent updateContent: case UpdateContent updateContent:
return UpdateReturnAsync(updateContent, async (c, ct) => return ApplyReturnAsync(updateContent, async (c, ct) =>
{ {
var operation = await ContentOperation.CreateAsync(serviceProvider, c, () => Snapshot); var operation = await ContentOperation.CreateAsync(serviceProvider, c, () => Snapshot);
@ -164,7 +166,7 @@ public partial class ContentDomainObject : DomainObject<ContentDomainObject.Stat
}, ct); }, ct);
case CancelContentSchedule cancelContentSchedule: case CancelContentSchedule cancelContentSchedule:
return UpdateReturnAsync(cancelContentSchedule, async (c, ct) => return ApplyReturnAsync(cancelContentSchedule, async (c, ct) =>
{ {
var operation = await ContentOperation.CreateAsync(serviceProvider, c, () => Snapshot); var operation = await ContentOperation.CreateAsync(serviceProvider, c, () => Snapshot);
@ -174,7 +176,7 @@ public partial class ContentDomainObject : DomainObject<ContentDomainObject.Stat
}, ct); }, ct);
case ChangeContentStatus changeContentStatus: case ChangeContentStatus changeContentStatus:
return UpdateReturnAsync(changeContentStatus, async (c, ct) => return ApplyReturnAsync(changeContentStatus, async (c, ct) =>
{ {
try try
{ {
@ -213,7 +215,7 @@ public partial class ContentDomainObject : DomainObject<ContentDomainObject.Stat
}, ct); }, ct);
case DeleteContent deleteContent: case DeleteContent deleteContent:
return UpdateAsync(deleteContent, async (c, ct) => return ApplyAsync(deleteContent, async (c, ct) =>
{ {
var operation = await ContentOperation.CreateAsync(serviceProvider, c, () => Snapshot); var operation = await ContentOperation.CreateAsync(serviceProvider, c, () => Snapshot);

1
backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/CalculateTokens.cs

@ -5,7 +5,6 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Text;
using Squidex.Domain.Apps.Core; using Squidex.Domain.Apps.Core;
using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Json;

32
backend/src/Squidex.Domain.Apps.Entities/Rules/DomainObject/RuleDomainObject.cs

@ -37,16 +37,24 @@ public partial class RuleDomainObject : DomainObject<RuleDomainObject.State>
return snapshot.IsDeleted; return snapshot.IsDeleted;
} }
protected override bool CanAcceptCreation(ICommand command) protected override bool CanAccept(ICommand command)
{ {
return command is RuleCommandBase; return command is RuleCommand c && c.AppId.Equals(Snapshot.AppId) && c.RuleId.Equals(Snapshot.Id);
} }
protected override bool CanAccept(ICommand command) protected override bool CanAccept(ICommand command, DomainObjectState state)
{ {
return command is RuleCommand ruleCommand && switch (state)
ruleCommand.AppId.Equals(Snapshot.AppId) && {
ruleCommand.RuleId.Equals(Snapshot.Id); case DomainObjectState.Undefined:
return command is CreateRule;
case DomainObjectState.Empty:
return command is CreateRule;
case DomainObjectState.Created:
return command is not CreateRule;
default:
return false;
}
} }
public override Task<CommandResult> ExecuteAsync(IAggregateCommand command, public override Task<CommandResult> ExecuteAsync(IAggregateCommand command,
@ -55,7 +63,7 @@ public partial class RuleDomainObject : DomainObject<RuleDomainObject.State>
switch (command) switch (command)
{ {
case CreateRule createRule: case CreateRule createRule:
return CreateReturnAsync(createRule, async (c, ct) => return ApplyReturnAsync(createRule, async (c, ct) =>
{ {
await GuardRule.CanCreate(c, AppProvider()); await GuardRule.CanCreate(c, AppProvider());
@ -65,7 +73,7 @@ public partial class RuleDomainObject : DomainObject<RuleDomainObject.State>
}, ct); }, ct);
case UpdateRule updateRule: case UpdateRule updateRule:
return UpdateReturnAsync(updateRule, async (c, ct) => return ApplyReturnAsync(updateRule, async (c, ct) =>
{ {
await GuardRule.CanUpdate(c, Snapshot, AppProvider()); await GuardRule.CanUpdate(c, Snapshot, AppProvider());
@ -75,7 +83,7 @@ public partial class RuleDomainObject : DomainObject<RuleDomainObject.State>
}, ct); }, ct);
case EnableRule enable: case EnableRule enable:
return UpdateReturn(enable, c => return ApplyReturn(enable, c =>
{ {
Enable(c); Enable(c);
@ -83,7 +91,7 @@ public partial class RuleDomainObject : DomainObject<RuleDomainObject.State>
}, ct); }, ct);
case DisableRule disable: case DisableRule disable:
return UpdateReturn(disable, c => return ApplyReturn(disable, c =>
{ {
Disable(c); Disable(c);
@ -91,13 +99,13 @@ public partial class RuleDomainObject : DomainObject<RuleDomainObject.State>
}, ct); }, ct);
case DeleteRule delete: case DeleteRule delete:
return Update(delete, c => return Apply(delete, c =>
{ {
Delete(c); Delete(c);
}, ct); }, ct);
case TriggerRule triggerRule: case TriggerRule triggerRule:
return UpdateReturnAsync(triggerRule, async (c, ct) => return ApplyReturnAsync(triggerRule, async (c, ct) =>
{ {
await Trigger(triggerRule); await Trigger(triggerRule);

63
backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/SchemaDomainObject.cs

@ -34,16 +34,24 @@ public sealed partial class SchemaDomainObject : DomainObject<SchemaDomainObject
return snapshot.IsDeleted; return snapshot.IsDeleted;
} }
protected override bool CanAcceptCreation(ICommand command) protected override bool CanAccept(ICommand command)
{ {
return command is SchemaCommandBase; return command is SchemaCommand c && Equals(c.AppId, Snapshot.AppId) && Equals(c.SchemaId?.Id, Snapshot.Id);
} }
protected override bool CanAccept(ICommand command) protected override bool CanAccept(ICommand command, DomainObjectState state)
{ {
return command is SchemaCommand schemaCommand && switch (state)
Equals(schemaCommand.AppId, Snapshot.AppId) && {
Equals(schemaCommand.SchemaId?.Id, Snapshot.Id); case DomainObjectState.Undefined:
return command is CreateSchema;
case DomainObjectState.Empty:
return command is CreateSchema;
case DomainObjectState.Created:
return command is not CreateSchema;
default:
return false;
}
} }
public override Task<CommandResult> ExecuteAsync(IAggregateCommand command, public override Task<CommandResult> ExecuteAsync(IAggregateCommand command,
@ -52,7 +60,7 @@ public sealed partial class SchemaDomainObject : DomainObject<SchemaDomainObject
switch (command) switch (command)
{ {
case AddField addField: case AddField addField:
return UpdateReturn(addField, c => return ApplyReturn(addField, c =>
{ {
GuardSchemaField.CanAdd(c, Snapshot.SchemaDef); GuardSchemaField.CanAdd(c, Snapshot.SchemaDef);
@ -62,7 +70,7 @@ public sealed partial class SchemaDomainObject : DomainObject<SchemaDomainObject
}, ct); }, ct);
case CreateSchema createSchema: case CreateSchema createSchema:
return CreateReturn(createSchema, c => return ApplyReturn(createSchema, c =>
{ {
GuardSchema.CanCreate(c); GuardSchema.CanCreate(c);
@ -72,7 +80,7 @@ public sealed partial class SchemaDomainObject : DomainObject<SchemaDomainObject
}, ct); }, ct);
case SynchronizeSchema synchronize: case SynchronizeSchema synchronize:
return UpdateReturn(synchronize, c => return ApplyReturn(synchronize, c =>
{ {
GuardSchema.CanSynchronize(c); GuardSchema.CanSynchronize(c);
@ -82,7 +90,7 @@ public sealed partial class SchemaDomainObject : DomainObject<SchemaDomainObject
}, ct); }, ct);
case DeleteField deleteField: case DeleteField deleteField:
return UpdateReturn(deleteField, c => return ApplyReturn(deleteField, c =>
{ {
GuardSchemaField.CanDelete(deleteField, Snapshot.SchemaDef); GuardSchemaField.CanDelete(deleteField, Snapshot.SchemaDef);
@ -92,7 +100,7 @@ public sealed partial class SchemaDomainObject : DomainObject<SchemaDomainObject
}, ct); }, ct);
case LockField lockField: case LockField lockField:
return UpdateReturn(lockField, c => return ApplyReturn(lockField, c =>
{ {
GuardSchemaField.CanLock(lockField, Snapshot.SchemaDef); GuardSchemaField.CanLock(lockField, Snapshot.SchemaDef);
@ -102,7 +110,7 @@ public sealed partial class SchemaDomainObject : DomainObject<SchemaDomainObject
}, ct); }, ct);
case HideField hideField: case HideField hideField:
return UpdateReturn(hideField, c => return ApplyReturn(hideField, c =>
{ {
GuardSchemaField.CanHide(c, Snapshot.SchemaDef); GuardSchemaField.CanHide(c, Snapshot.SchemaDef);
@ -112,7 +120,7 @@ public sealed partial class SchemaDomainObject : DomainObject<SchemaDomainObject
}, ct); }, ct);
case ShowField showField: case ShowField showField:
return UpdateReturn(showField, c => return ApplyReturn(showField, c =>
{ {
GuardSchemaField.CanShow(c, Snapshot.SchemaDef); GuardSchemaField.CanShow(c, Snapshot.SchemaDef);
@ -122,7 +130,7 @@ public sealed partial class SchemaDomainObject : DomainObject<SchemaDomainObject
}, ct); }, ct);
case DisableField disableField: case DisableField disableField:
return UpdateReturn(disableField, c => return ApplyReturn(disableField, c =>
{ {
GuardSchemaField.CanDisable(c, Snapshot.SchemaDef); GuardSchemaField.CanDisable(c, Snapshot.SchemaDef);
@ -132,7 +140,7 @@ public sealed partial class SchemaDomainObject : DomainObject<SchemaDomainObject
}, ct); }, ct);
case EnableField enableField: case EnableField enableField:
return UpdateReturn(enableField, c => return ApplyReturn(enableField, c =>
{ {
GuardSchemaField.CanEnable(c, Snapshot.SchemaDef); GuardSchemaField.CanEnable(c, Snapshot.SchemaDef);
@ -142,7 +150,7 @@ public sealed partial class SchemaDomainObject : DomainObject<SchemaDomainObject
}, ct); }, ct);
case UpdateField updateField: case UpdateField updateField:
return UpdateReturn(updateField, c => return ApplyReturn(updateField, c =>
{ {
GuardSchemaField.CanUpdate(c, Snapshot.SchemaDef); GuardSchemaField.CanUpdate(c, Snapshot.SchemaDef);
@ -152,7 +160,7 @@ public sealed partial class SchemaDomainObject : DomainObject<SchemaDomainObject
}, ct); }, ct);
case ReorderFields reorderFields: case ReorderFields reorderFields:
return UpdateReturn(reorderFields, c => return ApplyReturn(reorderFields, c =>
{ {
GuardSchema.CanReorder(c, Snapshot.SchemaDef); GuardSchema.CanReorder(c, Snapshot.SchemaDef);
@ -162,7 +170,7 @@ public sealed partial class SchemaDomainObject : DomainObject<SchemaDomainObject
}, ct); }, ct);
case ConfigureFieldRules configureFieldRules: case ConfigureFieldRules configureFieldRules:
return UpdateReturn(configureFieldRules, c => return ApplyReturn(configureFieldRules, c =>
{ {
GuardSchema.CanConfigureFieldRules(c); GuardSchema.CanConfigureFieldRules(c);
@ -172,7 +180,7 @@ public sealed partial class SchemaDomainObject : DomainObject<SchemaDomainObject
}, ct); }, ct);
case ConfigurePreviewUrls configurePreviewUrls: case ConfigurePreviewUrls configurePreviewUrls:
return UpdateReturn(configurePreviewUrls, c => return ApplyReturn(configurePreviewUrls, c =>
{ {
GuardSchema.CanConfigurePreviewUrls(c); GuardSchema.CanConfigurePreviewUrls(c);
@ -182,7 +190,7 @@ public sealed partial class SchemaDomainObject : DomainObject<SchemaDomainObject
}, ct); }, ct);
case ConfigureUIFields configureUIFields: case ConfigureUIFields configureUIFields:
return UpdateReturn(configureUIFields, c => return ApplyReturn(configureUIFields, c =>
{ {
GuardSchema.CanConfigureUIFields(c, Snapshot.SchemaDef); GuardSchema.CanConfigureUIFields(c, Snapshot.SchemaDef);
@ -192,7 +200,7 @@ public sealed partial class SchemaDomainObject : DomainObject<SchemaDomainObject
}, ct); }, ct);
case ChangeCategory changeCategory: case ChangeCategory changeCategory:
return UpdateReturn(changeCategory, c => return ApplyReturn(changeCategory, c =>
{ {
ChangeCategory(c); ChangeCategory(c);
@ -200,7 +208,7 @@ public sealed partial class SchemaDomainObject : DomainObject<SchemaDomainObject
}, ct); }, ct);
case UpdateSchema update: case UpdateSchema update:
return UpdateReturn(update, c => return ApplyReturn(update, c =>
{ {
Update(c); Update(c);
@ -208,7 +216,7 @@ public sealed partial class SchemaDomainObject : DomainObject<SchemaDomainObject
}, ct); }, ct);
case PublishSchema publish: case PublishSchema publish:
return UpdateReturn(publish, c => return ApplyReturn(publish, c =>
{ {
Publish(c); Publish(c);
@ -216,7 +224,7 @@ public sealed partial class SchemaDomainObject : DomainObject<SchemaDomainObject
}, ct); }, ct);
case UnpublishSchema unpublish: case UnpublishSchema unpublish:
return UpdateReturn(unpublish, c => return ApplyReturn(unpublish, c =>
{ {
Unpublish(c); Unpublish(c);
@ -224,7 +232,7 @@ public sealed partial class SchemaDomainObject : DomainObject<SchemaDomainObject
}, ct); }, ct);
case ConfigureScripts configureScripts: case ConfigureScripts configureScripts:
return UpdateReturn(configureScripts, c => return ApplyReturn(configureScripts, c =>
{ {
ConfigureScripts(c); ConfigureScripts(c);
@ -232,10 +240,7 @@ public sealed partial class SchemaDomainObject : DomainObject<SchemaDomainObject
}, ct); }, ct);
case DeleteSchema deleteSchema: case DeleteSchema deleteSchema:
return Update(deleteSchema, c => return Apply(deleteSchema, Delete, ct);
{
Delete(c);
}, ct);
default: default:
ThrowHelper.NotSupportedException(); ThrowHelper.NotSupportedException();

28
backend/src/Squidex.Domain.Apps.Entities/Teams/DomainObject/TeamDomainObject.cs

@ -39,14 +39,24 @@ public partial class TeamDomainObject : DomainObject<TeamDomainObject.State>
return false; return false;
} }
protected override bool CanAcceptCreation(ICommand command) protected override bool CanAccept(ICommand command)
{ {
return command is TeamCommandBase; return command is TeamCommand c && Equals(c.TeamId, Snapshot.Id);
} }
protected override bool CanAccept(ICommand command) protected override bool CanAccept(ICommand command, DomainObjectState state)
{ {
return command is TeamCommand update && Equals(update?.TeamId, Snapshot.Id); switch (state)
{
case DomainObjectState.Undefined:
return command is CreateTeam;
case DomainObjectState.Empty:
return command is CreateTeam;
case DomainObjectState.Created:
return command is not CreateTeam;
default:
return false;
}
} }
public override Task<CommandResult> ExecuteAsync(IAggregateCommand command, public override Task<CommandResult> ExecuteAsync(IAggregateCommand command,
@ -55,7 +65,7 @@ public partial class TeamDomainObject : DomainObject<TeamDomainObject.State>
switch (command) switch (command)
{ {
case CreateTeam create: case CreateTeam create:
return CreateReturn(create, c => return ApplyReturn(create, c =>
{ {
GuardTeam.CanCreate(c); GuardTeam.CanCreate(c);
@ -65,7 +75,7 @@ public partial class TeamDomainObject : DomainObject<TeamDomainObject.State>
}, ct); }, ct);
case UpdateTeam update: case UpdateTeam update:
return UpdateReturn(update, c => return ApplyReturn(update, c =>
{ {
GuardTeam.CanUpdate(c); GuardTeam.CanUpdate(c);
@ -75,7 +85,7 @@ public partial class TeamDomainObject : DomainObject<TeamDomainObject.State>
}, ct); }, ct);
case AssignContributor assignContributor: case AssignContributor assignContributor:
return UpdateReturnAsync(assignContributor, async (c, ct) => return ApplyReturnAsync(assignContributor, async (c, ct) =>
{ {
await GuardTeamContributors.CanAssign(c, Snapshot, Users); await GuardTeamContributors.CanAssign(c, Snapshot, Users);
@ -85,7 +95,7 @@ public partial class TeamDomainObject : DomainObject<TeamDomainObject.State>
}, ct); }, ct);
case RemoveContributor removeContributor: case RemoveContributor removeContributor:
return UpdateReturn(removeContributor, c => return ApplyReturn(removeContributor, c =>
{ {
GuardTeamContributors.CanRemove(c, Snapshot); GuardTeamContributors.CanRemove(c, Snapshot);
@ -95,7 +105,7 @@ public partial class TeamDomainObject : DomainObject<TeamDomainObject.State>
}, ct); }, ct);
case ChangePlan changePlan: case ChangePlan changePlan:
return UpdateReturnAsync(changePlan, async (c, ct) => return ApplyReturnAsync(changePlan, async (c, ct) =>
{ {
GuardTeam.CanChangePlan(c, BillingPlans); GuardTeam.CanChangePlan(c, BillingPlans);

215
backend/src/Squidex.Infrastructure/Commands/DomainObject.Execute.cs

@ -9,133 +9,40 @@ namespace Squidex.Infrastructure.Commands;
public partial class DomainObject<T> public partial class DomainObject<T>
{ {
protected Task<CommandResult> CreateReturnAsync<TCommand>(TCommand command, Func<TCommand, CancellationToken, Task<object?>> handler, protected async Task<CommandResult> ApplyReturnAsync<TCommand>(TCommand command, Func<TCommand, CancellationToken, Task<object?>> handler,
CancellationToken ct = default) where TCommand : ICommand CancellationToken ct = default) where TCommand : ICommand
{ {
EnsureCanCreate(command); return await UpsertCoreAsync(command, handler, ct);
return UpsertCoreAsync(command, handler, true, ct);
} }
protected Task<CommandResult> CreateReturn<TCommand>(TCommand command, Func<TCommand, object?> handler, protected async Task<CommandResult> ApplyReturn<TCommand>(TCommand command, Func<TCommand, object?> handler,
CancellationToken ct = default) where TCommand : ICommand CancellationToken ct = default) where TCommand : ICommand
{ {
return CreateReturnAsync(command, (c, _) => return await UpsertCoreAsync(command, (c, _) =>
{ {
var result = handler(c); var result = handler(c);
return Task.FromResult(result); return Task.FromResult(result);
}, ct); }, ct);
} }
protected Task<CommandResult> CreateAsync<TCommand>(TCommand command, Func<TCommand, CancellationToken, Task> handler, protected async Task<CommandResult> ApplyAsync<TCommand>(TCommand command, Func<TCommand, CancellationToken, Task> handler,
CancellationToken ct = default) where TCommand : ICommand
{
EnsureCanCreate(command);
return UpsertCoreAsync(command, async (c, ct) =>
{
await handler(c, ct);
return None.Value;
}, true, ct);
}
protected Task<CommandResult> Create<TCommand>(TCommand command, Action<TCommand> handler,
CancellationToken ct = default) where TCommand : ICommand
{
return CreateAsync(command, (c, ct) =>
{
handler(c);
return Task.FromResult<object?>(None.Value);
}, ct);
}
protected async Task<CommandResult> UpdateReturnAsync<TCommand>(TCommand command, Func<TCommand, CancellationToken, Task<object?>> handler,
CancellationToken ct = default) where TCommand : ICommand CancellationToken ct = default) where TCommand : ICommand
{ {
await EnsureCanUpdateAsync(command, ct);
return await UpsertCoreAsync(command, handler, false, ct);
}
protected Task<CommandResult> UpdateReturn<TCommand>(TCommand command, Func<TCommand, object?> handler,
CancellationToken ct = default) where TCommand : ICommand
{
return UpdateReturnAsync(command, (c, ct) =>
{
var result = handler(c);
return Task.FromResult(result);
}, ct);
}
protected async Task<CommandResult> UpdateAsync<TCommand>(TCommand command, Func<TCommand, CancellationToken, Task> handler,
CancellationToken ct = default) where TCommand : ICommand
{
await EnsureCanUpdateAsync(command, ct);
return await UpsertCoreAsync(command, async (c, ct) => return await UpsertCoreAsync(command, async (c, ct) =>
{ {
await handler(c, ct); await handler(c, ct);
return None.Value; return None.Value;
}, false, ct);
}
protected async Task<CommandResult> Update<TCommand>(TCommand command, Action<TCommand> handler,
CancellationToken ct = default) where TCommand : ICommand
{
return await UpdateAsync(command, (c, _) =>
{
handler(c);
return Task.FromResult<object?>(None.Value);
}, ct); }, ct);
} }
protected async Task<CommandResult> UpsertReturnAsync<TCommand>(TCommand command, Func<TCommand, CancellationToken, Task<object?>> handler, protected async Task<CommandResult> Apply<TCommand>(TCommand command, Action<TCommand> handler,
CancellationToken ct = default) where TCommand : ICommand
{
await EnsureCanUpsertAsync(command, ct);
return await UpsertCoreAsync(command, handler, true, ct);
}
protected async Task<CommandResult> UpsertReturn<TCommand>(TCommand command, Func<TCommand, object?> handler,
CancellationToken ct = default) where TCommand : ICommand
{
return await UpsertReturnAsync(command, (c, _) =>
{
var result = handler(c);
return Task.FromResult(result);
}, ct);
}
protected async Task<CommandResult> UpsertAsync<TCommand>(TCommand command, Func<TCommand, CancellationToken, Task> handler,
CancellationToken ct = default) where TCommand : ICommand
{
await EnsureCanUpsertAsync(command, ct);
return await UpsertCoreAsync(command, async (c, ct) =>
{
await handler(c, ct);
return None.Value;
}, true, ct);
}
protected async Task<CommandResult> Upsert<TCommand>(TCommand command, Action<TCommand> handler,
CancellationToken ct = default) where TCommand : ICommand CancellationToken ct = default) where TCommand : ICommand
{ {
Guard.NotNull(handler); Guard.NotNull(handler);
return await UpsertAsync(command, (c, _) => return await UpsertCoreAsync(command, (c, _) =>
{ {
handler(c); handler(c);
return Task.FromResult<object?>(None.Value); return Task.FromResult<object?>(None.Value);
}, ct); }, ct);
} }
@ -145,8 +52,6 @@ public partial class DomainObject<T>
{ {
Guard.NotNull(handler); Guard.NotNull(handler);
await EnsureCanDeleteAsync(command, ct);
return await DeleteCoreAsync(command, async (c, ct) => return await DeleteCoreAsync(command, async (c, ct) =>
{ {
await handler(c, ct); await handler(c, ct);
@ -160,115 +65,11 @@ public partial class DomainObject<T>
{ {
Guard.NotNull(handler); Guard.NotNull(handler);
return await DeletePermanentAsync(command, (c, _) => return await DeleteCoreAsync(command, (c, _) =>
{ {
handler(c); handler(c);
return Task.FromResult<object?>(None.Value); return Task.FromResult<object?>(None.Value);
}, ct); }, ct);
} }
private void EnsureCanCreate<TCommand>(TCommand command) where TCommand : ICommand
{
Guard.NotNull(command);
if (Version != EtagVersion.Empty && !(IsDeleted(Snapshot) && CanRecreate()))
{
throw new DomainObjectConflictException(uniqueId.ToString());
}
MatchingVersion(command);
MatchingCreateCommand(command);
}
private async Task EnsureCanUpdateAsync<TCommand>(TCommand command,
CancellationToken ct) where TCommand : ICommand
{
Guard.NotNull(command);
await EnsureLoadedAsync(ct);
NotDeleted();
NotEmpty();
MatchingVersion(command);
MatchingCommand(command);
}
private async Task EnsureCanUpsertAsync<TCommand>(TCommand command,
CancellationToken ct) where TCommand : ICommand
{
Guard.NotNull(command);
await EnsureLoadedAsync(ct);
if (IsDeleted(Snapshot) && !CanRecreate())
{
throw new DomainObjectDeletedException(uniqueId.ToString());
}
MatchingVersion(command);
if (Version <= EtagVersion.Empty)
{
MatchingCreateCommand(command);
}
else
{
MatchingCommand(command);
}
}
private async Task EnsureCanDeleteAsync<TCommand>(TCommand command,
CancellationToken ct) where TCommand : ICommand
{
Guard.NotNull(command);
await EnsureLoadedAsync(ct);
NotEmpty();
MatchingVersion(command);
MatchingCommand(command);
}
private void NotDeleted()
{
if (IsDeleted(Snapshot))
{
throw new DomainObjectDeletedException(uniqueId.ToString());
}
}
private void NotEmpty()
{
if (Version <= EtagVersion.Empty)
{
throw new DomainObjectNotFoundException(uniqueId.ToString());
}
}
private void MatchingVersion<TCommand>(TCommand command) where TCommand : ICommand
{
if (Version > EtagVersion.Empty && command.ExpectedVersion > EtagVersion.Any && Version != command.ExpectedVersion)
{
throw new DomainObjectVersionException(uniqueId.ToString(), Version, command.ExpectedVersion);
}
}
private void MatchingCreateCommand<TCommand>(TCommand command) where TCommand : ICommand
{
if (!CanAcceptCreation(command))
{
throw new DomainException("Invalid command.");
}
}
private void MatchingCommand<TCommand>(TCommand command) where TCommand : ICommand
{
if (!CanAccept(command))
{
throw new DomainException("Invalid command.");
}
}
} }

114
backend/src/Squidex.Infrastructure/Commands/DomainObject.cs

@ -39,6 +39,29 @@ public abstract partial class DomainObject<T> : IAggregate where T : class, IDom
get => snapshot.Version; get => snapshot.Version;
} }
public DomainObjectState ObjectState
{
get
{
if (!isLoaded)
{
return DomainObjectState.Undefined;
}
if (IsDeleted(Snapshot))
{
return DomainObjectState.Deleted;
}
if (Version <= EtagVersion.Empty)
{
return DomainObjectState.Empty;
}
return DomainObjectState.Created;
}
}
protected DomainObject(DomainId uniqueId, IPersistenceFactory<T> persistenceFactory, protected DomainObject(DomainId uniqueId, IPersistenceFactory<T> persistenceFactory,
ILogger log) ILogger log)
{ {
@ -170,6 +193,8 @@ public abstract partial class DomainObject<T> : IAggregate where T : class, IDom
{ {
Guard.NotNull(handler); Guard.NotNull(handler);
await EnsureCommandAsync(command, ct);
var previousSnapshot = Snapshot; var previousSnapshot = Snapshot;
var previousVersion = Version; var previousVersion = Version;
try try
@ -207,11 +232,15 @@ public abstract partial class DomainObject<T> : IAggregate where T : class, IDom
} }
} }
private async Task<CommandResult> UpsertCoreAsync<TCommand>(TCommand command, Func<TCommand, CancellationToken, Task<object?>> handler, bool isCreation, private async Task<CommandResult> UpsertCoreAsync<TCommand>(TCommand command, Func<TCommand, CancellationToken, Task<object?>> handler,
CancellationToken ct) where TCommand : ICommand CancellationToken ct) where TCommand : ICommand
{ {
Guard.NotNull(handler); Guard.NotNull(handler);
var isDryRun = !isLoaded && CanAccept(command, DomainObjectState.Undefined);
await EnsureCommandAsync(command, ct);
var previousSnapshot = Snapshot; var previousSnapshot = Snapshot;
var previousVersion = Version; var previousVersion = Version;
try try
@ -225,35 +254,29 @@ public abstract partial class DomainObject<T> : IAggregate where T : class, IDom
} }
catch (InconsistentStateException ex) catch (InconsistentStateException ex)
{ {
if (!isDryRun)
{
throw new DomainObjectVersionException(uniqueId.ToString(), ex.VersionCurrent, ex.VersionExpected, ex);
}
// Start from the previous, unchanged snapshot. // Start from the previous, unchanged snapshot.
snapshot = previousSnapshot; snapshot = previousSnapshot;
// Create commands do not load the domain object for performance reasons, therefore we ensure it is loaded. // Create commands do not load the domain object for performance reasons, therefore we ensure it is loaded.
await EnsureLoadedAsync(ct); await EnsureLoadedAsync(ct);
var isDeleted = IsDeleted(Snapshot); previousVersion = Version;
previousSnapshot = Snapshot;
if (isDeleted && isCreation && CanRecreate()) // Run the validation again, because we could not make some checks before.
{ EnsureCommand(command);
foreach (var @event in uncomittedEvents)
{
ApplyEvent(@event, Snapshot, Version, false, true);
}
await WriteAsync(events, ct); foreach (var @event in uncomittedEvents)
}
else if (isDeleted)
{
throw new DomainObjectDeletedException(uniqueId.ToString());
}
else if (isCreation)
{ {
throw new DomainObjectConflictException(uniqueId.ToString()); ApplyEvent(@event, Snapshot, Version, false, true);
}
else
{
throw new DomainObjectVersionException(uniqueId.ToString(), ex.VersionCurrent, ex.VersionExpected, ex);
} }
await WriteAsync(events, ct);
} }
isLoaded = true; isLoaded = true;
@ -271,14 +294,46 @@ public abstract partial class DomainObject<T> : IAggregate where T : class, IDom
} }
} }
protected virtual bool CanAcceptCreation(ICommand command) private async Task EnsureCommandAsync<TCommand>(TCommand command,
CancellationToken ct) where TCommand : ICommand
{ {
return true; if (!isLoaded && !CanAccept(command, DomainObjectState.Undefined))
{
await EnsureLoadedAsync(ct);
}
if (isLoaded)
{
EnsureCommand(command);
}
} }
protected virtual bool CanAccept(ICommand command) private void EnsureCommand<TCommand>(TCommand command) where TCommand : ICommand
{ {
return true; if (ObjectState > DomainObjectState.Empty && !CanAccept(command))
{
throw new DomainException("Invalid command.");
}
if (!CanAccept(command, ObjectState))
{
if (IsDeleted(Snapshot))
{
throw new DomainObjectDeletedException(uniqueId.ToString());
}
if (Version <= EtagVersion.Empty)
{
throw new DomainObjectNotFoundException(uniqueId.ToString());
}
throw new DomainObjectConflictException(uniqueId.ToString());
}
if (Version > EtagVersion.Empty && command.ExpectedVersion > EtagVersion.Any && Version != command.ExpectedVersion)
{
throw new DomainObjectVersionException(uniqueId.ToString(), Version, command.ExpectedVersion);
}
} }
protected virtual bool IsDeleted(T snapshot) protected virtual bool IsDeleted(T snapshot)
@ -286,12 +341,17 @@ public abstract partial class DomainObject<T> : IAggregate where T : class, IDom
return false; return false;
} }
protected virtual bool CanRecreate() protected virtual bool IsRecreation(IEvent @event)
{
return false;
}
protected virtual bool CanAccept(ICommand command)
{ {
return false; return false;
} }
protected virtual bool CanRecreate(IEvent @event) protected virtual bool CanAccept(ICommand command, DomainObjectState state)
{ {
return false; return false;
} }
@ -300,7 +360,7 @@ public abstract partial class DomainObject<T> : IAggregate where T : class, IDom
{ {
if (IsDeleted(snapshot)) if (IsDeleted(snapshot))
{ {
if (!CanRecreate(@event.Payload)) if (!IsRecreation(@event.Payload))
{ {
return default; return default;
} }

16
backend/src/Squidex.Infrastructure/Commands/DomainObjectState.cs

@ -0,0 +1,16 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Infrastructure.Commands;
public enum DomainObjectState
{
Undefined,
Empty,
Created,
Deleted
}

2
backend/src/Squidex.Infrastructure/DomainId.cs

@ -55,7 +55,7 @@ public readonly struct DomainId : IEquatable<DomainId>, IComparable<DomainId>
public override bool Equals(object? obj) public override bool Equals(object? obj)
{ {
return obj is DomainId status && Equals(status); return obj is DomainId id && Equals(id);
} }
public bool Equals(DomainId other) public bool Equals(DomainId other)

3
backend/src/Squidex.Infrastructure/RandomHash.cs

@ -5,9 +5,6 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Security.Cryptography;
using System.Text;
namespace Squidex.Infrastructure; namespace Squidex.Infrastructure;
public static class RandomHash public static class RandomHash

1
backend/src/Squidex.Web/ETagExtensions.cs

@ -7,7 +7,6 @@
using System.Globalization; using System.Globalization;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities;
using Squidex.Infrastructure; using Squidex.Infrastructure;

6
backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs

@ -64,11 +64,11 @@ public sealed class AssetContentController : ApiController
{ {
var requestContext = Context.Clone(b => b.WithNoAssetEnrichment()); var requestContext = Context.Clone(b => b.WithNoAssetEnrichment());
var asset = await assetQuery.FindAsync(requestContext, DomainId.Create(idOrSlug), ct: HttpContext.RequestAborted); var asset = await assetQuery.FindAsync(requestContext, DomainId.Create(idOrSlug), request.Deleted, ct: HttpContext.RequestAborted);
if (asset == null) if (asset == null)
{ {
asset = await assetQuery.FindBySlugAsync(requestContext, idOrSlug, HttpContext.RequestAborted); asset = await assetQuery.FindBySlugAsync(requestContext, idOrSlug, request.Deleted, HttpContext.RequestAborted);
} }
return await DeliverAssetAsync(requestContext, asset, request); return await DeliverAssetAsync(requestContext, asset, request);
@ -117,7 +117,7 @@ public sealed class AssetContentController : ApiController
{ {
if (context.App != null) if (context.App != null)
{ {
asset = await assetQuery.FindAsync(context, asset.Id, request.Version, HttpContext.RequestAborted); asset = await assetQuery.FindAsync(context, asset.Id, false, request.Version, HttpContext.RequestAborted);
} }
else else
{ {

6
backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetContentQueryDto.cs

@ -93,6 +93,12 @@ public sealed class AssetContentQueryDto
[FromQuery(Name = "force")] [FromQuery(Name = "force")]
public bool Force { get; set; } public bool Force { get; set; }
/// <summary>
/// Also return deleted content items.
/// </summary>
[FromQuery(Name = "deleted")]
public bool Deleted { get; set; }
/// <summary> /// <summary>
/// True to force a new resize even if it already stored. /// True to force a new resize even if it already stored.
/// </summary> /// </summary>

6
backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsFluidExtensionTests.cs

@ -321,7 +321,7 @@ public class AssetsFluidExtensionTests : GivenContext
AppId = AppId AppId = AppId
}; };
A.CallTo(() => assetQuery.FindAsync(A<Context>._, assetId, EtagVersion.Any, A<CancellationToken>._)) A.CallTo(() => assetQuery.FindAsync(A<Context>._, assetId, false, EtagVersion.Any, A<CancellationToken>._))
.Returns(asset); .Returns(asset);
var vars = new TemplateVars var vars = new TemplateVars
@ -349,10 +349,10 @@ public class AssetsFluidExtensionTests : GivenContext
AppId = AppId AppId = AppId
}; };
A.CallTo(() => assetQuery.FindAsync(A<Context>._, assetId1, EtagVersion.Any, A<CancellationToken>._)) A.CallTo(() => assetQuery.FindAsync(A<Context>._, assetId1, false, EtagVersion.Any, A<CancellationToken>._))
.Returns(asset1); .Returns(asset1);
A.CallTo(() => assetQuery.FindAsync(A<Context>._, assetId2, EtagVersion.Any, A<CancellationToken>._)) A.CallTo(() => assetQuery.FindAsync(A<Context>._, assetId2, false, EtagVersion.Any, A<CancellationToken>._))
.Returns(asset2); .Returns(asset2);
var vars = new TemplateVars var vars = new TemplateVars

2
backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetsQueryIntegrationTests.cs

@ -48,7 +48,7 @@ public class AssetsQueryIntegrationTests : IClassFixture<AssetsQueryFixture>, IA
{ {
var random = _.RandomValue(); var random = _.RandomValue();
var asset = await _.AssetRepository.FindAssetBySlugAsync(_.RandomAppId(), random); var asset = await _.AssetRepository.FindAssetBySlugAsync(_.RandomAppId(), random, false);
// The Slug is random here, as it does not really matter. // The Slug is random here, as it does not really matter.
Assert.NotNull(asset); Assert.NotNull(asset);

16
backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryServiceTests.cs

@ -45,10 +45,10 @@ public class AssetQueryServiceTests : GivenContext
{ {
var asset = CreateAsset(DomainId.NewGuid()); var asset = CreateAsset(DomainId.NewGuid());
A.CallTo(() => assetRepository.FindAssetBySlugAsync(AppId.Id, "slug", A<CancellationToken>._)) A.CallTo(() => assetRepository.FindAssetBySlugAsync(AppId.Id, "slug", true, A<CancellationToken>._))
.Returns(asset); .Returns(asset);
var actual = await sut.FindBySlugAsync(ApiContext, "slug", CancellationToken); var actual = await sut.FindBySlugAsync(ApiContext, "slug", true, CancellationToken);
AssertAsset(asset, actual); AssertAsset(asset, actual);
} }
@ -58,10 +58,10 @@ public class AssetQueryServiceTests : GivenContext
{ {
var asset = CreateAsset(DomainId.NewGuid()); var asset = CreateAsset(DomainId.NewGuid());
A.CallTo(() => assetRepository.FindAssetBySlugAsync(AppId.Id, "slug", A<CancellationToken>._)) A.CallTo(() => assetRepository.FindAssetBySlugAsync(AppId.Id, "slug", false, A<CancellationToken>._))
.Returns(Task.FromResult<IAssetEntity?>(null)); .Returns(Task.FromResult<IAssetEntity?>(null));
var actual = await sut.FindBySlugAsync(ApiContext, "slug", CancellationToken); var actual = await sut.FindBySlugAsync(ApiContext, "slug", false, CancellationToken);
Assert.Null(actual); Assert.Null(actual);
} }
@ -71,7 +71,7 @@ public class AssetQueryServiceTests : GivenContext
{ {
var asset = CreateAsset(DomainId.NewGuid()); var asset = CreateAsset(DomainId.NewGuid());
A.CallTo(() => assetRepository.FindAssetAsync(AppId.Id, asset.Id, A<CancellationToken>._)) A.CallTo(() => assetRepository.FindAssetAsync(AppId.Id, asset.Id, false, A<CancellationToken>._))
.Returns(asset); .Returns(asset);
var actual = await sut.FindAsync(ApiContext, asset.Id, ct: CancellationToken); var actual = await sut.FindAsync(ApiContext, asset.Id, ct: CancellationToken);
@ -84,7 +84,7 @@ public class AssetQueryServiceTests : GivenContext
{ {
var asset = CreateAsset(DomainId.NewGuid()); var asset = CreateAsset(DomainId.NewGuid());
A.CallTo(() => assetRepository.FindAssetAsync(AppId.Id, asset.Id, A<CancellationToken>._)) A.CallTo(() => assetRepository.FindAssetAsync(AppId.Id, asset.Id, false, A<CancellationToken>._))
.Returns(Task.FromResult<IAssetEntity?>(null)); .Returns(Task.FromResult<IAssetEntity?>(null));
var actual = await sut.FindAsync(ApiContext, asset.Id, ct: CancellationToken); var actual = await sut.FindAsync(ApiContext, asset.Id, ct: CancellationToken);
@ -100,7 +100,7 @@ public class AssetQueryServiceTests : GivenContext
A.CallTo(() => assetLoader.GetAsync(AppId.Id, asset.Id, 2, A<CancellationToken>._)) A.CallTo(() => assetLoader.GetAsync(AppId.Id, asset.Id, 2, A<CancellationToken>._))
.Returns(asset); .Returns(asset);
var actual = await sut.FindAsync(ApiContext, asset.Id, 2, CancellationToken); var actual = await sut.FindAsync(ApiContext, asset.Id, false, 2, CancellationToken);
AssertAsset(asset, actual); AssertAsset(asset, actual);
} }
@ -113,7 +113,7 @@ public class AssetQueryServiceTests : GivenContext
A.CallTo(() => assetLoader.GetAsync(AppId.Id, asset.Id, 2, A<CancellationToken>._)) A.CallTo(() => assetLoader.GetAsync(AppId.Id, asset.Id, 2, A<CancellationToken>._))
.Returns(Task.FromResult<IAssetEntity?>(null)); .Returns(Task.FromResult<IAssetEntity?>(null));
var actual = await sut.FindAsync(ApiContext, asset.Id, 2, CancellationToken); var actual = await sut.FindAsync(ApiContext, asset.Id, false, 2, CancellationToken);
Assert.Null(actual); Assert.Null(actual);
} }

65
backend/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectTests.cs

@ -24,7 +24,6 @@ public class DomainObjectTests
ct = cts.Token; ct = cts.Token;
state = new TestState<MyDomainState>(id); state = new TestState<MyDomainState>(id);
sut = new MyDomainObject(id, state.PersistenceFactory); sut = new MyDomainObject(id, state.PersistenceFactory);
} }
@ -136,7 +135,7 @@ public class DomainObjectTests
[Fact] [Fact]
public async Task Should_throw_exception_if_writing_causes_inconsistent_state_exception() public async Task Should_throw_exception_if_writing_causes_inconsistent_state_exception()
{ {
sut.Recreate = false; sut.RecreateCommand = false;
SetupCreated(2); SetupCreated(2);
@ -149,7 +148,7 @@ public class DomainObjectTests
[Fact] [Fact]
public async Task Should_throw_exception_if_writing_causes_inconsistent_state_exception_and_deleted() public async Task Should_throw_exception_if_writing_causes_inconsistent_state_exception_and_deleted()
{ {
sut.Recreate = false; sut.RecreateCommand = false;
SetupCreated(2); SetupCreated(2);
SetupDeleted(); SetupDeleted();
@ -163,7 +162,7 @@ public class DomainObjectTests
[Fact] [Fact]
public async Task Should_recreate_with_create_command_if_deleted_before() public async Task Should_recreate_with_create_command_if_deleted_before()
{ {
sut.Recreate = true; sut.RecreateCommand = true;
sut.RecreateEvent = true; sut.RecreateEvent = true;
SetupCreated(2); SetupCreated(2);
@ -175,7 +174,7 @@ public class DomainObjectTests
var actual = await sut.ExecuteAsync(new CreateAuto { Value = 4 }, ct); var actual = await sut.ExecuteAsync(new CreateAuto { Value = 4 }, ct);
A.CallTo(() => state.Persistence.WriteEventsAsync(A<IReadOnlyList<Envelope<IEvent>>>.That.Matches(x => x.Count == 1), ct)) A.CallTo(() => state.Persistence.WriteEventsAsync(A<IReadOnlyList<Envelope<IEvent>>>.That.Matches(x => x.Count == 1), ct))
.MustHaveHappenedANumberOfTimesMatching(x => x == 3); .MustHaveHappenedANumberOfTimesMatching(x => x == 2);
A.CallTo(() => state.Persistence.ReadAsync(A<long>._, ct)) A.CallTo(() => state.Persistence.ReadAsync(A<long>._, ct))
.MustHaveHappened(); .MustHaveHappened();
@ -191,7 +190,7 @@ public class DomainObjectTests
[Fact] [Fact]
public async Task Should_throw_exception_if_recreation_with_create_command_is_not_allowed() public async Task Should_throw_exception_if_recreation_with_create_command_is_not_allowed()
{ {
sut.Recreate = false; sut.RecreateCommand = false;
SetupCreated(2); SetupCreated(2);
SetupDeleted(); SetupDeleted();
@ -199,13 +198,13 @@ public class DomainObjectTests
A.CallTo(() => state.Persistence.WriteEventsAsync(A<IReadOnlyList<Envelope<IEvent>>>._, ct)) A.CallTo(() => state.Persistence.WriteEventsAsync(A<IReadOnlyList<Envelope<IEvent>>>._, ct))
.Throws(new InconsistentStateException(2, -1)).Once(); .Throws(new InconsistentStateException(2, -1)).Once();
await Assert.ThrowsAsync<DomainObjectConflictException>(() => sut.ExecuteAsync(new CreateAuto(), ct)); await Assert.ThrowsAsync<DomainObjectDeletedException>(() => sut.ExecuteAsync(new CreateAuto(), ct));
} }
[Fact] [Fact]
public async Task Should_recreate_with_upsert_command_if_deleted_before() public async Task Should_recreate_with_upsert_command_if_deleted_before()
{ {
sut.Recreate = true; sut.RecreateCommand = true;
sut.RecreateEvent = true; sut.RecreateEvent = true;
SetupCreated(2); SetupCreated(2);
@ -217,7 +216,7 @@ public class DomainObjectTests
var actual = await sut.ExecuteAsync(new Upsert { Value = 4 }, ct); var actual = await sut.ExecuteAsync(new Upsert { Value = 4 }, ct);
A.CallTo(() => state.Persistence.WriteEventsAsync(A<IReadOnlyList<Envelope<IEvent>>>.That.Matches(x => x.Count == 1), ct)) A.CallTo(() => state.Persistence.WriteEventsAsync(A<IReadOnlyList<Envelope<IEvent>>>.That.Matches(x => x.Count == 1), ct))
.MustHaveHappenedANumberOfTimesMatching(x => x == 3); .MustHaveHappenedANumberOfTimesMatching(x => x == 2);
A.CallTo(() => state.Persistence.ReadAsync(A<long>._, ct)) A.CallTo(() => state.Persistence.ReadAsync(A<long>._, ct))
.MustHaveHappened(); .MustHaveHappened();
@ -233,7 +232,7 @@ public class DomainObjectTests
[Fact] [Fact]
public async Task Should_throw_exception_if_recreation_with_upsert_command_is_not_allowed() public async Task Should_throw_exception_if_recreation_with_upsert_command_is_not_allowed()
{ {
sut.Recreate = false; sut.RecreateCommand = false;
SetupCreated(2); SetupCreated(2);
SetupDeleted(); SetupDeleted();
@ -337,10 +336,14 @@ public class DomainObjectTests
} }
[Fact] [Fact]
public async Task Should_throw_exception_on_create_command_is_rejected_due_to_version_conflict() public async Task Should_throw_exception_if_create_command_is_rejected_due_to_version_conflict()
{ {
A.CallTo(() => state.Persistence.WriteEventsAsync(A<IReadOnlyList<Envelope<IEvent>>>._, ct)) A.CallTo(() => state.Persistence.WriteEventsAsync(A<IReadOnlyList<Envelope<IEvent>>>._, ct))
.Throws(new InconsistentStateException(4, EtagVersion.Empty)); .Invokes(() =>
{
SetupCreated(4);
throw new InconsistentStateException(4, EtagVersion.Empty);
});
await Assert.ThrowsAsync<DomainObjectConflictException>(() => sut.ExecuteAsync(new CreateAuto(), ct)); await Assert.ThrowsAsync<DomainObjectConflictException>(() => sut.ExecuteAsync(new CreateAuto(), ct));
} }
@ -353,12 +356,6 @@ public class DomainObjectTests
await Assert.ThrowsAsync<DomainObjectConflictException>(() => sut.ExecuteAsync(new CreateAuto(), ct)); await Assert.ThrowsAsync<DomainObjectConflictException>(() => sut.ExecuteAsync(new CreateAuto(), ct));
} }
[Fact]
public async Task Should_throw_exception_if_create_command_not_accepted()
{
await Assert.ThrowsAsync<DomainException>(() => sut.ExecuteAsync(new CreateAuto { Value = 99 }, ct));
}
[Fact] [Fact]
public async Task Should_return_custom_actual_on_create() public async Task Should_return_custom_actual_on_create()
{ {
@ -441,14 +438,38 @@ public class DomainObjectTests
} }
[Fact] [Fact]
public async Task Should_write_events_to_delete_stream_on_delete() public async Task Should_write_events_to_delete_stream_on_permanent_delete()
{
SetupCreated(4);
var deleteStream = A.Fake<IPersistence<MyDomainState>>();
A.CallTo(() => state.PersistenceFactory.WithSnapshots(typeof(MyDomainObject), DomainId.Combine(id, DomainId.Create("deleted")), null))
.Returns(deleteStream);
await sut.ExecuteAsync(new DeletePermanent(), ct);
AssertSnapshot(sut.Snapshot, 0, EtagVersion.Empty, false);
A.CallTo(() => state.Persistence.DeleteAsync(ct))
.MustHaveHappened();
A.CallTo(() => state.Persistence.WriteEventsAsync(A<IReadOnlyList<Envelope<IEvent>>>._, ct))
.MustNotHaveHappened();
A.CallTo(() => deleteStream.WriteEventsAsync(A<IReadOnlyList<Envelope<IEvent>>>._, A<CancellationToken>._))
.MustHaveHappened();
}
[Fact]
public async Task Should_not_write_events_to_delete_stream_on_permanent_delete_if_already_deleted()
{ {
SetupCreated(4); SetupCreated(4);
SetupDeleted(); SetupDeleted();
var deleteStream = A.Fake<IPersistence<MyDomainState>>(); var deleteStream = A.Fake<IPersistence<MyDomainState>>();
A.CallTo(() => state.PersistenceFactory.WithEventSourcing(typeof(MyDomainObject), DomainId.Combine(id, DomainId.Create("deleted")), null)) A.CallTo(() => state.PersistenceFactory.WithSnapshots(typeof(MyDomainObject), DomainId.Combine(id, DomainId.Create("deleted")), null))
.Returns(deleteStream); .Returns(deleteStream);
await sut.ExecuteAsync(new DeletePermanent(), ct); await sut.ExecuteAsync(new DeletePermanent(), ct);
@ -459,7 +480,7 @@ public class DomainObjectTests
.MustHaveHappened(); .MustHaveHappened();
A.CallTo(() => state.Persistence.WriteEventsAsync(A<IReadOnlyList<Envelope<IEvent>>>._, ct)) A.CallTo(() => state.Persistence.WriteEventsAsync(A<IReadOnlyList<Envelope<IEvent>>>._, ct))
.MustHaveHappenedOnceExactly(); .MustNotHaveHappened();
A.CallTo(() => deleteStream.WriteEventsAsync(A<IReadOnlyList<Envelope<IEvent>>>._, A<CancellationToken>._)) A.CallTo(() => deleteStream.WriteEventsAsync(A<IReadOnlyList<Envelope<IEvent>>>._, A<CancellationToken>._))
.MustNotHaveHappened(); .MustNotHaveHappened();
@ -494,7 +515,7 @@ public class DomainObjectTests
private void SetupDeleted() private void SetupDeleted()
{ {
sut.ExecuteAsync(new Delete(), ct).Wait(ct); state.AddEvent(new Deleted());
} }
private void SetupCreated(int value) private void SetupCreated(int value)

62
backend/tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainObject.cs

@ -16,7 +16,7 @@ namespace Squidex.Infrastructure.TestHelpers;
public sealed class MyDomainObject : DomainObject<MyDomainState> public sealed class MyDomainObject : DomainObject<MyDomainState>
{ {
public bool Recreate { get; set; } public bool RecreateCommand { get; set; }
public bool RecreateEvent { get; set; } public bool RecreateEvent { get; set; }
@ -25,28 +25,23 @@ public sealed class MyDomainObject : DomainObject<MyDomainState>
{ {
} }
protected override bool CanRecreate(IEvent @event) protected override bool IsRecreation(IEvent @event)
{ {
return RecreateEvent && @event is ValueChanged; return RecreateEvent && @event is ValueChanged;
} }
protected override bool CanRecreate() protected override bool IsDeleted(MyDomainState snapshot)
{ {
return Recreate; return snapshot.IsDeleted;
} }
protected override bool CanAcceptCreation(ICommand command) protected override bool CanAccept(ICommand command)
{ {
if (command is CreateAuto update) if (command is CreateAuto create)
{ {
return update.Value != 99; return create.Value != 99;
} }
return true;
}
protected override bool CanAccept(ICommand command)
{
if (command is UpdateAuto update) if (command is UpdateAuto update)
{ {
return update.Value != 99; return update.Value != 99;
@ -55,9 +50,36 @@ public sealed class MyDomainObject : DomainObject<MyDomainState>
return true; return true;
} }
protected override bool IsDeleted(MyDomainState snapshot) protected override bool CanAccept(ICommand command, DomainObjectState state)
{ {
return snapshot.IsDeleted; static bool CanCreate(ICommand command)
{
return
command is CreateAuto ||
command is CreateCustom ||
command is Upsert;
}
static bool CanUpdate(ICommand command)
{
return
command is UpdateAuto ||
command is UpdateCustom ||
command is Delete ||
command is DeletePermanent;
}
switch (state)
{
case DomainObjectState.Undefined:
return CanCreate(command);
case DomainObjectState.Empty:
return CanCreate(command);
case DomainObjectState.Deleted:
return (CanCreate(command) && RecreateCommand) || command is DeletePermanent;
default:
return CanUpdate(command);
}
} }
public override Task<CommandResult> ExecuteAsync(IAggregateCommand command, public override Task<CommandResult> ExecuteAsync(IAggregateCommand command,
@ -66,19 +88,19 @@ public sealed class MyDomainObject : DomainObject<MyDomainState>
switch (command) switch (command)
{ {
case Upsert c: case Upsert c:
return Upsert(c, createAuto => return Apply(c, createAuto =>
{ {
RaiseEvent(new ValueChanged { Value = createAuto.Value }); RaiseEvent(new ValueChanged { Value = createAuto.Value });
}, ct); }, ct);
case CreateAuto c: case CreateAuto c:
return Create(c, createAuto => return Apply(c, createAuto =>
{ {
RaiseEvent(new ValueChanged { Value = createAuto.Value }); RaiseEvent(new ValueChanged { Value = createAuto.Value });
}, ct); }, ct);
case CreateCustom c: case CreateCustom c:
return CreateReturn(c, createCustom => return ApplyReturn(c, createCustom =>
{ {
RaiseEvent(new ValueChanged { Value = createCustom.Value }); RaiseEvent(new ValueChanged { Value = createCustom.Value });
@ -86,13 +108,13 @@ public sealed class MyDomainObject : DomainObject<MyDomainState>
}, ct); }, ct);
case UpdateAuto c: case UpdateAuto c:
return Update(c, updateAuto => return Apply(c, updateAuto =>
{ {
RaiseEvent(new ValueChanged { Value = updateAuto.Value }); RaiseEvent(new ValueChanged { Value = updateAuto.Value });
}, ct); }, ct);
case UpdateCustom c: case UpdateCustom c:
return UpdateReturn(c, updateCustom => return ApplyReturn(c, updateCustom =>
{ {
RaiseEvent(new ValueChanged { Value = updateCustom.Value }); RaiseEvent(new ValueChanged { Value = updateCustom.Value });
@ -100,7 +122,7 @@ public sealed class MyDomainObject : DomainObject<MyDomainState>
}, ct); }, ct);
case Delete c: case Delete c:
return Update(c, delete => return Apply(c, delete =>
{ {
RaiseEvent(new Deleted()); RaiseEvent(new Deleted());
}, ct); }, ct);

158
tools/TestSuite/TestSuite.ApiTests/AssetTests.cs

@ -6,9 +6,9 @@
// ========================================================================== // ==========================================================================
using System.Net; using System.Net;
using Squidex.Assets;
using Squidex.ClientLibrary; using Squidex.ClientLibrary;
using TestSuite.Fixtures; using TestSuite.Fixtures;
using TestSuite.Utils;
#pragma warning disable SA1300 // Element should begin with upper-case letter #pragma warning disable SA1300 // Element should begin with upper-case letter
#pragma warning disable SA1507 // Code should not contain multiple blank lines in a row #pragma warning disable SA1507 // Code should not contain multiple blank lines in a row
@ -78,7 +78,7 @@ public class AssetTests : IClassFixture<CreatedAppFixture>
var fileParameter = FileParameter.FromPath("Assets/SampleVideo_1280x720_1mb.mp4"); var fileParameter = FileParameter.FromPath("Assets/SampleVideo_1280x720_1mb.mp4");
await UploadInChunksAsync(fileParameter); await _.Client.Assets.UploadInChunksAsync(progress, fileParameter);
Assert.Null(progress.Exception); Assert.Null(progress.Exception);
Assert.NotEmpty(progress.Progress); Assert.NotEmpty(progress.Progress);
@ -219,7 +219,7 @@ public class AssetTests : IClassFixture<CreatedAppFixture>
var fileParameter = FileParameter.FromPath("Assets/SampleVideo_1280x720_1mb.mp4"); var fileParameter = FileParameter.FromPath("Assets/SampleVideo_1280x720_1mb.mp4");
await UploadInChunksAsync(fileParameter, asset_1.Id); await _.Client.Assets.UploadInChunksAsync(progress, fileParameter, asset_1.Id);
Assert.Null(progress.Exception); Assert.Null(progress.Exception);
Assert.NotEmpty(progress.Progress); Assert.NotEmpty(progress.Progress);
@ -738,146 +738,48 @@ public class AssetTests : IClassFixture<CreatedAppFixture>
Assert.NotEqual(asset_1.FileSize, asset_2.FileSize); Assert.NotEqual(asset_1.FileSize, asset_2.FileSize);
} }
private async Task UploadInChunksAsync(FileParameter fileParameter, string id = null) [Fact]
{ public async Task Should_recover_deleted_asset()
var pausingStream = new PauseStream(fileParameter.Data, 0.25);
var pausingFile = new FileParameter(pausingStream, fileParameter.FileName, fileParameter.ContentType)
{
ContentLength = fileParameter.Data.Length
};
await using (pausingFile.Data)
{
using var cts = new CancellationTokenSource(5000);
while (progress.Asset == null && progress.Exception == null && !cts.IsCancellationRequested)
{
pausingStream.Reset();
await _.Client.Assets.UploadAssetAsync(pausingFile, progress.AsOptions(id), cts.Token);
progress.Uploaded();
}
}
}
public class ProgressHandler : IAssetProgressHandler
{ {
public string FileId { get; private set; } = Guid.NewGuid().ToString(); // STEP 0: Create app.
var (client, _) = await _.PostAppAsync();
public List<int> Progress { get; } = new List<int>();
public List<int> Uploads { get; } = new List<int>();
public Exception Exception { get; private set; }
public AssetDto Asset { get; private set; }
public AssetUploadOptions AsOptions(string id = null)
{
var options = default(AssetUploadOptions);
options.ProgressHandler = this;
options.FileId = FileId;
options.Id = id;
return options;
}
public void Uploaded()
{
Uploads.Add(Progress.LastOrDefault());
}
public Task OnCompletedAsync(AssetUploadCompletedEvent @event,
CancellationToken ct)
{
Asset = @event.Asset;
return Task.CompletedTask;
}
public Task OnCreatedAsync(AssetUploadCreatedEvent @event,
CancellationToken ct)
{
FileId = @event.FileId;
return Task.CompletedTask;
}
public Task OnProgressAsync(AssetUploadProgressEvent @event,
CancellationToken ct)
{
Progress.Add(@event.Progress);
return Task.CompletedTask;
}
public Task OnFailedAsync(AssetUploadExceptionEvent @event, // STEP 1: Create asset.
CancellationToken ct) var asset_1 = await client.Assets.UploadFileAsync("Assets/logo-squared.png", "image/png");
{
Exception = @event.Exception;
return Task.CompletedTask;
}
}
public class PauseStream : DelegateStream
{
private readonly int maxLength;
private long totalRead;
private long totalRemaining;
private long seekStart;
public override long Length // STEP 2: Delete asset.
{ await client.Assets.DeleteAssetAsync(asset_1.Id);
get => Math.Min(maxLength, totalRemaining);
}
public override long Position
{
get => base.Position - seekStart;
set => throw new NotSupportedException();
}
public PauseStream(Stream innerStream, double pauseAfter) // STEP 3: Query and recreate asset.
: base(innerStream) var assets = await client.Assets.GetAssetsAsync(new AssetQuery
{ {
maxLength = (int)Math.Floor(innerStream.Length * pauseAfter) + 1; Query = new
{
filter = new
{
path = "isDeleted",
op = "eq",
value = true,
}
}
});
totalRemaining = innerStream.Length; Assert.NotEmpty(assets.Items);
}
public override long Seek(long offset, SeekOrigin origin) foreach (var asset in assets.Items)
{ {
var position = seekStart = base.Seek(offset, origin); var content = await client.Assets.GetAssetContentBySlugAsync(asset.Id, string.Empty, deleted: true);
totalRemaining = base.Length - position;
return position;
}
public void Reset() await client.Assets.PostAssetAsync(id: asset.Id, file: new FileParameter(content.Stream, asset.FileName, asset.MimeType));
{
totalRead = 0;
} }
public override async ValueTask<int> ReadAsync(Memory<byte> buffer,
CancellationToken cancellationToken = default)
{
var remaining = Length - totalRead;
if (remaining <= 0) // STEP 4: Query recreated asset.
{ var asset_2 = await client.Assets.GetAssetAsync(asset_1.Id);
return 0;
}
if (remaining < buffer.Length) Assert.NotNull(asset_2);
{
var remainingBytes = (int)remaining;
buffer = buffer[..remainingBytes];
}
var bytesRead = await base.ReadAsync(buffer, cancellationToken);
totalRead += bytesRead;
return bytesRead;
}
} }
} }

2
tools/TestSuite/TestSuite.ApiTests/TestSuite.ApiTests.csproj

@ -25,7 +25,7 @@
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
<PackageReference Include="NSwag.Core" Version="13.18.2" /> <PackageReference Include="NSwag.Core" Version="13.18.2" />
<PackageReference Include="PuppeteerSharp" Version="9.0.2" /> <PackageReference Include="PuppeteerSharp" Version="9.0.2" />
<PackageReference Include="Squidex.Assets" Version="5.5.0" /> <PackageReference Include="Squidex.Assets" Version="5.16.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="Verify.Xunit" Version="19.11.2" /> <PackageReference Include="Verify.Xunit" Version="19.11.2" />
<PackageReference Include="xunit" Version="2.4.2" /> <PackageReference Include="xunit" Version="2.4.2" />

39
tools/TestSuite/TestSuite.Shared/ClientExtensions.cs

@ -7,6 +7,7 @@
using Squidex.ClientLibrary; using Squidex.ClientLibrary;
using TestSuite.Fixtures; using TestSuite.Fixtures;
using TestSuite.Utils;
namespace TestSuite; namespace TestSuite;
@ -309,7 +310,29 @@ public static class ClientExtensions
return temp; return temp;
} }
public static async Task<AssetDto> UploadFileAsync(this IAssetsClient assetsClients, string path, AssetDto asset, string fileName = null) public static async Task UploadInChunksAsync(this IAssetsClient client, ProgressHandler progress, FileParameter fileParameter, string id = null)
{
var pausingStream = new PauseStream(fileParameter.Data, 0.25);
var pausingFile = new FileParameter(pausingStream, fileParameter.FileName, fileParameter.ContentType)
{
ContentLength = fileParameter.Data.Length
};
await using (pausingFile.Data)
{
using var cts = new CancellationTokenSource(5000);
while (progress.Asset == null && progress.Exception == null && !cts.IsCancellationRequested)
{
pausingStream.Reset();
await client.UploadAssetAsync(pausingFile, progress.AsOptions(id), cts.Token);
progress.Uploaded();
}
}
}
public static async Task<AssetDto> UploadFileAsync(this IAssetsClient client, string path, AssetDto asset, string fileName = null)
{ {
var fileInfo = new FileInfo(path); var fileInfo = new FileInfo(path);
@ -317,11 +340,11 @@ public static class ClientExtensions
{ {
var upload = new FileParameter(stream, fileName ?? fileInfo.Name, asset.MimeType); var upload = new FileParameter(stream, fileName ?? fileInfo.Name, asset.MimeType);
return await assetsClients.PutAssetContentAsync(asset.Id, upload); return await client.PutAssetContentAsync(asset.Id, upload);
} }
} }
public static async Task<AssetDto> UploadFileAsync(this IAssetsClient assetsClients, string path, string fileType, string fileName = null, string parentId = null, string id = null) public static async Task<AssetDto> UploadFileAsync(this IAssetsClient client, string path, string fileType, string fileName = null, string parentId = null, string id = null)
{ {
var fileInfo = new FileInfo(path); var fileInfo = new FileInfo(path);
@ -329,11 +352,11 @@ public static class ClientExtensions
{ {
var upload = new FileParameter(stream, fileName ?? fileInfo.Name, fileType); var upload = new FileParameter(stream, fileName ?? fileInfo.Name, fileType);
return await assetsClients.PostAssetAsync(parentId, id, true, upload); return await client.PostAssetAsync(parentId, id, true, upload);
} }
} }
public static async Task<AssetDto> UpdateFileAsync(this IAssetsClient assetsClients, string id, string path, string fileType, string fileName = null) public static async Task<AssetDto> UpdateFileAsync(this IAssetsClient client, string id, string path, string fileType, string fileName = null)
{ {
var fileInfo = new FileInfo(path); var fileInfo = new FileInfo(path);
@ -341,17 +364,17 @@ public static class ClientExtensions
{ {
var upload = new FileParameter(stream, fileName ?? fileInfo.Name, fileType); var upload = new FileParameter(stream, fileName ?? fileInfo.Name, fileType);
return await assetsClients.PutAssetContentAsync(id, upload); return await client.PutAssetContentAsync(id, upload);
} }
} }
public static async Task<AssetDto> UploadRandomFileAsync(this IAssetsClient assetsClients, int size, string parentId = null, string id = null) public static async Task<AssetDto> UploadRandomFileAsync(this IAssetsClient client, int size, string parentId = null, string id = null)
{ {
using (var stream = RandomAsset(size)) using (var stream = RandomAsset(size))
{ {
var upload = new FileParameter(stream, RandomName(".txt"), "text/csv"); var upload = new FileParameter(stream, RandomName(".txt"), "text/csv");
return await assetsClients.PostAssetAsync(parentId, id, true, upload); return await client.PostAssetAsync(parentId, id, true, upload);
} }
} }

5
tools/TestSuite/TestSuite.Shared/TestSuite.Shared.csproj

@ -16,8 +16,9 @@
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="7.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" /> <PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="Squidex.ClientLibrary" Version="16.0.0" /> <PackageReference Include="Squidex.Assets" Version="5.16.0" />
<PackageReference Include="Squidex.ClientLibrary.ServiceExtensions" Version="16.0.0" /> <PackageReference Include="Squidex.ClientLibrary" Version="16.1.0" />
<PackageReference Include="Squidex.ClientLibrary.ServiceExtensions" Version="16.1.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="Verify" Version="19.11.2" /> <PackageReference Include="Verify" Version="19.11.2" />
<PackageReference Include="xunit" Version="2.4.2" /> <PackageReference Include="xunit" Version="2.4.2" />

75
tools/TestSuite/TestSuite.Shared/Utils/PauseStream.cs

@ -0,0 +1,75 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Assets;
namespace TestSuite.Utils;
public sealed class PauseStream : DelegateStream
{
private readonly int maxLength;
private long totalRead;
private long totalRemaining;
private long seekStart;
public override long Length
{
get => Math.Min(maxLength, totalRemaining);
}
public override long Position
{
get => base.Position - seekStart;
set => throw new NotSupportedException();
}
public PauseStream(Stream innerStream, double pauseAfter)
: base(innerStream)
{
maxLength = (int)Math.Floor(innerStream.Length * pauseAfter) + 1;
totalRemaining = innerStream.Length;
}
public override long Seek(long offset, SeekOrigin origin)
{
var position = seekStart = base.Seek(offset, origin);
totalRemaining = base.Length - position;
return position;
}
public void Reset()
{
totalRead = 0;
}
public override async ValueTask<int> ReadAsync(Memory<byte> buffer,
CancellationToken cancellationToken = default)
{
var remaining = Length - totalRead;
if (remaining <= 0)
{
return 0;
}
if (remaining < buffer.Length)
{
var remainingBytes = (int)remaining;
buffer = buffer[..remainingBytes];
}
var bytesRead = await base.ReadAsync(buffer, cancellationToken);
totalRead += bytesRead;
return bytesRead;
}
}

66
tools/TestSuite/TestSuite.Shared/Utils/ProgressHandler.cs

@ -0,0 +1,66 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.ClientLibrary;
namespace TestSuite.Utils;
public sealed class ProgressHandler : IAssetProgressHandler
{
public string FileId { get; private set; } = Guid.NewGuid().ToString();
public List<int> Progress { get; } = new List<int>();
public List<int> Uploads { get; } = new List<int>();
public Exception Exception { get; private set; }
public AssetDto Asset { get; private set; }
public AssetUploadOptions AsOptions(string id = null)
{
var options = default(AssetUploadOptions);
options.ProgressHandler = this;
options.FileId = FileId;
options.Id = id;
return options;
}
public void Uploaded()
{
Uploads.Add(Progress.LastOrDefault());
}
public Task OnCompletedAsync(AssetUploadCompletedEvent @event,
CancellationToken ct)
{
Asset = @event.Asset;
return Task.CompletedTask;
}
public Task OnCreatedAsync(AssetUploadCreatedEvent @event,
CancellationToken ct)
{
FileId = @event.FileId;
return Task.CompletedTask;
}
public Task OnProgressAsync(AssetUploadProgressEvent @event,
CancellationToken ct)
{
Progress.Add(@event.Progress);
return Task.CompletedTask;
}
public Task OnFailedAsync(AssetUploadExceptionEvent @event,
CancellationToken ct)
{
Exception = @event.Exception;
return Task.CompletedTask;
}
}
Loading…
Cancel
Save