Browse Source

Query improvements (#477)

* Simplify the query code.
* Custom deep equals.
* Cleanup.
* Json fix.
* Equal fixed for custom equality comparer.
* Better equals solution.
* Fixes
pull/479/head
Sebastian Stehle 7 years ago
committed by GitHub
parent
commit
8e2f3e9d7e
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClient.cs
  2. 4
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClients.cs
  3. 1
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppImage.cs
  4. 1
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPattern.cs
  5. 2
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPatterns.cs
  6. 1
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPlan.cs
  7. 1
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/LanguageConfig.cs
  8. 9
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs
  9. 2
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/Role.cs
  10. 2
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/Roles.cs
  11. 15
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/JsonWorkflowTransition.cs
  12. 19
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/NoUpdate.cs
  13. 8
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/Status.cs
  14. 17
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/Workflow.cs
  15. 15
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowCondition.cs
  16. 17
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowStep.cs
  17. 19
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowTransition.cs
  18. 6
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/Workflows.cs
  19. 2
      backend/src/Squidex.Domain.Apps.Core.Model/FodyWeavers.xml
  20. 1
      backend/src/Squidex.Domain.Apps.Core.Model/FodyWeavers.xsd
  21. 4
      backend/src/Squidex.Domain.Apps.Core.Model/Freezable.cs
  22. 4
      backend/src/Squidex.Domain.Apps.Core.Model/Rules/RuleAction.cs
  23. 4
      backend/src/Squidex.Domain.Apps.Core.Model/Rules/RuleTrigger.cs
  24. 1
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayFieldProperties.cs
  25. 1
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/AssetsFieldProperties.cs
  26. 1
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeFieldProperties.cs
  27. 6
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldNames.cs
  28. 7
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldProperties.cs
  29. 1
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationFieldProperties.cs
  30. 1
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/JsonFieldProperties.cs
  31. 2
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NestedField{T}.cs
  32. 1
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldProperties.cs
  33. 1
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldProperties.cs
  34. 2
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/RootField{T}.cs
  35. 6
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Schema.cs
  36. 5
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaProperties.cs
  37. 8
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaScripts.cs
  38. 1
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldProperties.cs
  39. 1
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/UIFieldProperties.cs
  40. 8
      backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj
  41. 4
      backend/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizer.cs
  42. 7
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs
  43. 15
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FindExtensions.cs
  44. 270
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs
  45. 86
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs
  46. 4
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_EventHandling.cs
  47. 30
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs
  48. 39
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/Adapt.cs
  49. 2
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/AdaptionVisitor.cs
  50. 35
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/CleanupReferences.cs
  51. 12
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/Extensions.cs
  52. 39
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/OperationBase.cs
  53. 49
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryContent.cs
  54. 128
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryContentsByIds.cs
  55. 159
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryContentsByQuery.cs
  56. 88
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryIdsAsync.cs
  57. 44
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryScheduledContents.cs
  58. 134
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FilterFactory.cs
  59. 51
      backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs
  60. 5
      backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/LimitExtensions.cs
  61. 8
      backend/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs
  62. 4
      backend/src/Squidex.Infrastructure/CollectionExtensions.cs
  63. 4
      backend/src/Squidex.Infrastructure/Collections/ImmutableDictionary{TKey,TValue}.cs
  64. 3
      backend/src/Squidex.Infrastructure/FodyWeavers.xml
  65. 26
      backend/src/Squidex.Infrastructure/FodyWeavers.xsd
  66. 3
      backend/src/Squidex.Infrastructure/Guard.cs
  67. 2
      backend/src/Squidex.Infrastructure/Language.cs
  68. 18
      backend/src/Squidex.Infrastructure/NamedId{T}.cs
  69. 15
      backend/src/Squidex.Infrastructure/RefToken.cs
  70. 40
      backend/src/Squidex.Infrastructure/Reflection/Equality/ArrayComparer.cs
  71. 69
      backend/src/Squidex.Infrastructure/Reflection/Equality/CollectionComparer.cs
  72. 13
      backend/src/Squidex.Infrastructure/Reflection/Equality/DeepEqualityComparer.cs
  73. 36
      backend/src/Squidex.Infrastructure/Reflection/Equality/DefaultComparer.cs
  74. 37
      backend/src/Squidex.Infrastructure/Reflection/Equality/DictionaryComparer{TKey,TValue}.cs
  75. 14
      backend/src/Squidex.Infrastructure/Reflection/Equality/IDeepComparer.cs
  76. 17
      backend/src/Squidex.Infrastructure/Reflection/Equality/NoopComparer.cs
  77. 50
      backend/src/Squidex.Infrastructure/Reflection/Equality/ObjectComparer.cs
  78. 35
      backend/src/Squidex.Infrastructure/Reflection/Equality/SetComparer.cs
  79. 10
      backend/src/Squidex.Infrastructure/Reflection/IgnoreEqualsAttribute.cs
  80. 15
      backend/src/Squidex.Infrastructure/Reflection/Internal/PropertyAccessor.cs
  81. 0
      backend/src/Squidex.Infrastructure/Reflection/Internal/ReflectionExtensions.cs
  82. 78
      backend/src/Squidex.Infrastructure/Reflection/PropertiesTypeAccessor.cs
  83. 84
      backend/src/Squidex.Infrastructure/Reflection/SimpleCopier.cs
  84. 121
      backend/src/Squidex.Infrastructure/Reflection/SimpleEquals.cs
  85. 15
      backend/src/Squidex.Infrastructure/Reflection/SimpleMapper.cs
  86. 4
      backend/src/Squidex.Infrastructure/Security/Permission.cs
  87. 5
      backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj
  88. 2
      backend/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowTransitionDto.cs
  89. 2
      backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RoleTests.cs
  90. 14
      backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowsJsonTests.cs
  91. 2
      backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaTests.cs
  92. 56
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppDomainObjectTests.cs
  93. 18
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetDomainObjectTests.cs
  94. 8
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetFolderDomainObjectTests.cs
  95. 6
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/MongoDbQueryTests.cs
  96. 6
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/CommentsGrainTests.cs
  97. 34
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentDomainObjectTests.cs
  98. 9
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/MongoDbQueryTests.cs
  99. 10
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDomainObjectTests.cs
  100. 60
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaDomainObjectTests.cs

1
backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClient.cs

@ -10,6 +10,7 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Apps namespace Squidex.Domain.Apps.Core.Apps
{ {
[Equals(DoNotAddEqualityOperators = true)]
public sealed class AppClient : Named public sealed class AppClient : Named
{ {
public string Role { get; } public string Role { get; }

4
backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClients.cs

@ -71,7 +71,7 @@ namespace Squidex.Domain.Apps.Core.Apps
return this; return this;
} }
return With<AppClients>(id, client.Rename(newName), DeepComparer<AppClient>.Instance); return With<AppClients>(id, client.Rename(newName));
} }
[Pure] [Pure]
@ -84,7 +84,7 @@ namespace Squidex.Domain.Apps.Core.Apps
return this; return this;
} }
return With<AppClients>(id, client.Update(role), DeepComparer<AppClient>.Instance); return With<AppClients>(id, client.Update(role));
} }
} }
} }

1
backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppImage.cs

@ -9,6 +9,7 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Apps namespace Squidex.Domain.Apps.Core.Apps
{ {
[Equals(DoNotAddEqualityOperators = true)]
public sealed class AppImage public sealed class AppImage
{ {
public string MimeType { get; } public string MimeType { get; }

1
backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPattern.cs

@ -10,6 +10,7 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Apps namespace Squidex.Domain.Apps.Core.Apps
{ {
[Equals(DoNotAddEqualityOperators = true)]
public sealed class AppPattern : Named public sealed class AppPattern : Named
{ {
public string Pattern { get; } public string Pattern { get; }

2
backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPatterns.cs

@ -56,7 +56,7 @@ namespace Squidex.Domain.Apps.Core.Apps
return this; return this;
} }
return With<AppPatterns>(id, appPattern.Update(name, pattern, message), DeepComparer<AppPattern>.Instance); return With<AppPatterns>(id, appPattern.Update(name, pattern, message));
} }
} }
} }

1
backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPlan.cs

@ -9,6 +9,7 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Apps namespace Squidex.Domain.Apps.Core.Apps
{ {
[Equals(DoNotAddEqualityOperators = true)]
public sealed class AppPlan public sealed class AppPlan
{ {
public RefToken Owner { get; } public RefToken Owner { get; }

1
backend/src/Squidex.Domain.Apps.Core.Model/Apps/LanguageConfig.cs

@ -12,6 +12,7 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Apps namespace Squidex.Domain.Apps.Core.Apps
{ {
[Equals(DoNotAddEqualityOperators = true)]
public sealed class LanguageConfig public sealed class LanguageConfig
{ {
public static readonly LanguageConfig Default = new LanguageConfig(); public static readonly LanguageConfig Default = new LanguageConfig();

9
backend/src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs

@ -62,7 +62,7 @@ namespace Squidex.Domain.Apps.Core.Apps
} }
[Pure] [Pure]
public LanguagesConfig Set(Language language, bool isOptional = false, params Language[] fallbacks) public LanguagesConfig Set(Language language, bool isOptional = false, params Language[]? fallbacks)
{ {
Guard.NotNull(language); Guard.NotNull(language);
@ -94,7 +94,7 @@ namespace Squidex.Domain.Apps.Core.Apps
Cleanup(newLanguages, ref newMaster); Cleanup(newLanguages, ref newMaster);
if (newLanguages.EqualsDictionary(languages, EqualityComparer<string>.Default, DeepComparer<LanguageConfig>.Instance) && Equals(newMaster, master)) if (EqualLanguages(newLanguages) && Equals(newMaster, master))
{ {
return this; return this;
} }
@ -102,6 +102,11 @@ namespace Squidex.Domain.Apps.Core.Apps
return new LanguagesConfig(newLanguages, newMaster); return new LanguagesConfig(newLanguages, newMaster);
} }
private bool EqualLanguages(Dictionary<string, LanguageConfig> newLanguages)
{
return newLanguages.EqualsDictionary(languages);
}
private void Cleanup(Dictionary<string, LanguageConfig> newLanguages, ref string newMaster) private void Cleanup(Dictionary<string, LanguageConfig> newLanguages, ref string newMaster)
{ {
if (!newLanguages.ContainsKey(newMaster)) if (!newLanguages.ContainsKey(newMaster))

2
backend/src/Squidex.Domain.Apps.Core.Model/Apps/Role.cs

@ -15,6 +15,7 @@ using AllPermissions = Squidex.Shared.Permissions;
namespace Squidex.Domain.Apps.Core.Apps namespace Squidex.Domain.Apps.Core.Apps
{ {
[Equals(DoNotAddEqualityOperators = true)]
public sealed class Role : Named public sealed class Role : Named
{ {
public const string Editor = "Editor"; public const string Editor = "Editor";
@ -24,6 +25,7 @@ namespace Squidex.Domain.Apps.Core.Apps
public PermissionSet Permissions { get; } public PermissionSet Permissions { get; }
[IgnoreDuringEquals]
public bool IsDefault public bool IsDefault
{ {
get { return Roles.IsDefault(this); } get { return Roles.IsDefault(this); }

2
backend/src/Squidex.Domain.Apps.Core.Model/Apps/Roles.cs

@ -115,7 +115,7 @@ namespace Squidex.Domain.Apps.Core.Apps
return this; return this;
} }
return Create(inner.With(name, role.Update(permissions), DeepComparer<Role>.Instance)); return Create(inner.With(name, role.Update(permissions)));
} }
public static bool IsDefault(string role) public static bool IsDefault(string role)

15
backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/JsonWorkflowTransition.cs

@ -5,21 +5,20 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Linq;
using Newtonsoft.Json; using Newtonsoft.Json;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Domain.Apps.Core.Contents.Json namespace Squidex.Domain.Apps.Core.Contents.Json
{ {
public class JsonWorkflowTransition public class JsonWorkflowTransition
{ {
[JsonProperty] [JsonProperty]
public string Expression { get; set; } public string? Expression { get; set; }
[JsonProperty] [JsonProperty]
public string Role { get; set; } public string? Role { get; set; }
[JsonProperty] [JsonProperty]
public string[] Roles { get; } public string[]? Roles { get; set; }
public JsonWorkflowTransition() public JsonWorkflowTransition()
{ {
@ -27,7 +26,9 @@ namespace Squidex.Domain.Apps.Core.Contents.Json
public JsonWorkflowTransition(WorkflowTransition transition) public JsonWorkflowTransition(WorkflowTransition transition)
{ {
SimpleMapper.Map(transition, this); Roles = transition.Roles?.ToArray();
Expression = transition.Expression;
} }
public WorkflowTransition ToTransition() public WorkflowTransition ToTransition()
@ -39,7 +40,7 @@ namespace Squidex.Domain.Apps.Core.Contents.Json
roles = new[] { Role }; roles = new[] { Role };
} }
return new WorkflowTransition(Expression, roles); return WorkflowTransition.When(Expression, roles);
} }
} }
} }

19
backend/src/Squidex.Domain.Apps.Core.Model/Contents/NoUpdate.cs

@ -5,20 +5,35 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Collections.ObjectModel;
using Squidex.Infrastructure.Collections;
namespace Squidex.Domain.Apps.Core.Contents namespace Squidex.Domain.Apps.Core.Contents
{ {
[Equals(DoNotAddEqualityOperators = true)]
public sealed class NoUpdate : WorkflowCondition public sealed class NoUpdate : WorkflowCondition
{ {
public static readonly NoUpdate Always = new NoUpdate(null, null); public static readonly NoUpdate Always = new NoUpdate(null, null);
public NoUpdate(string? expression, params string[]? roles) public NoUpdate(string? expression, ReadOnlyCollection<string>? roles)
: base(expression, roles) : base(expression, roles)
{ {
} }
public static NoUpdate When(string? expression, params string[]? roles) public static NoUpdate When(string? expression, params string[]? roles)
{ {
return new NoUpdate(expression, roles); if (roles?.Length > 0)
{
return new NoUpdate(expression, ReadOnlyCollection.Create(roles));
}
else if (!string.IsNullOrWhiteSpace(expression))
{
return new NoUpdate(expression, null);
}
else
{
return Always;
}
} }
} }
} }

8
backend/src/Squidex.Domain.Apps.Core.Model/Contents/Status.cs

@ -7,11 +7,12 @@
using System; using System;
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
namespace Squidex.Domain.Apps.Core.Contents namespace Squidex.Domain.Apps.Core.Contents
{ {
[TypeConverter(typeof(StatusConverter))] [TypeConverter(typeof(StatusConverter))]
public struct Status : IEquatable<Status> public struct Status : IEquatable<Status>, IComparable<Status>
{ {
public static readonly Status Archived = new Status("Archived"); public static readonly Status Archived = new Status("Archived");
public static readonly Status Draft = new Status("Draft"); public static readonly Status Draft = new Status("Draft");
@ -49,6 +50,11 @@ namespace Squidex.Domain.Apps.Core.Contents
return Name; return Name;
} }
public int CompareTo([AllowNull] Status other)
{
return string.Compare(name, other.name, StringComparison.Ordinal);
}
public static bool operator ==(Status lhs, Status rhs) public static bool operator ==(Status lhs, Status rhs)
{ {
return lhs.Equals(rhs); return lhs.Equals(rhs);

17
backend/src/Squidex.Domain.Apps.Core.Model/Contents/Workflow.cs

@ -8,9 +8,13 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using Squidex.Infrastructure;
#pragma warning disable IDE0051 // Remove unused private members
namespace Squidex.Domain.Apps.Core.Contents namespace Squidex.Domain.Apps.Core.Contents
{ {
[Equals(DoNotAddEqualityOperators = true)]
public sealed class Workflow : Named public sealed class Workflow : Named
{ {
private const string DefaultName = "Unnamed"; private const string DefaultName = "Unnamed";
@ -20,6 +24,7 @@ namespace Squidex.Domain.Apps.Core.Contents
public static readonly Workflow Default = CreateDefault(); public static readonly Workflow Default = CreateDefault();
public static readonly Workflow Empty = new Workflow(default, EmptySteps); public static readonly Workflow Empty = new Workflow(default, EmptySteps);
[IgnoreDuringEquals]
public IReadOnlyDictionary<Status, WorkflowStep> Steps { get; } = EmptySteps; public IReadOnlyDictionary<Status, WorkflowStep> Steps { get; } = EmptySteps;
public IReadOnlyList<Guid> SchemaIds { get; } = EmptySchemaIds; public IReadOnlyList<Guid> SchemaIds { get; } = EmptySchemaIds;
@ -122,5 +127,17 @@ namespace Squidex.Domain.Apps.Core.Contents
{ {
return (Initial, Steps[Initial]); return (Initial, Steps[Initial]);
} }
[CustomEqualsInternal]
private bool CustomEquals(Workflow other)
{
return Steps.EqualsDictionary(other.Steps);
}
[CustomGetHashCode]
private int CustomHashCode()
{
return Steps.DictionaryHashCode();
}
} }
} }

15
backend/src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowCondition.cs

@ -6,7 +6,6 @@
// ========================================================================== // ==========================================================================
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using Squidex.Infrastructure.Collections;
namespace Squidex.Domain.Apps.Core.Contents namespace Squidex.Domain.Apps.Core.Contents
{ {
@ -16,14 +15,18 @@ namespace Squidex.Domain.Apps.Core.Contents
public ReadOnlyCollection<string>? Roles { get; } public ReadOnlyCollection<string>? Roles { get; }
protected WorkflowCondition(string? expression, params string[]? roles) protected WorkflowCondition(string? expression, ReadOnlyCollection<string>? roles)
{ {
Expression = expression; Expression = expression;
if (roles != null) Roles = roles;
{ }
Roles = ReadOnlyCollection.Create(roles);
} public override string ToString()
{
var roles = Roles?.Count > 0 ? string.Join(", ", Roles) : "*";
return $"When=${Expression}, For={roles}";
} }
} }
} }

17
backend/src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowStep.cs

@ -6,13 +6,18 @@
// ========================================================================== // ==========================================================================
using System.Collections.Generic; using System.Collections.Generic;
using Squidex.Infrastructure;
#pragma warning disable IDE0051 // Remove unused private members
namespace Squidex.Domain.Apps.Core.Contents namespace Squidex.Domain.Apps.Core.Contents
{ {
[Equals(DoNotAddEqualityOperators = true)]
public sealed class WorkflowStep public sealed class WorkflowStep
{ {
private static readonly IReadOnlyDictionary<Status, WorkflowTransition> EmptyTransitions = new Dictionary<Status, WorkflowTransition>(); private static readonly IReadOnlyDictionary<Status, WorkflowTransition> EmptyTransitions = new Dictionary<Status, WorkflowTransition>();
[IgnoreDuringEquals]
public IReadOnlyDictionary<Status, WorkflowTransition> Transitions { get; } public IReadOnlyDictionary<Status, WorkflowTransition> Transitions { get; }
public string? Color { get; } public string? Color { get; }
@ -27,5 +32,17 @@ namespace Squidex.Domain.Apps.Core.Contents
NoUpdate = noUpdate; NoUpdate = noUpdate;
} }
[CustomEqualsInternal]
private bool CustomEquals(WorkflowStep other)
{
return Transitions.EqualsDictionary(other.Transitions);
}
[CustomGetHashCode]
private int CustomHashCode()
{
return Transitions.DictionaryHashCode();
}
} }
} }

19
backend/src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowTransition.cs

@ -5,20 +5,35 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Collections.ObjectModel;
using Squidex.Infrastructure.Collections;
namespace Squidex.Domain.Apps.Core.Contents namespace Squidex.Domain.Apps.Core.Contents
{ {
[Equals(DoNotAddEqualityOperators = true)]
public sealed class WorkflowTransition : WorkflowCondition public sealed class WorkflowTransition : WorkflowCondition
{ {
public static readonly WorkflowTransition Always = new WorkflowTransition(null, null); public static readonly WorkflowTransition Always = new WorkflowTransition(null, null);
public WorkflowTransition(string? expression, params string[]? roles) public WorkflowTransition(string? expression, ReadOnlyCollection<string>? roles)
: base(expression, roles) : base(expression, roles)
{ {
} }
public static WorkflowTransition When(string? expression, params string[]? roles) public static WorkflowTransition When(string? expression, params string[]? roles)
{ {
return new WorkflowTransition(expression, roles); if (roles?.Length > 0)
{
return new WorkflowTransition(expression, ReadOnlyCollection.Create(roles));
}
else if (!string.IsNullOrWhiteSpace(expression))
{
return new WorkflowTransition(expression, null);
}
else
{
return Always;
}
} }
} }
} }

6
backend/src/Squidex.Domain.Apps.Core.Model/Contents/Workflows.cs

@ -46,7 +46,7 @@ namespace Squidex.Domain.Apps.Core.Contents
{ {
Guard.NotNull(workflow); Guard.NotNull(workflow);
return With<Workflows>(Guid.Empty, workflow, DeepComparer<Workflow>.Instance); return With<Workflows>(Guid.Empty, workflow);
} }
[Pure] [Pure]
@ -54,7 +54,7 @@ namespace Squidex.Domain.Apps.Core.Contents
{ {
Guard.NotNull(workflow); Guard.NotNull(workflow);
return With<Workflows>(id, workflow, DeepComparer<Workflow>.Instance); return With<Workflows>(id, workflow);
} }
[Pure] [Pure]
@ -72,7 +72,7 @@ namespace Squidex.Domain.Apps.Core.Contents
return this; return this;
} }
return With<Workflows>(id, workflow, DeepComparer<Workflow>.Instance); return With<Workflows>(id, workflow);
} }
public Workflow GetFirst() public Workflow GetFirst()

2
backend/src/Squidex.Domain.Apps.Core.Model/FodyWeavers.xml

@ -1,4 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> <Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<Freezable /> <Freezable />
<Equals />
</Weavers> </Weavers>

1
backend/src/Squidex.Domain.Apps.Core.Model/FodyWeavers.xsd

@ -4,6 +4,7 @@
<xs:element name="Weavers"> <xs:element name="Weavers">
<xs:complexType> <xs:complexType>
<xs:all> <xs:all>
<xs:element name="Equals" minOccurs="0" maxOccurs="1" type="xs:anyType" />
<xs:element name="Freezable" minOccurs="0" maxOccurs="1" type="xs:anyType" /> <xs:element name="Freezable" minOccurs="0" maxOccurs="1" type="xs:anyType" />
</xs:all> </xs:all>
<xs:attribute name="VerifyAssembly" type="xs:boolean"> <xs:attribute name="VerifyAssembly" type="xs:boolean">

4
backend/src/Squidex.Domain.Apps.Core.Model/Freezable.cs

@ -7,13 +7,17 @@
using System; using System;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Domain.Apps.Core namespace Squidex.Domain.Apps.Core
{ {
[Equals(DoNotAddEquals = true, DoNotAddGetHashCode = true, DoNotAddEqualityOperators = true)]
public abstract class Freezable : IFreezable public abstract class Freezable : IFreezable
{ {
private bool isFrozen; private bool isFrozen;
[IgnoreEquals]
[IgnoreDuringEquals]
public bool IsFrozen public bool IsFrozen
{ {
get { return isFrozen; } get { return isFrozen; }

4
backend/src/Squidex.Domain.Apps.Core.Model/Rules/RuleAction.cs

@ -8,7 +8,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;
using DeepEqual.Syntax; using Squidex.Infrastructure.Reflection;
using Squidex.Infrastructure.Validation; using Squidex.Infrastructure.Validation;
namespace Squidex.Domain.Apps.Core.Rules namespace Squidex.Domain.Apps.Core.Rules
@ -41,7 +41,7 @@ namespace Squidex.Domain.Apps.Core.Rules
public bool DeepEquals(RuleAction action) public bool DeepEquals(RuleAction action)
{ {
return this.WithDeepEqual(action).IgnoreProperty<Freezable>(x => x.IsFrozen).Compare(); return SimpleEquals.IsEquals(this, action);
} }
} }
} }

4
backend/src/Squidex.Domain.Apps.Core.Model/Rules/RuleTrigger.cs

@ -5,7 +5,7 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using DeepEqual.Syntax; using Squidex.Infrastructure.Reflection;
namespace Squidex.Domain.Apps.Core.Rules namespace Squidex.Domain.Apps.Core.Rules
{ {
@ -15,7 +15,7 @@ namespace Squidex.Domain.Apps.Core.Rules
public bool DeepEquals(RuleTrigger action) public bool DeepEquals(RuleTrigger action)
{ {
return this.WithDeepEqual(action).IgnoreProperty<Freezable>(x => x.IsFrozen).Compare(); return SimpleEquals.IsEquals(this, action);
} }
} }
} }

1
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayFieldProperties.cs

@ -9,6 +9,7 @@ using System;
namespace Squidex.Domain.Apps.Core.Schemas namespace Squidex.Domain.Apps.Core.Schemas
{ {
[Equals(DoNotAddEqualityOperators = true)]
public sealed class ArrayFieldProperties : FieldProperties public sealed class ArrayFieldProperties : FieldProperties
{ {
public int? MinItems { get; set; } public int? MinItems { get; set; }

1
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/AssetsFieldProperties.cs

@ -9,6 +9,7 @@ using System.Collections.ObjectModel;
namespace Squidex.Domain.Apps.Core.Schemas namespace Squidex.Domain.Apps.Core.Schemas
{ {
[Equals(DoNotAddEqualityOperators = true)]
public sealed class AssetsFieldProperties : FieldProperties public sealed class AssetsFieldProperties : FieldProperties
{ {
public bool MustBeImage { get; set; } public bool MustBeImage { get; set; }

1
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeFieldProperties.cs

@ -9,6 +9,7 @@ using NodaTime;
namespace Squidex.Domain.Apps.Core.Schemas namespace Squidex.Domain.Apps.Core.Schemas
{ {
[Equals(DoNotAddEqualityOperators = true)]
public sealed class DateTimeFieldProperties : FieldProperties public sealed class DateTimeFieldProperties : FieldProperties
{ {
public Instant? MaxValue { get; set; } public Instant? MaxValue { get; set; }

6
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldNames.cs

@ -8,7 +8,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
using DeepEqual.Syntax;
namespace Squidex.Domain.Apps.Core.Schemas namespace Squidex.Domain.Apps.Core.Schemas
{ {
@ -45,10 +44,5 @@ namespace Squidex.Domain.Apps.Core.Schemas
return new FieldNames(list); return new FieldNames(list);
} }
public bool DeepEquals(FieldNames names)
{
return this.IsDeepEqual(names);
}
} }
} }

7
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldProperties.cs

@ -6,10 +6,10 @@
// ========================================================================== // ==========================================================================
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using DeepEqual.Syntax;
namespace Squidex.Domain.Apps.Core.Schemas namespace Squidex.Domain.Apps.Core.Schemas
{ {
[Equals(DoNotAddEqualityOperators = true)]
public abstract class FieldProperties : NamedElementPropertiesBase public abstract class FieldProperties : NamedElementPropertiesBase
{ {
public bool IsRequired { get; set; } public bool IsRequired { get; set; }
@ -27,10 +27,5 @@ namespace Squidex.Domain.Apps.Core.Schemas
public abstract RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings? settings = null); public abstract RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings? settings = null);
public abstract NestedField CreateNestedField(long id, string name, IFieldSettings? settings = null); public abstract NestedField CreateNestedField(long id, string name, IFieldSettings? settings = null);
public bool DeepEquals(FieldProperties properties)
{
return this.WithDeepEqual(properties).IgnoreProperty<Freezable>(x => x.IsFrozen).Compare();
}
} }
} }

1
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationFieldProperties.cs

@ -7,6 +7,7 @@
namespace Squidex.Domain.Apps.Core.Schemas namespace Squidex.Domain.Apps.Core.Schemas
{ {
[Equals(DoNotAddEqualityOperators = true)]
public sealed class GeolocationFieldProperties : FieldProperties public sealed class GeolocationFieldProperties : FieldProperties
{ {
public GeolocationFieldEditor Editor { get; set; } public GeolocationFieldEditor Editor { get; set; }

1
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/JsonFieldProperties.cs

@ -7,6 +7,7 @@
namespace Squidex.Domain.Apps.Core.Schemas namespace Squidex.Domain.Apps.Core.Schemas
{ {
[Equals(DoNotAddEqualityOperators = true)]
public sealed class JsonFieldProperties : FieldProperties public sealed class JsonFieldProperties : FieldProperties
{ {
public override T Accept<T>(IFieldPropertiesVisitor<T> visitor) public override T Accept<T>(IFieldPropertiesVisitor<T> visitor)

2
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NestedField{T}.cs

@ -38,7 +38,7 @@ namespace Squidex.Domain.Apps.Core.Schemas
typedProperties.Freeze(); typedProperties.Freeze();
if (properties.DeepEquals(typedProperties)) if (properties.Equals(typedProperties))
{ {
return this; return this;
} }

1
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldProperties.cs

@ -9,6 +9,7 @@ using System.Collections.ObjectModel;
namespace Squidex.Domain.Apps.Core.Schemas namespace Squidex.Domain.Apps.Core.Schemas
{ {
[Equals(DoNotAddEqualityOperators = true)]
public sealed class NumberFieldProperties : FieldProperties public sealed class NumberFieldProperties : FieldProperties
{ {
public ReadOnlyCollection<double>? AllowedValues { get; set; } public ReadOnlyCollection<double>? AllowedValues { get; set; }

1
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldProperties.cs

@ -12,6 +12,7 @@ using System.Linq;
namespace Squidex.Domain.Apps.Core.Schemas namespace Squidex.Domain.Apps.Core.Schemas
{ {
[Equals(DoNotAddEqualityOperators = true)]
public sealed class ReferencesFieldProperties : FieldProperties public sealed class ReferencesFieldProperties : FieldProperties
{ {
public int? MinItems { get; set; } public int? MinItems { get; set; }

2
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/RootField{T}.cs

@ -36,7 +36,7 @@ namespace Squidex.Domain.Apps.Core.Schemas
{ {
var typedProperties = ValidateProperties(newProperties); var typedProperties = ValidateProperties(newProperties);
if (properties.DeepEquals(typedProperties)) if (properties.Equals(typedProperties))
{ {
return this; return this;
} }

6
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Schema.cs

@ -135,7 +135,7 @@ namespace Squidex.Domain.Apps.Core.Schemas
{ {
newScripts ??= new SchemaScripts(); newScripts ??= new SchemaScripts();
if (scripts.DeepEquals(newScripts)) if (scripts.Equals(newScripts))
{ {
return this; return this;
} }
@ -152,7 +152,7 @@ namespace Squidex.Domain.Apps.Core.Schemas
{ {
names ??= FieldNames.Empty; names ??= FieldNames.Empty;
if (fieldsInLists.DeepEquals(names)) if (fieldsInLists.SetEquals(names))
{ {
return this; return this;
} }
@ -174,7 +174,7 @@ namespace Squidex.Domain.Apps.Core.Schemas
{ {
names ??= FieldNames.Empty; names ??= FieldNames.Empty;
if (fieldsInReferences.DeepEquals(names)) if (fieldsInReferences.SetEquals(names))
{ {
return this; return this;
} }

5
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaProperties.cs

@ -6,17 +6,18 @@
// ========================================================================== // ==========================================================================
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using DeepEqual.Syntax; using Squidex.Infrastructure.Reflection;
namespace Squidex.Domain.Apps.Core.Schemas namespace Squidex.Domain.Apps.Core.Schemas
{ {
[Equals(DoNotAddEqualityOperators = true)]
public sealed class SchemaProperties : NamedElementPropertiesBase public sealed class SchemaProperties : NamedElementPropertiesBase
{ {
public ReadOnlyCollection<string> Tags { get; set; } public ReadOnlyCollection<string> Tags { get; set; }
public bool DeepEquals(SchemaProperties properties) public bool DeepEquals(SchemaProperties properties)
{ {
return this.WithDeepEqual(properties).IgnoreProperty<Freezable>(x => x.IsFrozen).Compare(); return SimpleEquals.IsEquals(this, properties);
} }
} }
} }

8
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaScripts.cs

@ -5,10 +5,9 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using DeepEqual.Syntax;
namespace Squidex.Domain.Apps.Core.Schemas namespace Squidex.Domain.Apps.Core.Schemas
{ {
[Equals(DoNotAddEqualityOperators =true)]
public sealed class SchemaScripts : Freezable public sealed class SchemaScripts : Freezable
{ {
public static readonly SchemaScripts Empty = new SchemaScripts(); public static readonly SchemaScripts Empty = new SchemaScripts();
@ -27,10 +26,5 @@ namespace Squidex.Domain.Apps.Core.Schemas
public string Delete { get; set; } public string Delete { get; set; }
public string Query { get; set; } public string Query { get; set; }
public bool DeepEquals(SchemaScripts scripts)
{
return this.WithDeepEqual(scripts).IgnoreProperty<Freezable>(x => x.IsFrozen).Compare();
}
} }
} }

1
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldProperties.cs

@ -9,6 +9,7 @@ using System.Collections.ObjectModel;
namespace Squidex.Domain.Apps.Core.Schemas namespace Squidex.Domain.Apps.Core.Schemas
{ {
[Equals(DoNotAddEqualityOperators = true)]
public sealed class StringFieldProperties : FieldProperties public sealed class StringFieldProperties : FieldProperties
{ {
public ReadOnlyCollection<string>? AllowedValues { get; set; } public ReadOnlyCollection<string>? AllowedValues { get; set; }

1
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/UIFieldProperties.cs

@ -7,6 +7,7 @@
namespace Squidex.Domain.Apps.Core.Schemas namespace Squidex.Domain.Apps.Core.Schemas
{ {
[Equals(DoNotAddEqualityOperators = true)]
public sealed class UIFieldProperties : FieldProperties public sealed class UIFieldProperties : FieldProperties
{ {
public UIFieldEditor Editor { get; set; } public UIFieldEditor Editor { get; set; }

8
backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj

@ -10,9 +10,13 @@
<DebugSymbols>True</DebugSymbols> <DebugSymbols>True</DebugSymbols>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="DeepEqual" Version="2.0.0" /> <PackageReference Include="Equals.Fody" Version="1.9.5" />
<PackageReference Include="Fody" Version="4.2.1" PrivateAssets="all" /> <PackageReference Include="Fody" Version="4.2.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Freezable.Fody" Version="1.9.3" PrivateAssets="all" /> <PackageReference Include="Freezable.Fody" Version="1.9.3" PrivateAssets="all" />
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="3.0.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" /> <PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.Collections.Immutable" Version="1.7.0" /> <PackageReference Include="System.Collections.Immutable" Version="1.7.0" />

4
backend/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizer.cs

@ -47,7 +47,7 @@ namespace Squidex.Domain.Apps.Core.EventSynchronization
yield return E(new SchemaCategoryChanged { Name = target.Category }); yield return E(new SchemaCategoryChanged { Name = target.Category });
} }
if (!source.Scripts.DeepEquals(target.Scripts)) if (!source.Scripts.Equals(target.Scripts))
{ {
yield return E(new SchemaScriptsConfigured { Scripts = target.Scripts }); yield return E(new SchemaScriptsConfigured { Scripts = target.Scripts });
} }
@ -128,7 +128,7 @@ namespace Squidex.Domain.Apps.Core.EventSynchronization
if (canUpdate(sourceField, targetField)) if (canUpdate(sourceField, targetField))
{ {
if (!sourceField.RawProperties.DeepEquals(targetField.RawProperties)) if (!sourceField.RawProperties.Equals(targetField.RawProperties))
{ {
yield return E(new FieldUpdated { FieldId = id, Properties = targetField.RawProperties }); yield return E(new FieldUpdated { FieldId = id, Properties = targetField.RawProperties });
} }

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

@ -17,6 +17,7 @@ using Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Log; using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.MongoDb; using Squidex.Infrastructure.MongoDb;
using Squidex.Infrastructure.MongoDb.Queries;
using Squidex.Infrastructure.Queries; using Squidex.Infrastructure.Queries;
namespace Squidex.Domain.Apps.Entities.MongoDb.Assets namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
@ -65,9 +66,9 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
var assetCount = Collection.Find(filter).CountDocumentsAsync(); var assetCount = Collection.Find(filter).CountDocumentsAsync();
var assetItems = var assetItems =
Collection.Find(filter) Collection.Find(filter)
.AssetTake(query) .Take(query)
.AssetSkip(query) .Skip(query)
.AssetSort(query) .Sort(query)
.ToListAsync(); .ToListAsync();
await Task.WhenAll(assetItems, assetCount); await Task.WhenAll(assetItems, assetCount);

15
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FindExtensions.cs

@ -36,21 +36,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors
return query; return query;
} }
public static IFindFluent<MongoAssetEntity, MongoAssetEntity> AssetSort(this IFindFluent<MongoAssetEntity, MongoAssetEntity> cursor, ClrQuery query)
{
return cursor.Sort(query.BuildSort<MongoAssetEntity>());
}
public static IFindFluent<MongoAssetEntity, MongoAssetEntity> AssetTake(this IFindFluent<MongoAssetEntity, MongoAssetEntity> cursor, ClrQuery query)
{
return cursor.Take(query);
}
public static IFindFluent<MongoAssetEntity, MongoAssetEntity> AssetSkip(this IFindFluent<MongoAssetEntity, MongoAssetEntity> cursor, ClrQuery query)
{
return cursor.Skip(query);
}
public static FilterDefinition<MongoAssetEntity> BuildFilter(this ClrQuery query, Guid appId, Guid? parentId) public static FilterDefinition<MongoAssetEntity> BuildFilter(this ClrQuery query, Guid appId, Guid? parentId)
{ {
var filters = new List<FilterDefinition<MongoAssetEntity>> var filters = new List<FilterDefinition<MongoAssetEntity>>

270
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs

@ -1,270 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MongoDB.Bson;
using MongoDB.Driver;
using NodaTime;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Contents;
using Squidex.Domain.Apps.Entities.Contents.State;
using Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json;
using Squidex.Infrastructure.MongoDb;
using Squidex.Infrastructure.Queries;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
{
internal class MongoContentCollection : MongoRepositoryBase<MongoContentEntity>
{
private readonly IAppProvider appProvider;
private readonly IJsonSerializer serializer;
public MongoContentCollection(IMongoDatabase database, IJsonSerializer serializer, IAppProvider appProvider)
: base(database)
{
this.appProvider = appProvider;
this.serializer = serializer;
}
protected override Task SetupCollectionAsync(IMongoCollection<MongoContentEntity> collection, CancellationToken ct = default)
{
return collection.Indexes.CreateManyAsync(new[]
{
new CreateIndexModel<MongoContentEntity>(Index
.Ascending(x => x.IndexedAppId)
.Ascending(x => x.IsDeleted)
.Ascending(x => x.Status)
.Descending(x => x.LastModified)),
new CreateIndexModel<MongoContentEntity>(Index
.Ascending(x => x.IndexedAppId)
.Ascending(x => x.IsDeleted)
.Ascending(x => x.Status)
.Ascending(x => x.Id)),
new CreateIndexModel<MongoContentEntity>(Index
.Ascending(x => x.IndexedSchemaId)
.Ascending(x => x.IsDeleted)
.Ascending(x => x.Status)
.Descending(x => x.LastModified)),
new CreateIndexModel<MongoContentEntity>(Index
.Ascending(x => x.IndexedSchemaId)
.Ascending(x => x.IsDeleted)
.Ascending(x => x.Status)
.Ascending(x => x.Id)),
new CreateIndexModel<MongoContentEntity>(Index
.Ascending(x => x.IndexedAppId)
.Ascending(x => x.Id)),
new CreateIndexModel<MongoContentEntity>(Index
.Ascending(x => x.ScheduledAt)
.Ascending(x => x.IsDeleted)),
new CreateIndexModel<MongoContentEntity>(Index
.Ascending(x => x.ReferencedIds))
}, ct);
}
protected override string CollectionName()
{
return "State_Contents";
}
public async Task<IResultList<IContentEntity>> QueryAsync(ISchemaEntity schema, ClrQuery query, List<Guid>? ids, Status[]? status, bool inDraft, bool includeDraft = true)
{
try
{
query = query.AdjustToModel(schema.SchemaDef, inDraft);
var filter = query.ToFilter(schema.Id, ids, status);
var contentCount = Collection.Find(filter).CountDocumentsAsync();
var contentItems =
Collection.Find(filter)
.WithoutDraft(includeDraft)
.ContentTake(query)
.ContentSkip(query)
.ContentSort(query)
.ToListAsync();
await Task.WhenAll(contentItems, contentCount);
foreach (var entity in contentItems.Result)
{
entity.ParseData(schema.SchemaDef, serializer);
}
return ResultList.Create<IContentEntity>(contentCount.Result, contentItems.Result);
}
catch (MongoQueryException ex)
{
if (ex.Message.Contains("17406"))
{
throw new DomainException("Result set is too large to be retrieved. Use $top parameter to reduce the number of items.");
}
else
{
throw;
}
}
}
public async Task<List<(IContentEntity Content, ISchemaEntity Schema)>> QueryAsync(IAppEntity app, HashSet<Guid> ids, Status[]? status, bool includeDraft)
{
var find = Collection.Find(FilterFactory.IdsByApp(app.Id, ids, status));
var contentItems = await find.WithoutDraft(includeDraft).ToListAsync();
var schemaIds = contentItems.Select(x => x.IndexedSchemaId).ToList();
var schemas = await Task.WhenAll(schemaIds.Select(x => appProvider.GetSchemaAsync(app.Id, x)));
var result = new List<(IContentEntity Content, ISchemaEntity Schema)>();
foreach (var entity in contentItems)
{
var schema = schemas.FirstOrDefault(x => x?.Id == entity.IndexedSchemaId);
if (schema != null)
{
entity.ParseData(schema.SchemaDef, serializer);
result.Add((entity, schema));
}
}
return result;
}
public async Task<IResultList<IContentEntity>> QueryAsync(ISchemaEntity schema, HashSet<Guid> ids, Status[]? status, bool includeDraft)
{
var find = Collection.Find(FilterFactory.IdsBySchema(schema.Id, ids, status));
var contentItems = await find.WithoutDraft(includeDraft).ToListAsync();
foreach (var entity in contentItems)
{
entity.ParseData(schema.SchemaDef, serializer);
}
return ResultList.Create<IContentEntity>(contentItems.Count, contentItems);
}
public async Task<IContentEntity?> FindContentAsync(ISchemaEntity schema, Guid id, Status[]? status, bool includeDraft)
{
var find = Collection.Find(x => x.Id == id);
var contentEntity = await find.WithoutDraft(includeDraft).FirstOrDefaultAsync();
if (contentEntity != null)
{
if (contentEntity.IndexedSchemaId != schema.Id || status?.Contains(contentEntity.Status) == false)
{
return null;
}
contentEntity?.ParseData(schema.SchemaDef, serializer);
}
return contentEntity;
}
public Task QueryScheduledWithoutDataAsync(Instant now, Func<IContentEntity, Task> callback)
{
return Collection.Find(x => x.ScheduledAt < now && x.IsDeleted != true)
.Not(x => x.DataByIds)
.Not(x => x.DataDraftByIds)
.ForEachAsync(c =>
{
callback(c);
});
}
public async Task<IReadOnlyList<(Guid SchemaId, Guid Id)>> QueryIdsAsync(ISchemaEntity schema, FilterNode<ClrValue> filterNode)
{
var filter = filterNode.AdjustToModel(schema.SchemaDef, true)?.ToFilter(schema.Id);
var contentEntities =
await Collection.Find(filter).Only(x => x.Id, x => x.IndexedSchemaId)
.ToListAsync();
return contentEntities.Select(x => (Guid.Parse(x["_si"].AsString), Guid.Parse(x["_id"].AsString))).ToList();
}
public async Task<IReadOnlyList<(Guid SchemaId, Guid Id)>> QueryIdsAsync(Guid appId, HashSet<Guid> ids)
{
var contentEntities =
await Collection.Find(Filter.And(Filter.Eq(x => x.IndexedAppId, appId), Filter.In(x => x.Id, ids))).Only(x => x.Id, x => x.IndexedSchemaId)
.ToListAsync();
return contentEntities.Select(x => (Guid.Parse(x["_si"].AsString), Guid.Parse(x["_id"].AsString))).ToList();
}
public async Task<IReadOnlyList<Guid>> QueryIdsAsync(Guid appId)
{
var contentEntities =
await Collection.Find(x => x.IndexedAppId == appId).Only(x => x.Id)
.ToListAsync();
return contentEntities.Select(x => Guid.Parse(x["_id"].AsString)).ToList();
}
public async Task<(ContentState Value, long Version)> ReadAsync(Guid key, Func<Guid, Guid, Task<ISchemaEntity>> getSchema)
{
var contentEntity =
await Collection.Find(x => x.Id == key)
.FirstOrDefaultAsync();
if (contentEntity != null)
{
var schema = await getSchema(contentEntity.IndexedAppId, contentEntity.IndexedSchemaId);
contentEntity.ParseData(schema.SchemaDef, serializer);
return (SimpleMapper.Map(contentEntity, new ContentState()), contentEntity.Version);
}
return (null!, EtagVersion.NotFound);
}
public Task ReadAllAsync(Func<ContentState, long, Task> callback, Func<Guid, Guid, Task<ISchemaEntity>> getSchema, CancellationToken ct = default)
{
return Collection.Find(new BsonDocument(), options: Batching.Options).ForEachPipelineAsync(async contentEntity =>
{
var schema = await getSchema(contentEntity.IndexedAppId, contentEntity.IndexedSchemaId);
contentEntity.ParseData(schema.SchemaDef, serializer);
await callback(SimpleMapper.Map(contentEntity, new ContentState()), contentEntity.Version);
}, ct);
}
public Task CleanupAsync(Guid id)
{
return Collection.UpdateManyAsync(
Filter.And(
Filter.AnyEq(x => x.ReferencedIds, id),
Filter.AnyNe(x => x.ReferencedIdsDeleted, id)),
Update.AddToSet(x => x.ReferencedIdsDeleted, id));
}
public Task RemoveAsync(Guid id)
{
return Collection.DeleteOneAsync(x => x.Id == id);
}
public Task UpsertAsync(MongoContentEntity content, long oldVersion)
{
return Collection.UpsertVersionedAsync(content.Id, oldVersion, content);
}
}
}

86
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs

@ -7,6 +7,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using MongoDB.Driver; using MongoDB.Driver;
@ -16,26 +17,31 @@ using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Contents; using Squidex.Domain.Apps.Entities.Contents;
using Squidex.Domain.Apps.Entities.Contents.Repositories; using Squidex.Domain.Apps.Entities.Contents.Repositories;
using Squidex.Domain.Apps.Entities.Contents.Text; using Squidex.Domain.Apps.Entities.Contents.Text;
using Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations;
using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Domain.Apps.Events.Assets; using Squidex.Domain.Apps.Events.Assets;
using Squidex.Domain.Apps.Events.Contents; using Squidex.Domain.Apps.Events.Contents;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Json;
using Squidex.Infrastructure.Log; using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.MongoDb;
using Squidex.Infrastructure.Queries; using Squidex.Infrastructure.Queries;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
{ {
public partial class MongoContentRepository : IContentRepository, IInitializable public partial class MongoContentRepository : MongoRepositoryBase<MongoContentEntity>, IContentRepository
{ {
private static readonly List<(Guid SchemaId, Guid Id)> EmptyIds = new List<(Guid SchemaId, Guid Id)>();
private readonly IAppProvider appProvider; private readonly IAppProvider appProvider;
private readonly IJsonSerializer serializer; private readonly IJsonSerializer serializer;
private readonly ITextIndexer indexer;
private readonly string typeAssetDeleted; private readonly string typeAssetDeleted;
private readonly string typeContentDeleted; private readonly string typeContentDeleted;
private readonly MongoContentCollection contents; private readonly CleanupReferences cleanupReferences;
private readonly QueryContent queryContentAsync;
private readonly QueryContentsByIds queryContentsById;
private readonly QueryContentsByQuery queryContentsByQuery;
private readonly QueryIdsAsync queryIdsAsync;
private readonly QueryScheduledContents queryScheduledItems;
static MongoContentRepository() static MongoContentRepository()
{ {
@ -43,77 +49,79 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
} }
public MongoContentRepository(IMongoDatabase database, IAppProvider appProvider, IJsonSerializer serializer, ITextIndexer indexer, TypeNameRegistry typeNameRegistry) public MongoContentRepository(IMongoDatabase database, IAppProvider appProvider, IJsonSerializer serializer, ITextIndexer indexer, TypeNameRegistry typeNameRegistry)
: base(database)
{ {
Guard.NotNull(appProvider); Guard.NotNull(appProvider);
Guard.NotNull(serializer); Guard.NotNull(serializer);
Guard.NotNull(indexer);
Guard.NotNull(typeNameRegistry); Guard.NotNull(typeNameRegistry);
this.appProvider = appProvider; this.appProvider = appProvider;
this.indexer = indexer;
this.serializer = serializer; this.serializer = serializer;
cleanupReferences = new CleanupReferences();
queryContentAsync = new QueryContent(serializer);
queryContentsById = new QueryContentsByIds(serializer, appProvider);
queryContentsByQuery = new QueryContentsByQuery(serializer, indexer);
queryIdsAsync = new QueryIdsAsync(appProvider);
queryScheduledItems = new QueryScheduledContents();
typeAssetDeleted = typeNameRegistry.GetName<AssetDeleted>(); typeAssetDeleted = typeNameRegistry.GetName<AssetDeleted>();
typeContentDeleted = typeNameRegistry.GetName<ContentDeleted>(); typeContentDeleted = typeNameRegistry.GetName<ContentDeleted>();
}
contents = new MongoContentCollection(database, serializer, appProvider); protected override async Task SetupCollectionAsync(IMongoCollection<MongoContentEntity> collection, CancellationToken ct = default)
{
await cleanupReferences.PrepareAsync(collection, ct);
await queryContentAsync.PrepareAsync(collection, ct);
await queryContentsById.PrepareAsync(collection, ct);
await queryContentsByQuery.PrepareAsync(collection, ct);
await queryIdsAsync.PrepareAsync(collection, ct);
await queryScheduledItems.PrepareAsync(collection, ct);
} }
public Task InitializeAsync(CancellationToken ct = default) protected override string CollectionName()
{ {
return contents.InitializeAsync(ct); return "State_Contents";
} }
public async Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[]? status, bool inDraft, ClrQuery query, bool includeDraft = true) public async Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[]? status, bool inDraft, ClrQuery query, bool includeDraft = true)
{ {
Guard.NotNull(app);
Guard.NotNull(schema);
Guard.NotNull(query);
using (Profiler.TraceMethod<MongoContentRepository>("QueryAsyncByQuery")) using (Profiler.TraceMethod<MongoContentRepository>("QueryAsyncByQuery"))
{ {
var fullTextIds = await indexer.SearchAsync(query.FullText, app, schema.Id, inDraft ? Scope.Draft : Scope.Published); return await queryContentsByQuery.DoAsync(app, schema, query, status, inDraft, includeDraft);
if (fullTextIds?.Count == 0)
{
return ResultList.CreateFrom<IContentEntity>(0);
}
return await contents.QueryAsync(schema, query, fullTextIds, status, inDraft, includeDraft);
} }
} }
public async Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[]? status, HashSet<Guid> ids, bool includeDraft = true) public async Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[]? status, HashSet<Guid> ids, bool includeDraft = true)
{ {
Guard.NotNull(app); Guard.NotNull(app);
Guard.NotNull(ids);
Guard.NotNull(schema);
using (Profiler.TraceMethod<MongoContentRepository>("QueryAsyncByIds")) using (Profiler.TraceMethod<MongoContentRepository>("QueryAsyncByIds"))
{ {
return await contents.QueryAsync(schema, ids, status, includeDraft); var result = await queryContentsById.DoAsync(app.Id, schema, ids, status, includeDraft);
return ResultList.Create(result.Count, result.Select(x => x.Content));
} }
} }
public async Task<List<(IContentEntity Content, ISchemaEntity Schema)>> QueryAsync(IAppEntity app, Status[]? status, HashSet<Guid> ids, bool includeDraft = true) public async Task<List<(IContentEntity Content, ISchemaEntity Schema)>> QueryAsync(IAppEntity app, Status[]? status, HashSet<Guid> ids, bool includeDraft = true)
{ {
Guard.NotNull(app); Guard.NotNull(app);
Guard.NotNull(ids);
using (Profiler.TraceMethod<MongoContentRepository>("QueryAsyncByIdsWithoutSchema")) using (Profiler.TraceMethod<MongoContentRepository>("QueryAsyncByIdsWithoutSchema"))
{ {
return await contents.QueryAsync(app, ids, status, includeDraft); var result = await queryContentsById.DoAsync(app.Id, null, ids, status, includeDraft);
return result;
} }
} }
public async Task<IContentEntity?> FindContentAsync(IAppEntity app, ISchemaEntity schema, Status[]? status, Guid id, bool includeDraft = true) public async Task<IContentEntity?> FindContentAsync(IAppEntity app, ISchemaEntity schema, Status[]? status, Guid id, bool includeDraft = true)
{ {
Guard.NotNull(app);
Guard.NotNull(schema);
using (Profiler.TraceMethod<MongoContentRepository>()) using (Profiler.TraceMethod<MongoContentRepository>())
{ {
return await contents.FindContentAsync(schema, id, status, includeDraft); return await queryContentAsync.DoAsync(schema, id, status, includeDraft);
} }
} }
@ -121,7 +129,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
{ {
using (Profiler.TraceMethod<MongoContentRepository>()) using (Profiler.TraceMethod<MongoContentRepository>())
{ {
await contents.QueryScheduledWithoutDataAsync(now, callback); await queryScheduledItems.DoAsync(now, callback);
} }
} }
@ -129,7 +137,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
{ {
using (Profiler.TraceMethod<MongoContentRepository>()) using (Profiler.TraceMethod<MongoContentRepository>())
{ {
return await contents.QueryIdsAsync(appId, ids); return await queryIdsAsync.DoAsync(appId, ids);
} }
} }
@ -137,20 +145,8 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
{ {
using (Profiler.TraceMethod<MongoContentRepository>()) using (Profiler.TraceMethod<MongoContentRepository>())
{ {
var schema = await appProvider.GetSchemaAsync(appId, schemaId); return await queryIdsAsync.DoAsync(appId, schemaId, filterNode);
if (schema == null)
{
return EmptyIds;
}
return await contents.QueryIdsAsync(schema, filterNode);
} }
} }
public Task ClearAsync()
{
return contents.ClearAsync();
}
} }
} }

4
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_EventHandling.cs

@ -35,10 +35,10 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
switch (@event.Payload) switch (@event.Payload)
{ {
case AssetDeleted e: case AssetDeleted e:
return contents.CleanupAsync(e.AssetId); return cleanupReferences.DoAsync(e.AssetId);
case ContentDeleted e: case ContentDeleted e:
return contents.CleanupAsync(e.ContentId); return cleanupReferences.DoAsync(e.ContentId);
} }
return TaskHelper.Done; return TaskHelper.Done;

30
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs

@ -8,10 +8,12 @@
using System; using System;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using MongoDB.Driver;
using Squidex.Domain.Apps.Entities.Contents.State; using Squidex.Domain.Apps.Entities.Contents.State;
using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Log; using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.MongoDb;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
using Squidex.Infrastructure.States; using Squidex.Infrastructure.States;
@ -19,19 +21,16 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
{ {
public partial class MongoContentRepository : ISnapshotStore<ContentState, Guid> public partial class MongoContentRepository : ISnapshotStore<ContentState, Guid>
{ {
async Task ISnapshotStore<ContentState, Guid>.RemoveAsync(Guid key) Task ISnapshotStore<ContentState, Guid>.ReadAllAsync(Func<ContentState, long, Task> callback, CancellationToken ct)
{ {
using (Profiler.TraceMethod<MongoContentRepository>()) throw new NotSupportedException();
{
await contents.RemoveAsync(key);
}
} }
async Task ISnapshotStore<ContentState, Guid>.ReadAllAsync(Func<ContentState, long, Task> callback, CancellationToken ct) async Task ISnapshotStore<ContentState, Guid>.RemoveAsync(Guid key)
{ {
using (Profiler.TraceMethod<MongoContentRepository>()) using (Profiler.TraceMethod<MongoContentRepository>())
{ {
await contents.ReadAllAsync(callback, GetSchemaAsync, ct); await Collection.DeleteOneAsync(x => x.Id == key);
} }
} }
@ -39,7 +38,20 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
{ {
using (Profiler.TraceMethod<MongoContentRepository>()) using (Profiler.TraceMethod<MongoContentRepository>())
{ {
return await contents.ReadAsync(key, GetSchemaAsync); var contentEntity =
await Collection.Find(x => x.Id == key)
.FirstOrDefaultAsync();
if (contentEntity != null)
{
var schema = await GetSchemaAsync(contentEntity.IndexedAppId, contentEntity.IndexedSchemaId);
contentEntity.ParseData(schema.SchemaDef, serializer);
return (SimpleMapper.Map(contentEntity, new ContentState()), contentEntity.Version);
}
return (null!, EtagVersion.NotFound);
} }
} }
@ -74,7 +86,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
Version = newVersion Version = newVersion
}); });
await contents.UpsertAsync(content, oldVersion); await Collection.UpsertVersionedAsync(content.Id, oldVersion, content);
} }
} }

39
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/Adapt.cs → backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/Adapt.cs

@ -10,17 +10,31 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using MongoDB.Bson.Serialization.Attributes; using MongoDB.Bson.Serialization.Attributes;
using MongoDB.Driver;
using Squidex.Domain.Apps.Core.GenerateEdmSchema; using Squidex.Domain.Apps.Core.GenerateEdmSchema;
using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure.Queries; using Squidex.Infrastructure.Queries;
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations
{ {
public static class Adapt public static class Adapt
{ {
private static readonly Dictionary<string, string> PropertyMap = private static readonly Dictionary<string, string> PropertyMap =
typeof(MongoContentEntity).GetProperties() typeof(MongoContentEntity).GetProperties()
.ToDictionary(x => x.Name, x => x.GetCustomAttribute<BsonElementAttribute>()?.ElementName ?? x.Name, StringComparer.OrdinalIgnoreCase); .ToDictionary(
x => ToElementName(x),
x => ToName(x),
StringComparer.OrdinalIgnoreCase);
private static string ToName(PropertyInfo x)
{
return x.GetCustomAttribute<BsonElementAttribute>()?.ElementName ?? x.Name;
}
private static string ToElementName(PropertyInfo x)
{
return x.Name;
}
public static Func<PropertyPath, PropertyPath> Path(Schema schema, bool inDraft) public static Func<PropertyPath, PropertyPath> Path(Schema schema, bool inDraft)
{ {
@ -79,5 +93,26 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors
return result; return result;
}; };
} }
public static ClrQuery AdjustToModel(this ClrQuery query, Schema schema, bool useDraft)
{
var pathConverter = Path(schema, useDraft);
if (query.Filter != null)
{
query.Filter = query.Filter.Accept(new AdaptionVisitor(pathConverter));
}
query.Sort = query.Sort.Select(x => new SortNode(pathConverter(x.Path), x.Order)).ToList();
return query;
}
public static FilterNode<ClrValue>? AdjustToModel(this FilterNode<ClrValue> filterNode, Schema schema, bool useDraft)
{
var pathConverter = Path(schema, useDraft);
return filterNode.Accept(new AdaptionVisitor(pathConverter));
}
} }
} }

2
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/AdaptionVisitor.cs → backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/AdaptionVisitor.cs

@ -11,7 +11,7 @@ using System.Linq;
using NodaTime; using NodaTime;
using Squidex.Infrastructure.Queries; using Squidex.Infrastructure.Queries;
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations
{ {
internal sealed class AdaptionVisitor : TransformVisitor<ClrValue> internal sealed class AdaptionVisitor : TransformVisitor<ClrValue>
{ {

35
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/CleanupReferences.cs

@ -0,0 +1,35 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading;
using System.Threading.Tasks;
using MongoDB.Driver;
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations
{
internal sealed class CleanupReferences : OperationBase
{
protected override Task PrepareAsync(CancellationToken ct = default)
{
var index =
new CreateIndexModel<MongoContentEntity>(
Index.Ascending(x => x.ReferencedIds));
return Collection.Indexes.CreateOneAsync(index, cancellationToken: ct);
}
public Task DoAsync(Guid id)
{
return Collection.UpdateManyAsync(
Filter.And(
Filter.AnyEq(x => x.ReferencedIds, id),
Filter.AnyNe(x => x.ReferencedIdsDeleted, id)),
Update.AddToSet(x => x.ReferencedIdsDeleted, id));
}
}
}

12
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Extensions.cs → backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/Extensions.cs

@ -8,11 +8,13 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using MongoDB.Driver;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.ConvertContent; using Squidex.Domain.Apps.Core.ConvertContent;
using Squidex.Domain.Apps.Core.ExtractReferenceIds; using Squidex.Domain.Apps.Core.ExtractReferenceIds;
using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Json;
using Squidex.Infrastructure.MongoDb;
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
{ {
@ -42,5 +44,15 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
FieldConverters.ForNestedName2Id( FieldConverters.ForNestedName2Id(
ValueConverters.EncodeJson(serializer))); ValueConverters.EncodeJson(serializer)));
} }
public static bool HasStatus(this MongoContentEntity content, Status[]? status)
{
return status == null || status.Contains(content.Status);
}
public static IFindFluent<MongoContentEntity, MongoContentEntity> WithoutDraft(this IFindFluent<MongoContentEntity, MongoContentEntity> cursor, bool includeDraft)
{
return !includeDraft ? cursor.Not(x => x.DataDraftByIds, x => x.IsDeleted) : cursor;
}
} }
} }

39
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/OperationBase.cs

@ -0,0 +1,39 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Threading;
using System.Threading.Tasks;
using MongoDB.Driver;
using Squidex.Infrastructure.MongoDb;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations
{
public abstract class OperationBase
{
protected static readonly SortDefinitionBuilder<MongoContentEntity> Sort = Builders<MongoContentEntity>.Sort;
protected static readonly UpdateDefinitionBuilder<MongoContentEntity> Update = Builders<MongoContentEntity>.Update;
protected static readonly FieldDefinitionBuilder<MongoContentEntity> Fields = FieldDefinitionBuilder<MongoContentEntity>.Instance;
protected static readonly FilterDefinitionBuilder<MongoContentEntity> Filter = Builders<MongoContentEntity>.Filter;
protected static readonly IndexKeysDefinitionBuilder<MongoContentEntity> Index = Builders<MongoContentEntity>.IndexKeys;
protected static readonly ProjectionDefinitionBuilder<MongoContentEntity> Projection = Builders<MongoContentEntity>.Projection;
public IMongoCollection<MongoContentEntity> Collection { get; private set; }
public Task PrepareAsync(IMongoCollection<MongoContentEntity> collection, CancellationToken ct = default)
{
Collection = collection;
return PrepareAsync(ct);
}
protected virtual Task PrepareAsync(CancellationToken ct = default)
{
return TaskHelper.Done;
}
}
}

49
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryContent.cs

@ -0,0 +1,49 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
using MongoDB.Driver;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.Contents;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json;
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations
{
internal sealed class QueryContent : OperationBase
{
private readonly IJsonSerializer serializer;
public QueryContent(IJsonSerializer serializer)
{
this.serializer = serializer;
}
public async Task<IContentEntity?> DoAsync(ISchemaEntity schema, Guid id, Status[]? status, bool includeDraft)
{
Guard.NotNull(schema);
var find = Collection.Find(x => x.Id == id).WithoutDraft(includeDraft);
var contentEntity = await find.FirstOrDefaultAsync();
if (contentEntity != null)
{
if (contentEntity.IndexedSchemaId != schema.Id || !contentEntity.HasStatus(status))
{
return null;
}
contentEntity?.ParseData(schema.SchemaDef, serializer);
}
return contentEntity;
}
}
}

128
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryContentsByIds.cs

@ -0,0 +1,128 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MongoDB.Driver;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.Contents;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json;
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations
{
internal sealed class QueryContentsByIds : OperationBase
{
private readonly IJsonSerializer serializer;
private readonly IAppProvider appProvider;
public QueryContentsByIds(IJsonSerializer serializer, IAppProvider appProvider)
{
this.serializer = serializer;
this.appProvider = appProvider;
}
protected override Task PrepareAsync(CancellationToken ct = default)
{
var index =
new CreateIndexModel<MongoContentEntity>(Index
.Ascending(x => x.IndexedAppId)
.Ascending(x => x.IsDeleted)
.Ascending(x => x.Status)
.Descending(x => x.LastModified));
return Collection.Indexes.CreateOneAsync(index, cancellationToken: ct);
}
public async Task<List<(IContentEntity Content, ISchemaEntity Schema)>> DoAsync(Guid appId, ISchemaEntity? schema, HashSet<Guid> ids, Status[]? status, bool includeDraft)
{
Guard.NotNull(ids);
var find = Collection.Find(CreateFilter(appId, ids, status)).WithoutDraft(includeDraft);
var contentItems = await find.ToListAsync();
var contentSchemas = await GetSchemasAsync(appId, schema, contentItems);
var result = new List<(IContentEntity Content, ISchemaEntity Schema)>();
foreach (var contentEntity in contentItems)
{
if (contentEntity.HasStatus(status) && contentSchemas.TryGetValue(contentEntity.IndexedSchemaId, out var contentSchema))
{
contentEntity.ParseData(contentSchema.SchemaDef, serializer);
result.Add((contentEntity, contentSchema));
}
}
return result;
}
private async Task<IDictionary<Guid, ISchemaEntity>> GetSchemasAsync(Guid appId, ISchemaEntity? schema, List<MongoContentEntity> contentItems)
{
var schemas = new Dictionary<Guid, ISchemaEntity>();
if (schema != null)
{
schemas[schema.Id] = schema;
}
var misingSchemaIds = contentItems.Select(x => x.IndexedSchemaId).Distinct().Where(x => !schemas.ContainsKey(x));
var missingSchemas = await Task.WhenAll(misingSchemaIds.Select(x => appProvider.GetSchemaAsync(appId, x)));
foreach (var missingSchema in missingSchemas)
{
schemas[missingSchema.Id] = missingSchema;
}
return schemas;
}
private static FilterDefinition<MongoContentEntity> CreateFilter(Guid appId, ICollection<Guid> ids, Status[]? status)
{
var filters = new List<FilterDefinition<MongoContentEntity>>
{
Filter.Eq(x => x.IndexedAppId, appId),
Filter.Ne(x => x.IsDeleted, true)
};
if (status != null)
{
filters.Add(Filter.In(x => x.Status, status));
}
else
{
filters.Add(Filter.Exists(x => x.Status));
}
if (ids != null && ids.Count > 0)
{
if (ids.Count > 1)
{
filters.Add(
Filter.Or(
Filter.In(x => x.Id, ids)));
}
else
{
var first = ids.First();
filters.Add(
Filter.Or(
Filter.Eq(x => x.Id, first)));
}
}
return Filter.And(filters);
}
}
}

159
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryContentsByQuery.cs

@ -0,0 +1,159 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MongoDB.Driver;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Contents;
using Squidex.Domain.Apps.Entities.Contents.Text;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json;
using Squidex.Infrastructure.MongoDb.Queries;
using Squidex.Infrastructure.Queries;
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations
{
internal sealed class QueryContentsByQuery : OperationBase
{
private readonly IJsonSerializer serializer;
private readonly ITextIndexer indexer;
public QueryContentsByQuery(IJsonSerializer serializer, ITextIndexer indexer)
{
this.serializer = serializer;
this.indexer = indexer;
}
protected override Task PrepareAsync(CancellationToken ct = default)
{
var index1 =
new CreateIndexModel<MongoContentEntity>(Index
.Ascending(x => x.IndexedSchemaId)
.Ascending(x => x.IsDeleted)
.Ascending(x => x.Status)
.Ascending(x => x.Id)
.Ascending(x => x.ReferencedIds)
.Descending(x => x.LastModified));
var index2 =
new CreateIndexModel<MongoContentEntity>(Index
.Ascending(x => x.IndexedSchemaId)
.Ascending(x => x.IsDeleted)
.Ascending(x => x.Status)
.Descending(x => x.LastModified));
return Collection.Indexes.CreateManyAsync(new[] { index1, index2, }, ct);
}
public async Task<IResultList<IContentEntity>> DoAsync(IAppEntity app, ISchemaEntity schema, ClrQuery query, Status[]? status, bool inDraft, bool includeDraft = true)
{
Guard.NotNull(app);
Guard.NotNull(schema);
Guard.NotNull(query);
try
{
query = query.AdjustToModel(schema.SchemaDef, inDraft);
List<Guid>? fullTextIds = null;
if (!string.IsNullOrWhiteSpace(query.FullText))
{
fullTextIds = await indexer.SearchAsync(query.FullText, app, schema.Id, inDraft ? Scope.Draft : Scope.Published);
if (fullTextIds?.Count == 0)
{
return ResultList.CreateFrom<IContentEntity>(0);
}
}
var filter = CreateFilter(schema.Id, fullTextIds, status, query);
var contentCount = Collection.Find(filter).CountDocumentsAsync();
var contentItems =
Collection.Find(filter)
.WithoutDraft(includeDraft)
.Take(query)
.Skip(query)
.Sort(query)
.ToListAsync();
await Task.WhenAll(contentItems, contentCount);
foreach (var entity in contentItems.Result)
{
entity.ParseData(schema.SchemaDef, serializer);
}
return ResultList.Create<IContentEntity>(contentCount.Result, contentItems.Result);
}
catch (MongoQueryException ex)
{
if (ex.Message.Contains("17406"))
{
throw new DomainException("Result set is too large to be retrieved. Use $top parameter to reduce the number of items.");
}
else
{
throw;
}
}
}
private static FilterDefinition<MongoContentEntity> CreateFilter(Guid schemaId, ICollection<Guid>? ids, Status[]? status, ClrQuery? query)
{
var filters = new List<FilterDefinition<MongoContentEntity>>
{
Filter.Eq(x => x.IndexedSchemaId, schemaId),
Filter.Ne(x => x.IsDeleted, true)
};
if (status != null)
{
filters.Add(Filter.In(x => x.Status, status));
}
else
{
filters.Add(Filter.Exists(x => x.Status));
}
if (ids != null && ids.Count > 0)
{
if (ids.Count > 1)
{
filters.Add(
Filter.Or(
Filter.In(x => x.Id, ids),
Filter.AnyIn(x => x.ReferencedIds, ids)));
}
else
{
var first = ids.First();
filters.Add(
Filter.Or(
Filter.Eq(x => x.Id, first),
Filter.AnyEq(x => x.ReferencedIds, first)));
}
}
if (query?.Filter != null)
{
filters.Add(query.Filter.BuildFilter<MongoContentEntity>());
}
return Filter.And(filters);
}
}
}

88
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryIdsAsync.cs

@ -0,0 +1,88 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MongoDB.Driver;
using Squidex.Infrastructure.MongoDb;
using Squidex.Infrastructure.MongoDb.Queries;
using Squidex.Infrastructure.Queries;
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations
{
internal sealed class QueryIdsAsync : OperationBase
{
private static readonly List<(Guid SchemaId, Guid Id)> EmptyIds = new List<(Guid SchemaId, Guid Id)>();
private readonly IAppProvider appProvider;
public QueryIdsAsync(IAppProvider appProvider)
{
this.appProvider = appProvider;
}
protected override Task PrepareAsync(CancellationToken ct = default)
{
var index1 =
new CreateIndexModel<MongoContentEntity>(Index
.Ascending(x => x.IndexedAppId)
.Ascending(x => x.Id)
.Ascending(x => x.ReferencedIds));
var index2 =
new CreateIndexModel<MongoContentEntity>(Index
.Ascending(x => x.IndexedSchemaId));
return Collection.Indexes.CreateManyAsync(new[] { index1, index2, }, ct);
}
public async Task<IReadOnlyList<(Guid SchemaId, Guid Id)>> DoAsync(Guid appId, HashSet<Guid> ids)
{
var contentEntities =
await Collection.Find(Filter.And(Filter.Eq(x => x.IndexedAppId, appId), Filter.In(x => x.Id, ids))).Only(x => x.Id, x => x.IndexedSchemaId)
.ToListAsync();
return contentEntities.Select(x => (Guid.Parse(x["_si"].AsString), Guid.Parse(x["_id"].AsString))).ToList();
}
public async Task<IReadOnlyList<(Guid SchemaId, Guid Id)>> DoAsync(Guid appId, Guid schemaId, FilterNode<ClrValue> filterNode)
{
var schema = await appProvider.GetSchemaAsync(appId, schemaId);
if (schema == null)
{
return EmptyIds;
}
var filter = BuildFilter(filterNode.AdjustToModel(schema.SchemaDef, true), schemaId);
var contentEntities =
await Collection.Find(filter).Only(x => x.Id, x => x.IndexedSchemaId)
.ToListAsync();
return contentEntities.Select(x => (Guid.Parse(x["_si"].AsString), Guid.Parse(x["_id"].AsString))).ToList();
}
public static FilterDefinition<MongoContentEntity> BuildFilter(FilterNode<ClrValue>? filterNode, Guid schemaId)
{
var filters = new List<FilterDefinition<MongoContentEntity>>
{
Filter.Eq(x => x.IndexedSchemaId, schemaId),
Filter.Ne(x => x.IsDeleted, true),
};
if (filterNode != null)
{
filters.Add(filterNode.BuildFilter<MongoContentEntity>());
}
return Filter.And(filters);
}
}
}

44
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryScheduledContents.cs

@ -0,0 +1,44 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading;
using System.Threading.Tasks;
using MongoDB.Driver;
using NodaTime;
using Squidex.Domain.Apps.Entities.Contents;
using Squidex.Infrastructure;
using Squidex.Infrastructure.MongoDb;
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations
{
internal sealed class QueryScheduledContents : OperationBase
{
protected override Task PrepareAsync(CancellationToken ct = default)
{
var index =
new CreateIndexModel<MongoContentEntity>(Index
.Ascending(x => x.ScheduledAt)
.Ascending(x => x.IsDeleted));
return Collection.Indexes.CreateOneAsync(index, cancellationToken: ct);
}
public Task DoAsync(Instant now, Func<IContentEntity, Task> callback)
{
Guard.NotNull(callback);
return Collection.Find(x => x.ScheduledAt < now && x.IsDeleted != true)
.Not(x => x.DataByIds)
.Not(x => x.DataDraftByIds)
.ForEachAsync(c =>
{
callback(c);
});
}
}
}

134
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FilterFactory.cs

@ -1,134 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using MongoDB.Driver;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure.MongoDb;
using Squidex.Infrastructure.MongoDb.Queries;
using Squidex.Infrastructure.Queries;
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors
{
public static class FilterFactory
{
private static readonly FilterDefinitionBuilder<MongoContentEntity> Filter = Builders<MongoContentEntity>.Filter;
public static ClrQuery AdjustToModel(this ClrQuery query, Schema schema, bool useDraft)
{
var pathConverter = Adapt.Path(schema, useDraft);
if (query.Filter != null)
{
query.Filter = query.Filter.Accept(new AdaptionVisitor(pathConverter));
}
query.Sort = query.Sort.Select(x => new SortNode(pathConverter(x.Path), x.Order)).ToList();
return query;
}
public static FilterNode<ClrValue>? AdjustToModel(this FilterNode<ClrValue> filterNode, Schema schema, bool useDraft)
{
var pathConverter = Adapt.Path(schema, useDraft);
return filterNode.Accept(new AdaptionVisitor(pathConverter));
}
public static IFindFluent<MongoContentEntity, MongoContentEntity> ContentSort(this IFindFluent<MongoContentEntity, MongoContentEntity> cursor, ClrQuery query)
{
return cursor.Sort(query.BuildSort<MongoContentEntity>());
}
public static IFindFluent<MongoContentEntity, MongoContentEntity> ContentTake(this IFindFluent<MongoContentEntity, MongoContentEntity> cursor, ClrQuery query)
{
return cursor.Take(query);
}
public static IFindFluent<MongoContentEntity, MongoContentEntity> ContentSkip(this IFindFluent<MongoContentEntity, MongoContentEntity> cursor, ClrQuery query)
{
return cursor.Skip(query);
}
public static IFindFluent<MongoContentEntity, MongoContentEntity> WithoutDraft(this IFindFluent<MongoContentEntity, MongoContentEntity> cursor, bool includeDraft)
{
return !includeDraft ? cursor.Not(x => x.DataDraftByIds, x => x.IsDeleted) : cursor;
}
public static FilterDefinition<MongoContentEntity> IdsByApp(Guid appId, ICollection<Guid> ids, Status[]? status)
{
return CreateFilter(appId, null, ids, status, null);
}
public static FilterDefinition<MongoContentEntity> IdsBySchema(Guid schemaId, ICollection<Guid> ids, Status[]? status)
{
return CreateFilter(null, schemaId, ids, status, null);
}
public static FilterDefinition<MongoContentEntity> ToFilter(this ClrQuery query, Guid schemaId, ICollection<Guid>? ids, Status[]? status)
{
return CreateFilter(null, schemaId, ids, status, query);
}
private static FilterDefinition<MongoContentEntity> CreateFilter(Guid? appId, Guid? schemaId, ICollection<Guid>? ids, Status[]? status,
ClrQuery? query)
{
var filters = new List<FilterDefinition<MongoContentEntity>>();
if (appId.HasValue)
{
filters.Add(Filter.Eq(x => x.IndexedAppId, appId.Value));
}
if (schemaId.HasValue)
{
filters.Add(Filter.Eq(x => x.IndexedSchemaId, schemaId.Value));
}
filters.Add(Filter.Ne(x => x.IsDeleted, true));
if (status != null)
{
filters.Add(Filter.In(x => x.Status, status));
}
if (ids != null && ids.Count > 0)
{
if (ids.Count > 1)
{
filters.Add(Filter.In(x => x.Id, ids));
}
else
{
filters.Add(Filter.Eq(x => x.Id, ids.First()));
}
}
if (query?.Filter != null)
{
filters.Add(query.Filter.BuildFilter<MongoContentEntity>());
}
return Filter.And(filters);
}
public static FilterDefinition<MongoContentEntity> ToFilter(this FilterNode<ClrValue> filterNode, Guid schemaId)
{
var filters = new List<FilterDefinition<MongoContentEntity>>
{
Filter.Eq(x => x.IndexedSchemaId, schemaId),
Filter.Ne(x => x.IsDeleted, true),
filterNode.BuildFilter<MongoContentEntity>()
};
return Filter.And(filters);
}
}
}

51
backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs

@ -29,11 +29,19 @@ namespace Squidex.Infrastructure.MongoDb
protected static readonly ProjectionDefinitionBuilder<TEntity> Projection = Builders<TEntity>.Projection; protected static readonly ProjectionDefinitionBuilder<TEntity> Projection = Builders<TEntity>.Projection;
private readonly IMongoDatabase mongoDatabase; private readonly IMongoDatabase mongoDatabase;
private readonly Lazy<IMongoCollection<TEntity>> mongoCollection; private IMongoCollection<TEntity> mongoCollection;
protected IMongoCollection<TEntity> Collection protected IMongoCollection<TEntity> Collection
{ {
get { return mongoCollection.Value; } get
{
if (mongoCollection == null)
{
throw new InvalidOperationException("Collection has not been initialized yet.");
}
return mongoCollection;
}
} }
protected IMongoDatabase Database protected IMongoDatabase Database
@ -48,12 +56,16 @@ namespace Squidex.Infrastructure.MongoDb
InstantSerializer.Register(); InstantSerializer.Register();
} }
protected MongoRepositoryBase(IMongoDatabase database) protected MongoRepositoryBase(IMongoDatabase database, bool setup = false)
{ {
Guard.NotNull(database); Guard.NotNull(database);
mongoDatabase = database; mongoDatabase = database;
mongoCollection = CreateCollection();
if (setup)
{
CreateCollection();
}
} }
protected virtual MongoCollectionSettings CollectionSettings() protected virtual MongoCollectionSettings CollectionSettings()
@ -66,14 +78,6 @@ namespace Squidex.Infrastructure.MongoDb
return string.Format(CultureInfo.InvariantCulture, CollectionFormat, typeof(TEntity).Name); return string.Format(CultureInfo.InvariantCulture, CollectionFormat, typeof(TEntity).Name);
} }
private Lazy<IMongoCollection<TEntity>> CreateCollection()
{
return new Lazy<IMongoCollection<TEntity>>(() =>
mongoDatabase.GetCollection<TEntity>(
CollectionName(),
CollectionSettings() ?? new MongoCollectionSettings()));
}
protected virtual Task SetupCollectionAsync(IMongoCollection<TEntity> collection, CancellationToken ct = default) protected virtual Task SetupCollectionAsync(IMongoCollection<TEntity> collection, CancellationToken ct = default)
{ {
return TaskHelper.Done; return TaskHelper.Done;
@ -81,15 +85,27 @@ namespace Squidex.Infrastructure.MongoDb
public virtual async Task ClearAsync() public virtual async Task ClearAsync()
{ {
await Database.DropCollectionAsync(CollectionName()); try
{
await Database.DropCollectionAsync(CollectionName());
}
catch (MongoCommandException ex)
{
if (ex.Code != 26)
{
throw;
}
}
await SetupCollectionAsync(Collection); await InitializeAsync();
} }
public async Task InitializeAsync(CancellationToken ct = default) public async Task InitializeAsync(CancellationToken ct = default)
{ {
try try
{ {
CreateCollection();
await SetupCollectionAsync(Collection, ct); await SetupCollectionAsync(Collection, ct);
} }
catch (Exception ex) catch (Exception ex)
@ -97,5 +113,12 @@ namespace Squidex.Infrastructure.MongoDb
throw new ConfigurationException($"MongoDb connection failed to connect to database {Database.DatabaseNamespace.DatabaseName}", ex); throw new ConfigurationException($"MongoDb connection failed to connect to database {Database.DatabaseNamespace.DatabaseName}", ex);
} }
} }
private void CreateCollection()
{
mongoCollection = mongoDatabase.GetCollection<TEntity>(
CollectionName(),
CollectionSettings() ?? new MongoCollectionSettings());
}
} }
} }

5
backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/LimitExtensions.cs

@ -31,5 +31,10 @@ namespace Squidex.Infrastructure.MongoDb.Queries
return cursor; return cursor;
} }
public static IFindFluent<T, T> Sort<T>(this IFindFluent<T, T> cursor, ClrQuery query)
{
return cursor.Sort(query.BuildSort<T>());
}
} }
} }

8
backend/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs

@ -20,11 +20,17 @@ namespace Squidex.Infrastructure.States
public class MongoSnapshotStore<T, TKey> : MongoRepositoryBase<MongoState<T, TKey>>, ISnapshotStore<T, TKey> where TKey : notnull public class MongoSnapshotStore<T, TKey> : MongoRepositoryBase<MongoState<T, TKey>>, ISnapshotStore<T, TKey> where TKey : notnull
{ {
public MongoSnapshotStore(IMongoDatabase database, JsonSerializer jsonSerializer) public MongoSnapshotStore(IMongoDatabase database, JsonSerializer jsonSerializer)
: base(database) : base(database, Register(jsonSerializer))
{
}
private static bool Register(JsonSerializer jsonSerializer)
{ {
Guard.NotNull(jsonSerializer); Guard.NotNull(jsonSerializer);
BsonJsonConvention.Register(jsonSerializer); BsonJsonConvention.Register(jsonSerializer);
return true;
} }
protected override string CollectionName() protected override string CollectionName()

4
backend/src/Squidex.Infrastructure/CollectionExtensions.cs

@ -138,12 +138,12 @@ namespace Squidex.Infrastructure
return hashCode; return hashCode;
} }
public static int DictionaryHashCode<TKey, TValue>(this IDictionary<TKey, TValue> dictionary) where TKey : notnull public static int DictionaryHashCode<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> dictionary) where TKey : notnull
{ {
return DictionaryHashCode(dictionary, EqualityComparer<TKey>.Default, EqualityComparer<TValue>.Default); return DictionaryHashCode(dictionary, EqualityComparer<TKey>.Default, EqualityComparer<TValue>.Default);
} }
public static int DictionaryHashCode<TKey, TValue>(this IDictionary<TKey, TValue> dictionary, IEqualityComparer<TKey> keyComparer, IEqualityComparer<TValue> valueComparer) where TKey : notnull public static int DictionaryHashCode<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> dictionary, IEqualityComparer<TKey> keyComparer, IEqualityComparer<TValue> valueComparer) where TKey : notnull
{ {
var hashCode = 17; var hashCode = 17;

4
backend/src/Squidex.Infrastructure/Collections/ImmutableDictionary{TKey,TValue}.cs

@ -81,7 +81,9 @@ namespace Squidex.Infrastructure.Collections
private static bool IsEqual(TValue lhs, TValue rhs, IEqualityComparer<TValue>? comparer = null) private static bool IsEqual(TValue lhs, TValue rhs, IEqualityComparer<TValue>? comparer = null)
{ {
return comparer == null || comparer.Equals(lhs, rhs); comparer ??= EqualityComparer<TValue>.Default;
return comparer.Equals(lhs, rhs);
} }
public ImmutableDictionary<TKey, TValue> Without(TKey key) public ImmutableDictionary<TKey, TValue> Without(TKey key)

3
backend/src/Squidex.Infrastructure/FodyWeavers.xml

@ -0,0 +1,3 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<Equals />
</Weavers>

26
backend/src/Squidex.Infrastructure/FodyWeavers.xsd

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. -->
<xs:element name="Weavers">
<xs:complexType>
<xs:all>
<xs:element name="Equals" minOccurs="0" maxOccurs="1" type="xs:anyType" />
</xs:all>
<xs:attribute name="VerifyAssembly" type="xs:boolean">
<xs:annotation>
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="VerifyIgnoreCodes" type="xs:string">
<xs:annotation>
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="GenerateXsd" type="xs:boolean">
<xs:annotation>
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
</xs:element>
</xs:schema>

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

@ -8,7 +8,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
@ -74,7 +73,7 @@ namespace Squidex.Infrastructure
[DebuggerStepThrough] [DebuggerStepThrough]
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void HasType(object? target, [AllowNull] Type expectedType, [CallerArgumentExpression("target")] string? parameterName = null) public static void HasType(object? target, Type? expectedType, [CallerArgumentExpression("target")] string? parameterName = null)
{ {
if (target != null && expectedType != null && target.GetType() != expectedType) if (target != null && expectedType != null && target.GetType() != expectedType)
{ {

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

@ -12,6 +12,7 @@ using System.Text.RegularExpressions;
namespace Squidex.Infrastructure namespace Squidex.Infrastructure
{ {
[Equals(DoNotAddEqualityOperators = true)]
public sealed partial class Language public sealed partial class Language
{ {
private static readonly Regex CultureRegex = new Regex("^([a-z]{2})(\\-[a-z]{2})?$", RegexOptions.IgnoreCase); private static readonly Regex CultureRegex = new Regex("^([a-z]{2})(\\-[a-z]{2})?$", RegexOptions.IgnoreCase);
@ -41,6 +42,7 @@ namespace Squidex.Infrastructure
get { return AllLanguagesField.Values; } get { return AllLanguagesField.Values; }
} }
[IgnoreDuringEquals]
public string EnglishName { get; } public string EnglishName { get; }
public string Iso2Code { get; } public string Iso2Code { get; }

18
backend/src/Squidex.Infrastructure/NamedId{T}.cs

@ -14,7 +14,8 @@ namespace Squidex.Infrastructure
{ {
public delegate bool Parser<T>(string input, out T result); public delegate bool Parser<T>(string input, out T result);
public sealed class NamedId<T> : IEquatable<NamedId<T>> where T : notnull [Equals(DoNotAddEqualityOperators = true)]
public sealed class NamedId<T> where T : notnull
{ {
private static readonly int GuidLength = Guid.Empty.ToString().Length; private static readonly int GuidLength = Guid.Empty.ToString().Length;
@ -37,21 +38,6 @@ namespace Squidex.Infrastructure
return $"{Id},{Name}"; return $"{Id},{Name}";
} }
public override bool Equals(object? obj)
{
return Equals(obj as NamedId<T>);
}
public bool Equals(NamedId<T>? other)
{
return other != null && (ReferenceEquals(this, other) || (Id.Equals(other.Id) && Name.Equals(other.Name)));
}
public override int GetHashCode()
{
return (Id.GetHashCode() * 397) ^ Name.GetHashCode();
}
public static bool TryParse(string value, Parser<T> parser, [MaybeNullWhen(false)] out NamedId<T> result) public static bool TryParse(string value, Parser<T> parser, [MaybeNullWhen(false)] out NamedId<T> result)
{ {
if (value != null) if (value != null)

15
backend/src/Squidex.Infrastructure/RefToken.cs

@ -10,17 +10,20 @@ using System.Diagnostics.CodeAnalysis;
namespace Squidex.Infrastructure namespace Squidex.Infrastructure
{ {
public sealed class RefToken : IEquatable<RefToken> [Equals(DoNotAddEqualityOperators = true)]
public sealed class RefToken
{ {
public string Type { get; } public string Type { get; }
public string Identifier { get; } public string Identifier { get; }
[IgnoreDuringEquals]
public bool IsClient public bool IsClient
{ {
get { return string.Equals(Type, RefTokenType.Client, StringComparison.OrdinalIgnoreCase); } get { return string.Equals(Type, RefTokenType.Client, StringComparison.OrdinalIgnoreCase); }
} }
[IgnoreDuringEquals]
public bool IsSubject public bool IsSubject
{ {
get { return string.Equals(Type, RefTokenType.Subject, StringComparison.OrdinalIgnoreCase); } get { return string.Equals(Type, RefTokenType.Subject, StringComparison.OrdinalIgnoreCase); }
@ -41,16 +44,6 @@ namespace Squidex.Infrastructure
return $"{Type}:{Identifier}"; return $"{Type}:{Identifier}";
} }
public override bool Equals(object? obj)
{
return Equals(obj as RefToken);
}
public bool Equals(RefToken? other)
{
return other != null && (ReferenceEquals(this, other) || (Type.Equals(other.Type) && Identifier.Equals(other.Identifier)));
}
public override int GetHashCode() public override int GetHashCode()
{ {
return (Type.GetHashCode() * 397) ^ Identifier.GetHashCode(); return (Type.GetHashCode() * 397) ^ Identifier.GetHashCode();

40
backend/src/Squidex.Infrastructure/Reflection/Equality/ArrayComparer.cs

@ -0,0 +1,40 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Infrastructure.Reflection.Equality
{
internal sealed class ArrayComparer<T> : IDeepComparer
{
private readonly IDeepComparer itemComparer;
public ArrayComparer(IDeepComparer itemComparer)
{
this.itemComparer = itemComparer;
}
public bool IsEquals(object? x, object? y)
{
var lhs = (T[])x!;
var rhs = (T[])y!;
if (lhs.Length != rhs.Length)
{
return false;
}
for (var i = 0; i < lhs.Length; i++)
{
if (!itemComparer.IsEquals(lhs[i], rhs[i]))
{
return false;
}
}
return true;
}
}
}

69
backend/src/Squidex.Infrastructure/Reflection/Equality/CollectionComparer.cs

@ -0,0 +1,69 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections;
using Squidex.Infrastructure.Reflection.Internal;
namespace Squidex.Infrastructure.Reflection.Equality
{
internal sealed class CollectionComparer : IDeepComparer
{
private readonly IDeepComparer itemComparer;
private readonly PropertyAccessor? sizeProperty;
public CollectionComparer(IDeepComparer itemComparer, PropertyAccessor? sizeProperty)
{
this.itemComparer = itemComparer;
this.sizeProperty = sizeProperty;
}
public bool IsEquals(object? x, object? y)
{
var lhs = (IEnumerable)x!;
var rhs = (IEnumerable)y!;
if (sizeProperty != null)
{
var sizeLhs = sizeProperty.Get(lhs);
var sizeRhs = sizeProperty.Get(rhs);
if (!Equals(sizeLhs, sizeRhs))
{
return false;
}
}
var enumeratorLhs = lhs.GetEnumerator();
var enumeratorRhs = rhs.GetEnumerator();
while (true)
{
var movedLhs = enumeratorLhs.MoveNext();
var movedRhs = enumeratorRhs.MoveNext();
if (movedRhs != movedLhs)
{
return false;
}
if (movedRhs)
{
if (!itemComparer.IsEquals(enumeratorLhs.Current, enumeratorRhs.Current))
{
return false;
}
}
else
{
break;
}
}
return true;
}
}
}

13
backend/src/Squidex.Domain.Apps.Core.Model/DeepComparer.cs → backend/src/Squidex.Infrastructure/Reflection/Equality/DeepEqualityComparer.cs

@ -6,21 +6,22 @@
// ========================================================================== // ==========================================================================
using System.Collections.Generic; using System.Collections.Generic;
using DeepEqual.Syntax;
namespace Squidex.Domain.Apps.Core namespace Squidex.Infrastructure.Reflection.Equality
{ {
public sealed class DeepComparer<T> : IEqualityComparer<T> public sealed class DeepEqualityComparer<T> : IEqualityComparer<T>
{ {
public static readonly DeepComparer<T> Instance = new DeepComparer<T>(); public static readonly DeepEqualityComparer<T> Default = new DeepEqualityComparer<T>();
private readonly IDeepComparer comparer;
private DeepComparer() public DeepEqualityComparer(IDeepComparer? comparer = null)
{ {
this.comparer = comparer ?? SimpleEquals.Build(typeof(T));
} }
public bool Equals(T x, T y) public bool Equals(T x, T y)
{ {
return x.IsDeepEqual(y); return comparer.IsEquals(x, y);
} }
public int GetHashCode(T obj) public int GetHashCode(T obj)

36
backend/src/Squidex.Infrastructure/Reflection/Equality/DefaultComparer.cs

@ -0,0 +1,36 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Infrastructure.Reflection.Equality
{
internal sealed class DefaultComparer : IDeepComparer
{
public bool IsEquals(object? x, object? y)
{
if (Equals(x, y))
{
return true;
}
if (x == null || y == null)
{
return false;
}
var type = x.GetType();
if (type != y.GetType())
{
return false;
}
var inner = SimpleEquals.BuildInner(type);
return inner.IsEquals(x, y);
}
}
}

37
backend/src/Squidex.Infrastructure/Reflection/Equality/DictionaryComparer{TKey,TValue}.cs

@ -0,0 +1,37 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Linq;
namespace Squidex.Infrastructure.Reflection.Equality
{
internal sealed class DictionaryComparer<TKey, TValue> : IDeepComparer where TKey : notnull
{
private readonly IEqualityComparer<KeyValuePair<TKey, TValue>> comparer;
public DictionaryComparer(IDeepComparer comparer)
{
this.comparer = new CollectionExtensions.KeyValuePairComparer<TKey, TValue>(
new DeepEqualityComparer<TKey>(comparer),
new DeepEqualityComparer<TValue>(comparer));
}
public bool IsEquals(object? x, object? y)
{
var lhs = (IReadOnlyDictionary<TKey, TValue>)x!;
var rhs = (IReadOnlyDictionary<TKey, TValue>)y!;
if (lhs.Count != rhs.Count)
{
return false;
}
return !lhs.Except(rhs, comparer).Any();
}
}
}

14
backend/src/Squidex.Infrastructure/Reflection/Equality/IDeepComparer.cs

@ -0,0 +1,14 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Infrastructure.Reflection.Equality
{
public interface IDeepComparer
{
bool IsEquals(object? x, object? y);
}
}

17
backend/src/Squidex.Infrastructure/Reflection/Equality/NoopComparer.cs

@ -0,0 +1,17 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Infrastructure.Reflection.Equality
{
internal sealed class NoopComparer : IDeepComparer
{
public bool IsEquals(object? x, object? y)
{
return false;
}
}
}

50
backend/src/Squidex.Infrastructure/Reflection/Equality/ObjectComparer.cs

@ -0,0 +1,50 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Linq;
using System.Reflection;
using Squidex.Infrastructure.Reflection.Internal;
namespace Squidex.Infrastructure.Reflection.Equality
{
public sealed class ObjectComparer : IDeepComparer
{
private readonly PropertyAccessor[] propertyAccessors;
private readonly IDeepComparer valueComparer;
public ObjectComparer(IDeepComparer valueComparer, Type type)
{
propertyAccessors =
type.GetPublicProperties()
.Where(x => x.CanRead)
.Where(x => x.GetCustomAttribute<IgnoreEqualsAttribute>() == null)
.Select(x => new PropertyAccessor(x.DeclaringType!, x))
.ToArray();
this.valueComparer = valueComparer;
}
public bool IsEquals(object? x, object? y)
{
for (var i = 0; i < propertyAccessors.Length; i++)
{
var property = propertyAccessors[i];
var lhs = property.Get(x!);
var rhs = property.Get(y!);
if (!valueComparer.IsEquals(lhs, rhs))
{
return false;
}
}
return true;
}
}
}

35
backend/src/Squidex.Infrastructure/Reflection/Equality/SetComparer.cs

@ -0,0 +1,35 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Linq;
namespace Squidex.Infrastructure.Reflection.Equality
{
internal sealed class SetComparer<T> : IDeepComparer
{
private readonly IEqualityComparer<T> equalityComparer;
public SetComparer(IDeepComparer comparer)
{
equalityComparer = new DeepEqualityComparer<T>(comparer);
}
public bool IsEquals(object? x, object? y)
{
var lhs = (ISet<T>)x!;
var rhs = (ISet<T>)y!;
if (lhs.Count != rhs.Count)
{
return false;
}
return lhs.Intersect(rhs, equalityComparer).Count() == rhs.Count;
}
}
}

10
backend/src/Squidex.Infrastructure/Reflection/IPropertyAccessor.cs → backend/src/Squidex.Infrastructure/Reflection/IgnoreEqualsAttribute.cs

@ -1,16 +1,16 @@
// ========================================================================== // ==========================================================================
// Squidex Headless CMS // Squidex Headless CMS
// ========================================================================== // ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt) // Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
namespace Squidex.Infrastructure.Reflection namespace Squidex.Infrastructure.Reflection
{ {
public interface IPropertyAccessor [AttributeUsage(AttributeTargets.Property)]
public sealed class IgnoreEqualsAttribute : Attribute
{ {
object? Get(object target);
void Set(object target, object? value);
} }
} }

15
backend/src/Squidex.Infrastructure/Reflection/PropertyAccessor.cs → backend/src/Squidex.Infrastructure/Reflection/Internal/PropertyAccessor.cs

@ -8,10 +8,17 @@
using System; using System;
using System.Reflection; using System.Reflection;
namespace Squidex.Infrastructure.Reflection namespace Squidex.Infrastructure.Reflection.Internal
{ {
public sealed class PropertyAccessor : IPropertyAccessor public sealed class PropertyAccessor
{ {
private interface IPropertyAccessor
{
object? Get(object target);
void Set(object target, object? value);
}
private sealed class PropertyWrapper<TObject, TValue> : IPropertyAccessor private sealed class PropertyWrapper<TObject, TValue> : IPropertyAccessor
{ {
private readonly Func<TObject, TValue> getMethod; private readonly Func<TObject, TValue> getMethod;
@ -56,7 +63,9 @@ namespace Squidex.Infrastructure.Reflection
Guard.NotNull(targetType); Guard.NotNull(targetType);
Guard.NotNull(propertyInfo); Guard.NotNull(propertyInfo);
internalAccessor = (IPropertyAccessor)Activator.CreateInstance(typeof(PropertyWrapper<,>).MakeGenericType(propertyInfo.DeclaringType!, propertyInfo.PropertyType), propertyInfo)!; var type = typeof(PropertyWrapper<,>).MakeGenericType(propertyInfo.DeclaringType!, propertyInfo.PropertyType);
internalAccessor = (IPropertyAccessor)Activator.CreateInstance(type, propertyInfo)!;
} }
public object? Get(object target) public object? Get(object target)

0
backend/src/Squidex.Infrastructure/Reflection/ReflectionExtensions.cs → backend/src/Squidex.Infrastructure/Reflection/Internal/ReflectionExtensions.cs

78
backend/src/Squidex.Infrastructure/Reflection/PropertiesTypeAccessor.cs

@ -1,78 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Reflection;
namespace Squidex.Infrastructure.Reflection
{
public sealed class PropertiesTypeAccessor
{
private static readonly ConcurrentDictionary<Type, PropertiesTypeAccessor> AccessorCache = new ConcurrentDictionary<Type, PropertiesTypeAccessor>();
private readonly Dictionary<string, IPropertyAccessor> accessors = new Dictionary<string, IPropertyAccessor>();
private readonly List<PropertyInfo> properties = new List<PropertyInfo>();
public IEnumerable<PropertyInfo> Properties
{
get
{
return properties;
}
}
private PropertiesTypeAccessor(Type type)
{
var allProperties = type.GetProperties(BindingFlags.Instance | BindingFlags.Public);
foreach (var property in allProperties)
{
accessors[property.Name] = new PropertyAccessor(type, property);
properties.Add(property);
}
}
public static PropertiesTypeAccessor Create(Type targetType)
{
Guard.NotNull(targetType);
return AccessorCache.GetOrAdd(targetType, x => new PropertiesTypeAccessor(x));
}
public void SetValue(object target, string propertyName, object? value)
{
Guard.NotNull(target);
var accessor = FindAccessor(propertyName);
accessor.Set(target, value);
}
public object? GetValue(object target, string propertyName)
{
Guard.NotNull(target);
var accessor = FindAccessor(propertyName);
return accessor.Get(target);
}
private IPropertyAccessor FindAccessor(string propertyName)
{
Guard.NotNullOrEmpty(propertyName);
if (!accessors.TryGetValue(propertyName, out var accessor))
{
throw new ArgumentException("Property does not exist.", nameof(propertyName));
}
return accessor;
}
}
}

84
backend/src/Squidex.Infrastructure/Reflection/SimpleCopier.cs

@ -1,84 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
#pragma warning disable RECS0108 // Warns about static fields in generic types
namespace Squidex.Infrastructure.Reflection
{
public static class SimpleCopier
{
private struct PropertyMapper
{
private readonly IPropertyAccessor accessor;
private readonly Func<object?, object?> converter;
public PropertyMapper(IPropertyAccessor accessor, Func<object?, object?> converter)
{
this.accessor = accessor;
this.converter = converter;
}
public void MapProperty(object source, object target)
{
var value = converter(accessor.Get(source));
accessor.Set(target, value);
}
}
private static class ClassCopier<T> where T : class, new()
{
private static readonly List<PropertyMapper> Mappers = new List<PropertyMapper>();
static ClassCopier()
{
var type = typeof(T);
foreach (var property in type.GetPublicProperties())
{
if (!property.CanWrite || !property.CanRead)
{
continue;
}
var accessor = new PropertyAccessor(type, property);
if (property.PropertyType.Implements<ICloneable>())
{
Mappers.Add(new PropertyMapper(accessor, x => ((ICloneable)x!)?.Clone()));
}
else
{
Mappers.Add(new PropertyMapper(accessor, x => x));
}
}
}
public static T CopyThis(T source)
{
var destination = new T();
foreach (var mapper in Mappers)
{
mapper.MapProperty(source, destination);
}
return destination;
}
}
public static T Copy<T>(this T source) where T : class, new()
{
Guard.NotNull(source);
return ClassCopier<T>.CopyThis(source);
}
}
}

121
backend/src/Squidex.Infrastructure/Reflection/SimpleEquals.cs

@ -0,0 +1,121 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using Squidex.Infrastructure.Reflection.Equality;
using Squidex.Infrastructure.Reflection.Internal;
namespace Squidex.Infrastructure.Reflection
{
public static class SimpleEquals
{
private static readonly ConcurrentDictionary<Type, IDeepComparer> Comparers = new ConcurrentDictionary<Type, IDeepComparer>();
private static readonly DefaultComparer DefaultComparer = new DefaultComparer();
private static readonly NoopComparer NoopComparer = new NoopComparer();
internal static IDeepComparer Build(Type type)
{
return BuildCore(type) ?? DefaultComparer;
}
internal static IDeepComparer BuildInner(Type type)
{
return BuildCore(type) ?? NoopComparer;
}
private static IDeepComparer BuildCore(Type t)
{
return Comparers.GetOrAdd(t, type =>
{
if (IsSimpleType(type) || IsEquatable(type))
{
return NoopComparer;
}
if (IsArray(type))
{
var comparerType = typeof(ArrayComparer<>).MakeGenericType(type.GetElementType()!);
return (IDeepComparer)Activator.CreateInstance(comparerType, DefaultComparer)!;
}
if (IsSet(type))
{
var comparerType = typeof(SetComparer<>).MakeGenericType(type.GetGenericArguments());
return (IDeepComparer)Activator.CreateInstance(comparerType, DefaultComparer)!;
}
if (IsDictionary(type))
{
var comparerType = typeof(DictionaryComparer<,>).MakeGenericType(type.GetGenericArguments());
return (IDeepComparer)Activator.CreateInstance(comparerType, DefaultComparer)!;
}
if (IsCollection(type))
{
PropertyAccessor? count = null;
var countProperty = type.GetProperty("Count");
if (countProperty != null && countProperty.PropertyType == typeof(int))
{
count = new PropertyAccessor(type, countProperty);
}
return (IDeepComparer)Activator.CreateInstance(typeof(CollectionComparer), DefaultComparer, count)!;
}
return new ObjectComparer(DefaultComparer, type);
});
}
private static bool IsArray(Type type)
{
return type.IsArray;
}
private static bool IsCollection(Type type)
{
return type.GetInterfaces().Contains(typeof(IEnumerable));
}
private static bool IsSimpleType(Type type)
{
return type.IsValueType || type == typeof(string);
}
private static bool IsEquatable(Type type)
{
return type.GetInterfaces().Any(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(IEquatable<>));
}
private static bool IsSet(Type type)
{
return
type.GetGenericArguments().Length == 1 &&
type.GetInterfaces().Any(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(ISet<>));
}
private static bool IsDictionary(Type type)
{
return
type.GetGenericArguments().Length == 2 &&
type.GetInterfaces().Any(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(IReadOnlyDictionary<,>));
}
public static bool IsEquals<T>(T x, T y)
{
return DefaultComparer.IsEquals(x, y);
}
}
}

15
backend/src/Squidex.Infrastructure/Reflection/SimpleMapper.cs

@ -9,6 +9,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using Squidex.Infrastructure.Reflection.Internal;
#pragma warning disable RECS0108 // Warns about static fields in generic types #pragma warning disable RECS0108 // Warns about static fields in generic types
@ -19,8 +20,8 @@ namespace Squidex.Infrastructure.Reflection
private sealed class StringConversionPropertyMapper : PropertyMapper private sealed class StringConversionPropertyMapper : PropertyMapper
{ {
public StringConversionPropertyMapper( public StringConversionPropertyMapper(
IPropertyAccessor sourceAccessor, PropertyAccessor sourceAccessor,
IPropertyAccessor targetAccessor) PropertyAccessor targetAccessor)
: base(sourceAccessor, targetAccessor) : base(sourceAccessor, targetAccessor)
{ {
} }
@ -38,8 +39,8 @@ namespace Squidex.Infrastructure.Reflection
private readonly Type targetType; private readonly Type targetType;
public ConversionPropertyMapper( public ConversionPropertyMapper(
IPropertyAccessor sourceAccessor, PropertyAccessor sourceAccessor,
IPropertyAccessor targetAccessor, PropertyAccessor targetAccessor,
Type targetType) Type targetType)
: base(sourceAccessor, targetAccessor) : base(sourceAccessor, targetAccessor)
{ {
@ -70,10 +71,10 @@ namespace Squidex.Infrastructure.Reflection
private class PropertyMapper private class PropertyMapper
{ {
private readonly IPropertyAccessor sourceAccessor; private readonly PropertyAccessor sourceAccessor;
private readonly IPropertyAccessor targetAccessor; private readonly PropertyAccessor targetAccessor;
public PropertyMapper(IPropertyAccessor sourceAccessor, IPropertyAccessor targetAccessor) public PropertyMapper(PropertyAccessor sourceAccessor, PropertyAccessor targetAccessor)
{ {
this.sourceAccessor = sourceAccessor; this.sourceAccessor = sourceAccessor;
this.targetAccessor = targetAccessor; this.targetAccessor = targetAccessor;

4
backend/src/Squidex.Infrastructure/Security/Permission.cs

@ -9,7 +9,8 @@ using System;
namespace Squidex.Infrastructure.Security namespace Squidex.Infrastructure.Security
{ {
public sealed partial class Permission : IComparable<Permission>, IEquatable<Permission> [Equals(DoNotAddEqualityOperators = true)]
public sealed partial class Permission : IComparable<Permission>
{ {
public const string Any = "*"; public const string Any = "*";
public const string Exclude = "^"; public const string Exclude = "^";
@ -22,6 +23,7 @@ namespace Squidex.Infrastructure.Security
get { return id; } get { return id; }
} }
[IgnoreDuringEquals]
private Part[] Path private Part[] Path
{ {
get { return path ??= Part.ParsePath(id); } get { return path ??= Part.ParsePath(id); }

5
backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj

@ -9,7 +9,12 @@
<DebugSymbols>True</DebugSymbols> <DebugSymbols>True</DebugSymbols>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Equals.Fody" Version="1.9.5" />
<PackageReference Include="FluentFTP" Version="28.0.2" /> <PackageReference Include="FluentFTP" Version="28.0.2" />
<PackageReference Include="Fody" Version="4.2.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="McMaster.NETCore.Plugins" Version="0.3.1" /> <PackageReference Include="McMaster.NETCore.Plugins" Version="0.3.1" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="3.1.0" /> <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="3.1.0" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions" Version="3.1.0" /> <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions" Version="3.1.0" />

2
backend/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowTransitionDto.cs

@ -29,7 +29,7 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
public WorkflowTransition ToTransition() public WorkflowTransition ToTransition()
{ {
return new WorkflowTransition(Expression, Roles); return WorkflowTransition.When(Expression, Roles);
} }
} }
} }

2
backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RoleTests.cs

@ -72,7 +72,7 @@ namespace Squidex.Domain.Apps.Core.Model.Apps
{ {
var role = new Role("Custom"); var role = new Role("Custom");
Assert.False(role.Equals(null!)); Assert.False(role.Equals((string)null!));
Assert.False(role.Equals("Other")); Assert.False(role.Equals("Other"));
} }
} }

14
backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowsJsonTests.cs

@ -5,6 +5,8 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using System.Collections.Generic;
using FluentAssertions; using FluentAssertions;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Xunit; using Xunit;
@ -16,7 +18,17 @@ namespace Squidex.Domain.Apps.Core.Model.Contents
[Fact] [Fact]
public void Should_serialize_and_deserialize() public void Should_serialize_and_deserialize()
{ {
var workflow = Workflows.Empty.Set(Workflow.Default); var workflow = new Workflow(
Status.Draft, new Dictionary<Status, WorkflowStep>
{
[Status.Draft] = new WorkflowStep(
new Dictionary<Status, WorkflowTransition>
{
[Status.Published] = WorkflowTransition.When("Expression", "Role1", "Role2")
},
"#00ff00",
NoUpdate.When("Expression", "Role1", "Role2"))
}, new List<Guid> { Guid.NewGuid() }, "MyName");
var serialized = workflow.SerializeAndDeserialize(); var serialized = workflow.SerializeAndDeserialize();

2
backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaTests.cs

@ -32,7 +32,7 @@ namespace Squidex.Domain.Apps.Core.Model.Schemas
var lhs = (FieldProperties)Activator.CreateInstance(type)!; var lhs = (FieldProperties)Activator.CreateInstance(type)!;
var rhs = (FieldProperties)Activator.CreateInstance(type)!; var rhs = (FieldProperties)Activator.CreateInstance(type)!;
Assert.True(lhs.DeepEquals(rhs)); Assert.True(lhs.Equals(rhs));
} }
[Fact] [Fact]

56
backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppDomainObjectTests.cs

@ -94,7 +94,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
var result = await PublishAsync(command); var result = await PublishAsync(command);
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
Assert.Equal(AppName, sut.Snapshot.Name); Assert.Equal(AppName, sut.Snapshot.Name);
@ -114,7 +114,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
var result = await PublishAsync(command); var result = await PublishAsync(command);
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
Assert.Equal(AppName, sut.Snapshot.Name); Assert.Equal(AppName, sut.Snapshot.Name);
@ -135,7 +135,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
var result = await PublishIdempotentAsync(command); var result = await PublishIdempotentAsync(command);
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
Assert.Equal("my-label", sut.Snapshot.Label); Assert.Equal("my-label", sut.Snapshot.Label);
Assert.Equal("my-description", sut.Snapshot.Description); Assert.Equal("my-description", sut.Snapshot.Description);
@ -155,7 +155,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
var result = await PublishAsync(command); var result = await PublishAsync(command);
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
Assert.Equal("image/png", sut.Snapshot.Image!.MimeType); Assert.Equal("image/png", sut.Snapshot.Image!.MimeType);
@ -175,7 +175,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
var result = await PublishIdempotentAsync(command); var result = await PublishIdempotentAsync(command);
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
Assert.Null(sut.Snapshot.Image); Assert.Null(sut.Snapshot.Image);
@ -216,7 +216,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
var result = await PublishIdempotentAsync(command); var result = await PublishIdempotentAsync(command);
result.ShouldBeEquivalent2(new EntitySavedResult(4)); result.ShouldBeEquivalent(new EntitySavedResult(4));
Assert.Equal(planIdPaid, sut.Snapshot.Plan!.PlanId); Assert.Equal(planIdPaid, sut.Snapshot.Plan!.PlanId);
@ -242,7 +242,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
var result = await PublishIdempotentAsync(command); var result = await PublishIdempotentAsync(command);
result.ShouldBeEquivalent2(new EntitySavedResult(5)); result.ShouldBeEquivalent(new EntitySavedResult(5));
Assert.Null(sut.Snapshot.Plan); Assert.Null(sut.Snapshot.Plan);
@ -293,7 +293,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
var result = await PublishIdempotentAsync(command); var result = await PublishIdempotentAsync(command);
result.ShouldBeEquivalent2(new RedirectToCheckoutResult(new Uri("http://squidex.io"))); result.ShouldBeEquivalent(new RedirectToCheckoutResult(new Uri("http://squidex.io")));
Assert.Null(sut.Snapshot.Plan); Assert.Null(sut.Snapshot.Plan);
} }
@ -307,7 +307,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
var result = await PublishIdempotentAsync(command); var result = await PublishIdempotentAsync(command);
result.ShouldBeEquivalent2(new EntitySavedResult(4)); result.ShouldBeEquivalent(new EntitySavedResult(4));
A.CallTo(() => appPlansBillingManager.ChangePlanAsync(Actor.Identifier, AppNamedId, planIdPaid)) A.CallTo(() => appPlansBillingManager.ChangePlanAsync(Actor.Identifier, AppNamedId, planIdPaid))
.MustNotHaveHappened(); .MustNotHaveHappened();
@ -322,7 +322,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
var result = await PublishIdempotentAsync(command); var result = await PublishIdempotentAsync(command);
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
Assert.Equal(Role.Editor, sut.Snapshot.Contributors[contributorId]); Assert.Equal(Role.Editor, sut.Snapshot.Contributors[contributorId]);
@ -342,7 +342,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
var result = await PublishIdempotentAsync(command); var result = await PublishIdempotentAsync(command);
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
Assert.Equal(Role.Owner, sut.Snapshot.Contributors[contributorId]); Assert.Equal(Role.Owner, sut.Snapshot.Contributors[contributorId]);
@ -362,7 +362,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
var result = await PublishAsync(command); var result = await PublishAsync(command);
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
Assert.False(sut.Snapshot.Contributors.ContainsKey(contributorId)); Assert.False(sut.Snapshot.Contributors.ContainsKey(contributorId));
@ -381,7 +381,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
var result = await PublishAsync(command); var result = await PublishAsync(command);
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
Assert.True(sut.Snapshot.Clients.ContainsKey(clientId)); Assert.True(sut.Snapshot.Clients.ContainsKey(clientId));
@ -401,7 +401,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
var result = await PublishIdempotentAsync(command); var result = await PublishIdempotentAsync(command);
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
Assert.Equal(clientNewName, sut.Snapshot.Clients[clientId].Name); Assert.Equal(clientNewName, sut.Snapshot.Clients[clientId].Name);
@ -422,7 +422,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
var result = await PublishAsync(command); var result = await PublishAsync(command);
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
Assert.False(sut.Snapshot.Clients.ContainsKey(clientId)); Assert.False(sut.Snapshot.Clients.ContainsKey(clientId));
@ -441,7 +441,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
var result = await PublishAsync(command); var result = await PublishAsync(command);
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
Assert.NotEmpty(sut.Snapshot.Workflows); Assert.NotEmpty(sut.Snapshot.Workflows);
@ -461,7 +461,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
var result = await PublishIdempotentAsync(command); var result = await PublishIdempotentAsync(command);
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
Assert.NotEmpty(sut.Snapshot.Workflows); Assert.NotEmpty(sut.Snapshot.Workflows);
@ -481,7 +481,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
var result = await PublishAsync(command); var result = await PublishAsync(command);
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
Assert.Empty(sut.Snapshot.Workflows); Assert.Empty(sut.Snapshot.Workflows);
@ -500,7 +500,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
var result = await PublishAsync(command); var result = await PublishAsync(command);
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
Assert.True(sut.Snapshot.LanguagesConfig.Contains(Language.DE)); Assert.True(sut.Snapshot.LanguagesConfig.Contains(Language.DE));
@ -520,7 +520,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
var result = await PublishAsync(command); var result = await PublishAsync(command);
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
Assert.False(sut.Snapshot.LanguagesConfig.Contains(Language.DE)); Assert.False(sut.Snapshot.LanguagesConfig.Contains(Language.DE));
@ -540,7 +540,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
var result = await PublishIdempotentAsync(command); var result = await PublishIdempotentAsync(command);
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
Assert.True(sut.Snapshot.LanguagesConfig.Contains(Language.DE)); Assert.True(sut.Snapshot.LanguagesConfig.Contains(Language.DE));
@ -559,7 +559,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
var result = await PublishAsync(command); var result = await PublishAsync(command);
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
Assert.Equal(1, sut.Snapshot.Roles.CustomCount); Assert.Equal(1, sut.Snapshot.Roles.CustomCount);
@ -579,7 +579,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
var result = await PublishAsync(command); var result = await PublishAsync(command);
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
Assert.Equal(0, sut.Snapshot.Roles.CustomCount); Assert.Equal(0, sut.Snapshot.Roles.CustomCount);
@ -599,7 +599,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
var result = await PublishIdempotentAsync(command); var result = await PublishIdempotentAsync(command);
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
LastEvents LastEvents
.ShouldHaveSameEvents( .ShouldHaveSameEvents(
@ -616,7 +616,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
var result = await PublishAsync(command); var result = await PublishAsync(command);
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
Assert.Equal(initialPatterns.Count + 1, sut.Snapshot.Patterns.Count); Assert.Equal(initialPatterns.Count + 1, sut.Snapshot.Patterns.Count);
@ -636,7 +636,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
var result = await PublishAsync(command); var result = await PublishAsync(command);
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
Assert.Equal(initialPatterns.Count, sut.Snapshot.Patterns.Count); Assert.Equal(initialPatterns.Count, sut.Snapshot.Patterns.Count);
@ -656,7 +656,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
var result = await PublishIdempotentAsync(command); var result = await PublishIdempotentAsync(command);
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
LastEvents LastEvents
.ShouldHaveSameEvents( .ShouldHaveSameEvents(
@ -673,7 +673,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
var result = await PublishAsync(command); var result = await PublishAsync(command);
result.ShouldBeEquivalent2(new EntitySavedResult(4)); result.ShouldBeEquivalent(new EntitySavedResult(4));
LastEvents LastEvents
.ShouldHaveSameEvents( .ShouldHaveSameEvents(

18
backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetDomainObjectTests.cs

@ -67,7 +67,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
var result = await PublishAsync(command); var result = await PublishAsync(command);
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
Assert.Equal(0, sut.Snapshot.FileVersion); Assert.Equal(0, sut.Snapshot.FileVersion);
Assert.Equal(command.FileHash, sut.Snapshot.FileHash); Assert.Equal(command.FileHash, sut.Snapshot.FileHash);
@ -97,7 +97,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
var result = await PublishAsync(command); var result = await PublishAsync(command);
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
Assert.Equal(1, sut.Snapshot.FileVersion); Assert.Equal(1, sut.Snapshot.FileVersion);
Assert.Equal(command.FileHash, sut.Snapshot.FileHash); Assert.Equal(command.FileHash, sut.Snapshot.FileHash);
@ -124,7 +124,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
var result = await PublishIdempotentAsync(command); var result = await PublishIdempotentAsync(command);
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
Assert.Equal(command.FileName, sut.Snapshot.FileName); Assert.Equal(command.FileName, sut.Snapshot.FileName);
@ -143,7 +143,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
var result = await PublishIdempotentAsync(command); var result = await PublishIdempotentAsync(command);
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
Assert.Equal(command.Slug, sut.Snapshot.Slug); Assert.Equal(command.Slug, sut.Snapshot.Slug);
@ -162,7 +162,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
var result = await PublishIdempotentAsync(command); var result = await PublishIdempotentAsync(command);
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
Assert.Equal(command.IsProtected, sut.Snapshot.IsProtected); Assert.Equal(command.IsProtected, sut.Snapshot.IsProtected);
@ -181,7 +181,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
var result = await PublishIdempotentAsync(command); var result = await PublishIdempotentAsync(command);
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
Assert.Equal(command.Metadata, sut.Snapshot.Metadata); Assert.Equal(command.Metadata, sut.Snapshot.Metadata);
@ -200,7 +200,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
var result = await PublishIdempotentAsync(command); var result = await PublishIdempotentAsync(command);
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
LastEvents LastEvents
.ShouldHaveSameEvents( .ShouldHaveSameEvents(
@ -217,7 +217,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
var result = await PublishIdempotentAsync(command); var result = await PublishIdempotentAsync(command);
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
Assert.Equal(parentId, sut.Snapshot.ParentId); Assert.Equal(parentId, sut.Snapshot.ParentId);
@ -237,7 +237,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
var result = await PublishAsync(command); var result = await PublishAsync(command);
result.ShouldBeEquivalent2(new EntitySavedResult(2)); result.ShouldBeEquivalent(new EntitySavedResult(2));
Assert.True(sut.Snapshot.IsDeleted); Assert.True(sut.Snapshot.IsDeleted);

8
backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetFolderDomainObjectTests.cs

@ -57,7 +57,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
var result = await PublishAsync(command); var result = await PublishAsync(command);
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
Assert.Equal(command.FolderName, sut.Snapshot.FolderName); Assert.Equal(command.FolderName, sut.Snapshot.FolderName);
@ -79,7 +79,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
var result = await PublishIdempotentAsync(command); var result = await PublishIdempotentAsync(command);
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
Assert.Equal(command.FolderName, sut.Snapshot.FolderName); Assert.Equal(command.FolderName, sut.Snapshot.FolderName);
@ -98,7 +98,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
var result = await PublishIdempotentAsync(command); var result = await PublishIdempotentAsync(command);
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
Assert.Equal(parentId, sut.Snapshot.ParentId); Assert.Equal(parentId, sut.Snapshot.ParentId);
@ -117,7 +117,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
var result = await PublishAsync(command); var result = await PublishAsync(command);
result.ShouldBeEquivalent2(new EntitySavedResult(1)); result.ShouldBeEquivalent(new EntitySavedResult(1));
Assert.True(sut.Snapshot.IsDeleted); Assert.True(sut.Snapshot.IsDeleted);

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

@ -170,7 +170,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.MongoDb
var query = new ClrQuery { Take = 3 }; var query = new ClrQuery { Take = 3 };
var cursor = A.Fake<IFindFluent<MongoAssetEntity, MongoAssetEntity>>(); var cursor = A.Fake<IFindFluent<MongoAssetEntity, MongoAssetEntity>>();
cursor.AssetTake(query.AdjustToModel()); cursor.Take(query.AdjustToModel());
A.CallTo(() => cursor.Limit(3)) A.CallTo(() => cursor.Limit(3))
.MustHaveHappened(); .MustHaveHappened();
@ -182,7 +182,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.MongoDb
var query = new ClrQuery { Skip = 3 }; var query = new ClrQuery { Skip = 3 };
var cursor = A.Fake<IFindFluent<MongoAssetEntity, MongoAssetEntity>>(); var cursor = A.Fake<IFindFluent<MongoAssetEntity, MongoAssetEntity>>();
cursor.AssetSkip(query.AdjustToModel()); cursor.Skip(query.AdjustToModel());
A.CallTo(() => cursor.Skip(3)) A.CallTo(() => cursor.Skip(3))
.MustHaveHappened(); .MustHaveHappened();
@ -210,7 +210,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.MongoDb
i = sortDefinition.Render(Serializer, Registry).ToString(); i = sortDefinition.Render(Serializer, Registry).ToString();
}); });
cursor.AssetSort(new ClrQuery { Sort = sorts.ToList() }.AdjustToModel()); cursor.Sort(new ClrQuery { Sort = sorts.ToList() }.AdjustToModel());
return i; return i;
} }

6
backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/CommentsGrainTests.cs

@ -55,7 +55,7 @@ namespace Squidex.Domain.Apps.Entities.Comments
var result = await sut.ExecuteAsync(CreateCommentsCommand(command)); var result = await sut.ExecuteAsync(CreateCommentsCommand(command));
result.ShouldBeEquivalent(EntityCreatedResult.Create(command.CommentId, 0)); result.ShouldBeEquivalent((object)EntityCreatedResult.Create(command.CommentId, 0));
sut.GetCommentsAsync(0).Result.Should().BeEquivalentTo(new CommentsResult { Version = 0 }); sut.GetCommentsAsync(0).Result.Should().BeEquivalentTo(new CommentsResult { Version = 0 });
sut.GetCommentsAsync(-1).Result.Should().BeEquivalentTo(new CommentsResult sut.GetCommentsAsync(-1).Result.Should().BeEquivalentTo(new CommentsResult
@ -82,7 +82,7 @@ namespace Squidex.Domain.Apps.Entities.Comments
var result = await sut.ExecuteAsync(CreateCommentsCommand(updateCommand)); var result = await sut.ExecuteAsync(CreateCommentsCommand(updateCommand));
result.ShouldBeEquivalent(new EntitySavedResult(1)); result.ShouldBeEquivalent((object)new EntitySavedResult(1));
sut.GetCommentsAsync(-1).Result.Should().BeEquivalentTo(new CommentsResult sut.GetCommentsAsync(-1).Result.Should().BeEquivalentTo(new CommentsResult
{ {
@ -118,7 +118,7 @@ namespace Squidex.Domain.Apps.Entities.Comments
var result = await sut.ExecuteAsync(CreateCommentsCommand(deleteCommand)); var result = await sut.ExecuteAsync(CreateCommentsCommand(deleteCommand));
result.ShouldBeEquivalent(new EntitySavedResult(2)); result.ShouldBeEquivalent((object)new EntitySavedResult(2));
sut.GetCommentsAsync(-1).Result.Should().BeEquivalentTo(new CommentsResult { Version = 2 }); sut.GetCommentsAsync(-1).Result.Should().BeEquivalentTo(new CommentsResult { Version = 2 });
sut.GetCommentsAsync(0).Result.Should().BeEquivalentTo(new CommentsResult sut.GetCommentsAsync(0).Result.Should().BeEquivalentTo(new CommentsResult

34
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentDomainObjectTests.cs

@ -126,7 +126,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
var result = await PublishAsync(CreateContentCommand(command)); var result = await PublishAsync(CreateContentCommand(command));
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
Assert.Equal(Status.Draft, sut.Snapshot.Status); Assert.Equal(Status.Draft, sut.Snapshot.Status);
@ -148,7 +148,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
var result = await PublishAsync(CreateContentCommand(command)); var result = await PublishAsync(CreateContentCommand(command));
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
Assert.Equal(Status.Published, sut.Snapshot.Status); Assert.Equal(Status.Published, sut.Snapshot.Status);
@ -181,7 +181,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
var result = await PublishAsync(CreateContentCommand(command)); var result = await PublishAsync(CreateContentCommand(command));
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
LastEvents LastEvents
.ShouldHaveSameEvents( .ShouldHaveSameEvents(
@ -202,7 +202,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
var result = await PublishAsync(CreateContentCommand(command)); var result = await PublishAsync(CreateContentCommand(command));
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
Assert.True(sut.Snapshot.IsPending); Assert.True(sut.Snapshot.IsPending);
@ -224,7 +224,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
var result = await PublishAsync(CreateContentCommand(command)); var result = await PublishAsync(CreateContentCommand(command));
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
Assert.Single(LastEvents); Assert.Single(LastEvents);
@ -251,7 +251,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
var result = await PublishAsync(CreateContentCommand(command)); var result = await PublishAsync(CreateContentCommand(command));
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
LastEvents LastEvents
.ShouldHaveSameEvents( .ShouldHaveSameEvents(
@ -272,7 +272,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
var result = await PublishAsync(CreateContentCommand(command)); var result = await PublishAsync(CreateContentCommand(command));
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
Assert.True(sut.Snapshot.IsPending); Assert.True(sut.Snapshot.IsPending);
@ -294,7 +294,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
var result = await PublishAsync(CreateContentCommand(command)); var result = await PublishAsync(CreateContentCommand(command));
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
Assert.Single(LastEvents); Assert.Single(LastEvents);
@ -311,7 +311,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
var result = await PublishAsync(CreateContentCommand(command)); var result = await PublishAsync(CreateContentCommand(command));
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
Assert.Equal(Status.Published, sut.Snapshot.Status); Assert.Equal(Status.Published, sut.Snapshot.Status);
@ -333,7 +333,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
var result = await PublishAsync(CreateContentCommand(command)); var result = await PublishAsync(CreateContentCommand(command));
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
Assert.Equal(Status.Archived, sut.Snapshot.Status); Assert.Equal(Status.Archived, sut.Snapshot.Status);
@ -356,7 +356,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
var result = await PublishAsync(CreateContentCommand(command)); var result = await PublishAsync(CreateContentCommand(command));
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
Assert.Equal(Status.Draft, sut.Snapshot.Status); Assert.Equal(Status.Draft, sut.Snapshot.Status);
@ -379,7 +379,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
var result = await PublishAsync(CreateContentCommand(command)); var result = await PublishAsync(CreateContentCommand(command));
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
Assert.Equal(Status.Draft, sut.Snapshot.Status); Assert.Equal(Status.Draft, sut.Snapshot.Status);
@ -403,7 +403,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
var result = await PublishAsync(CreateContentCommand(command)); var result = await PublishAsync(CreateContentCommand(command));
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
Assert.False(sut.Snapshot.IsPending); Assert.False(sut.Snapshot.IsPending);
@ -427,7 +427,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
var result = await PublishAsync(CreateContentCommand(command)); var result = await PublishAsync(CreateContentCommand(command));
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
Assert.Equal(Status.Draft, sut.Snapshot.Status); Assert.Equal(Status.Draft, sut.Snapshot.Status);
Assert.Equal(Status.Published, sut.Snapshot.ScheduleJob!.Status); Assert.Equal(Status.Published, sut.Snapshot.ScheduleJob!.Status);
@ -455,7 +455,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
var result = await PublishAsync(CreateContentCommand(command)); var result = await PublishAsync(CreateContentCommand(command));
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
Assert.Null(sut.Snapshot.ScheduleJob); Assert.Null(sut.Snapshot.ScheduleJob);
@ -477,7 +477,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
var result = await PublishAsync(CreateContentCommand(command)); var result = await PublishAsync(CreateContentCommand(command));
result.ShouldBeEquivalent2(new EntitySavedResult(1)); result.ShouldBeEquivalent(new EntitySavedResult(1));
Assert.True(sut.Snapshot.IsDeleted); Assert.True(sut.Snapshot.IsDeleted);
@ -501,7 +501,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
var result = await PublishAsync(CreateContentCommand(command)); var result = await PublishAsync(CreateContentCommand(command));
result.ShouldBeEquivalent2(new EntitySavedResult(3)); result.ShouldBeEquivalent(new EntitySavedResult(3));
Assert.False(sut.Snapshot.IsPending); Assert.False(sut.Snapshot.IsPending);

9
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/MongoDbQueryTests.cs

@ -16,8 +16,9 @@ using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors;
using Squidex.Domain.Apps.Entities.MongoDb.Contents; using Squidex.Domain.Apps.Entities.MongoDb.Contents;
using Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors; using Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations;
using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.MongoDb; using Squidex.Infrastructure.MongoDb;
@ -276,7 +277,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
var query = new ClrQuery { Take = 3 }; var query = new ClrQuery { Take = 3 };
var cursor = A.Fake<IFindFluent<MongoContentEntity, MongoContentEntity>>(); var cursor = A.Fake<IFindFluent<MongoContentEntity, MongoContentEntity>>();
cursor.ContentTake(query.AdjustToModel(schemaDef, false)); cursor.Take(query);
A.CallTo(() => cursor.Limit(3)) A.CallTo(() => cursor.Limit(3))
.MustHaveHappened(); .MustHaveHappened();
@ -288,7 +289,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
var query = new ClrQuery { Skip = 3 }; var query = new ClrQuery { Skip = 3 };
var cursor = A.Fake<IFindFluent<MongoContentEntity, MongoContentEntity>>(); var cursor = A.Fake<IFindFluent<MongoContentEntity, MongoContentEntity>>();
cursor.ContentSkip(query.AdjustToModel(schemaDef, false)); cursor.Skip(query);
A.CallTo(() => cursor.Skip(3)) A.CallTo(() => cursor.Skip(3))
.MustHaveHappened(); .MustHaveHappened();
@ -316,7 +317,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
i = sortDefinition.Render(Serializer, Registry).ToString(); i = sortDefinition.Render(Serializer, Registry).ToString();
}); });
cursor.ContentSort(new ClrQuery { Sort = sorts.ToList() }.AdjustToModel(schemaDef, false)); cursor.Sort(new ClrQuery { Sort = sorts.ToList() }.AdjustToModel(schemaDef, false));
return i; return i;
} }

10
backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDomainObjectTests.cs

@ -61,7 +61,7 @@ namespace Squidex.Domain.Apps.Entities.Rules
var result = await PublishAsync(command); var result = await PublishAsync(command);
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
Assert.Equal(AppId, sut.Snapshot.AppId.Id); Assert.Equal(AppId, sut.Snapshot.AppId.Id);
@ -83,7 +83,7 @@ namespace Squidex.Domain.Apps.Entities.Rules
var result = await PublishIdempotentAsync(command); var result = await PublishIdempotentAsync(command);
result.ShouldBeEquivalent2(new EntitySavedResult(1)); result.ShouldBeEquivalent(new EntitySavedResult(1));
Assert.Same(command.Trigger, sut.Snapshot.RuleDef.Trigger); Assert.Same(command.Trigger, sut.Snapshot.RuleDef.Trigger);
Assert.Same(command.Action, sut.Snapshot.RuleDef.Action); Assert.Same(command.Action, sut.Snapshot.RuleDef.Action);
@ -106,7 +106,7 @@ namespace Squidex.Domain.Apps.Entities.Rules
var result = await PublishIdempotentAsync(command); var result = await PublishIdempotentAsync(command);
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
Assert.True(sut.Snapshot.RuleDef.IsEnabled); Assert.True(sut.Snapshot.RuleDef.IsEnabled);
@ -125,7 +125,7 @@ namespace Squidex.Domain.Apps.Entities.Rules
var result = await PublishIdempotentAsync(command); var result = await PublishIdempotentAsync(command);
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
Assert.False(sut.Snapshot.RuleDef.IsEnabled); Assert.False(sut.Snapshot.RuleDef.IsEnabled);
@ -144,7 +144,7 @@ namespace Squidex.Domain.Apps.Entities.Rules
var result = await PublishAsync(command); var result = await PublishAsync(command);
result.ShouldBeEquivalent2(new EntitySavedResult(1)); result.ShouldBeEquivalent(new EntitySavedResult(1));
Assert.True(sut.Snapshot.IsDeleted); Assert.True(sut.Snapshot.IsDeleted);

60
backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaDomainObjectTests.cs

@ -61,7 +61,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
var result = await PublishAsync(command); var result = await PublishAsync(command);
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
Assert.Equal(AppId, sut.Snapshot.AppId.Id); Assert.Equal(AppId, sut.Snapshot.AppId.Id);
@ -101,7 +101,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
var result = await PublishAsync(command); var result = await PublishAsync(command);
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
var @event = (SchemaCreated)LastEvents.Single().Payload; var @event = (SchemaCreated)LastEvents.Single().Payload;
@ -121,7 +121,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
var result = await PublishIdempotentAsync(command); var result = await PublishIdempotentAsync(command);
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
Assert.Equal(command.Properties, sut.Snapshot.SchemaDef.Properties); Assert.Equal(command.Properties, sut.Snapshot.SchemaDef.Properties);
@ -146,7 +146,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
var result = await PublishIdempotentAsync(command); var result = await PublishIdempotentAsync(command);
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
LastEvents LastEvents
.ShouldHaveSameEvents( .ShouldHaveSameEvents(
@ -167,7 +167,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
var result = await PublishIdempotentAsync(command); var result = await PublishIdempotentAsync(command);
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
Assert.Equal(command.FieldsInLists, sut.Snapshot.SchemaDef.FieldsInLists); Assert.Equal(command.FieldsInLists, sut.Snapshot.SchemaDef.FieldsInLists);
@ -190,7 +190,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
var result = await PublishIdempotentAsync(command); var result = await PublishIdempotentAsync(command);
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
Assert.Equal(command.FieldsInReferences, sut.Snapshot.SchemaDef.FieldsInReferences); Assert.Equal(command.FieldsInReferences, sut.Snapshot.SchemaDef.FieldsInReferences);
@ -209,7 +209,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
var result = await PublishIdempotentAsync(command); var result = await PublishIdempotentAsync(command);
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
Assert.True(sut.Snapshot.SchemaDef.IsPublished); Assert.True(sut.Snapshot.SchemaDef.IsPublished);
@ -229,7 +229,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
var result = await PublishIdempotentAsync(command); var result = await PublishIdempotentAsync(command);
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
Assert.False(sut.Snapshot.SchemaDef.IsPublished); Assert.False(sut.Snapshot.SchemaDef.IsPublished);
@ -248,7 +248,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
var result = await PublishIdempotentAsync(command); var result = await PublishIdempotentAsync(command);
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
Assert.Equal(command.Name, sut.Snapshot.SchemaDef.Category); Assert.Equal(command.Name, sut.Snapshot.SchemaDef.Category);
@ -273,7 +273,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
var result = await PublishIdempotentAsync(command); var result = await PublishIdempotentAsync(command);
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
Assert.Equal(command.PreviewUrls, sut.Snapshot.SchemaDef.PreviewUrls); Assert.Equal(command.PreviewUrls, sut.Snapshot.SchemaDef.PreviewUrls);
@ -292,7 +292,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
var result = await PublishAsync(command); var result = await PublishAsync(command);
result.ShouldBeEquivalent2(new EntitySavedResult(1)); result.ShouldBeEquivalent(new EntitySavedResult(1));
Assert.True(sut.Snapshot.IsDeleted); Assert.True(sut.Snapshot.IsDeleted);
@ -313,7 +313,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
var result = await PublishIdempotentAsync(command); var result = await PublishIdempotentAsync(command);
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
LastEvents LastEvents
.ShouldHaveSameEvents( .ShouldHaveSameEvents(
@ -333,7 +333,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
var result = await PublishIdempotentAsync(command); var result = await PublishIdempotentAsync(command);
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
LastEvents LastEvents
.ShouldHaveSameEvents( .ShouldHaveSameEvents(
@ -350,7 +350,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
var result = await PublishAsync(command); var result = await PublishAsync(command);
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
Assert.Equal(command.Properties, GetField(1).RawProperties); Assert.Equal(command.Properties, GetField(1).RawProperties);
@ -370,7 +370,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
var result = await PublishAsync(command); var result = await PublishAsync(command);
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
Assert.Same(command.Properties, GetNestedField(1, 2).RawProperties); Assert.Same(command.Properties, GetNestedField(1, 2).RawProperties);
@ -390,7 +390,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
var result = await PublishIdempotentAsync(command); var result = await PublishIdempotentAsync(command);
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
Assert.Equal(command.Properties, GetField(1).RawProperties); Assert.Equal(command.Properties, GetField(1).RawProperties);
@ -411,7 +411,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
var result = await PublishIdempotentAsync(command); var result = await PublishIdempotentAsync(command);
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
Assert.Same(command.Properties, GetNestedField(1, 2).RawProperties); Assert.Same(command.Properties, GetNestedField(1, 2).RawProperties);
@ -431,7 +431,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
var result = await PublishIdempotentAsync(command); var result = await PublishIdempotentAsync(command);
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
Assert.False(GetField(1).IsDisabled); Assert.False(GetField(1).IsDisabled);
@ -452,7 +452,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
var result = await PublishIdempotentAsync(command); var result = await PublishIdempotentAsync(command);
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
Assert.True(GetNestedField(1, 2).IsLocked); Assert.True(GetNestedField(1, 2).IsLocked);
@ -472,7 +472,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
var result = await PublishIdempotentAsync(command); var result = await PublishIdempotentAsync(command);
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
Assert.True(GetField(1).IsHidden); Assert.True(GetField(1).IsHidden);
@ -493,7 +493,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
var result = await PublishIdempotentAsync(command); var result = await PublishIdempotentAsync(command);
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
Assert.True(GetNestedField(1, 2).IsHidden); Assert.True(GetNestedField(1, 2).IsHidden);
@ -514,7 +514,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
var result = await PublishIdempotentAsync(command); var result = await PublishIdempotentAsync(command);
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
Assert.False(GetField(1).IsHidden); Assert.False(GetField(1).IsHidden);
@ -536,7 +536,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
var result = await PublishIdempotentAsync(command); var result = await PublishIdempotentAsync(command);
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
Assert.False(GetNestedField(1, 2).IsHidden); Assert.False(GetNestedField(1, 2).IsHidden);
@ -556,7 +556,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
var result = await PublishIdempotentAsync(command); var result = await PublishIdempotentAsync(command);
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
Assert.True(GetField(1).IsDisabled); Assert.True(GetField(1).IsDisabled);
@ -577,7 +577,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
var result = await PublishIdempotentAsync(command); var result = await PublishIdempotentAsync(command);
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
Assert.True(GetNestedField(1, 2).IsDisabled); Assert.True(GetNestedField(1, 2).IsDisabled);
@ -598,7 +598,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
var result = await PublishIdempotentAsync(command); var result = await PublishIdempotentAsync(command);
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
Assert.False(GetField(1).IsDisabled); Assert.False(GetField(1).IsDisabled);
@ -620,7 +620,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
var result = await PublishAsync(command); var result = await PublishAsync(command);
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
Assert.False(GetNestedField(1, 2).IsDisabled); Assert.False(GetNestedField(1, 2).IsDisabled);
@ -640,7 +640,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
var result = await PublishAsync(command); var result = await PublishAsync(command);
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
Assert.Null(GetField(1)); Assert.Null(GetField(1));
@ -661,7 +661,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
var result = await PublishAsync(command); var result = await PublishAsync(command);
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
Assert.Null(GetNestedField(1, 2)); Assert.Null(GetNestedField(1, 2));
@ -683,7 +683,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
var result = await PublishIdempotentAsync(command); var result = await PublishIdempotentAsync(command);
result.ShouldBeEquivalent2(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
Assert.Equal(command.Category, sut.Snapshot.SchemaDef.Category); Assert.Equal(command.Category, sut.Snapshot.SchemaDef.Category);

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save