diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClient.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClient.cs index 1a1b64193..8b5035f33 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClient.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClient.cs @@ -10,6 +10,7 @@ using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Core.Apps { + [Equals(DoNotAddEqualityOperators = true)] public sealed class AppClient : Named { public string Role { get; } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClients.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClients.cs index fbd7da0f4..943499b5d 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClients.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClients.cs @@ -71,7 +71,7 @@ namespace Squidex.Domain.Apps.Core.Apps return this; } - return With(id, client.Rename(newName), DeepComparer.Instance); + return With(id, client.Rename(newName)); } [Pure] @@ -84,7 +84,7 @@ namespace Squidex.Domain.Apps.Core.Apps return this; } - return With(id, client.Update(role), DeepComparer.Instance); + return With(id, client.Update(role)); } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppImage.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppImage.cs index 3d8c59d2c..0eb14e3db 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppImage.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppImage.cs @@ -9,6 +9,7 @@ using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Core.Apps { + [Equals(DoNotAddEqualityOperators = true)] public sealed class AppImage { public string MimeType { get; } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPattern.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPattern.cs index 967b891e7..99c53693e 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPattern.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPattern.cs @@ -10,6 +10,7 @@ using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Core.Apps { + [Equals(DoNotAddEqualityOperators = true)] public sealed class AppPattern : Named { public string Pattern { get; } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPatterns.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPatterns.cs index 5e3bb59cf..58403b473 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPatterns.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPatterns.cs @@ -56,7 +56,7 @@ namespace Squidex.Domain.Apps.Core.Apps return this; } - return With(id, appPattern.Update(name, pattern, message), DeepComparer.Instance); + return With(id, appPattern.Update(name, pattern, message)); } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPlan.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPlan.cs index c3ef0856e..d200b638d 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPlan.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPlan.cs @@ -9,6 +9,7 @@ using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Core.Apps { + [Equals(DoNotAddEqualityOperators = true)] public sealed class AppPlan { public RefToken Owner { get; } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/LanguageConfig.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/LanguageConfig.cs index a95a3673c..ef36d27aa 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/LanguageConfig.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/LanguageConfig.cs @@ -12,6 +12,7 @@ using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Core.Apps { + [Equals(DoNotAddEqualityOperators = true)] public sealed class LanguageConfig { public static readonly LanguageConfig Default = new LanguageConfig(); diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs index 7baa8db25..8d1f0befc 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs @@ -62,7 +62,7 @@ namespace Squidex.Domain.Apps.Core.Apps } [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); @@ -94,7 +94,7 @@ namespace Squidex.Domain.Apps.Core.Apps Cleanup(newLanguages, ref newMaster); - if (newLanguages.EqualsDictionary(languages, EqualityComparer.Default, DeepComparer.Instance) && Equals(newMaster, master)) + if (EqualLanguages(newLanguages) && Equals(newMaster, master)) { return this; } @@ -102,6 +102,11 @@ namespace Squidex.Domain.Apps.Core.Apps return new LanguagesConfig(newLanguages, newMaster); } + private bool EqualLanguages(Dictionary newLanguages) + { + return newLanguages.EqualsDictionary(languages); + } + private void Cleanup(Dictionary newLanguages, ref string newMaster) { if (!newLanguages.ContainsKey(newMaster)) diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Role.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Role.cs index 297f23ec4..2cabda6c0 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Role.cs +++ b/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 { + [Equals(DoNotAddEqualityOperators = true)] public sealed class Role : Named { public const string Editor = "Editor"; @@ -24,6 +25,7 @@ namespace Squidex.Domain.Apps.Core.Apps public PermissionSet Permissions { get; } + [IgnoreDuringEquals] public bool IsDefault { get { return Roles.IsDefault(this); } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Roles.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Roles.cs index 92b141cf1..0ee4fde5b 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Roles.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Roles.cs @@ -115,7 +115,7 @@ namespace Squidex.Domain.Apps.Core.Apps return this; } - return Create(inner.With(name, role.Update(permissions), DeepComparer.Instance)); + return Create(inner.With(name, role.Update(permissions))); } public static bool IsDefault(string role) diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/JsonWorkflowTransition.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/JsonWorkflowTransition.cs index ff43a3bf1..38f530115 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/JsonWorkflowTransition.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/JsonWorkflowTransition.cs @@ -5,21 +5,20 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Linq; using Newtonsoft.Json; -using Squidex.Infrastructure.Reflection; - namespace Squidex.Domain.Apps.Core.Contents.Json { public class JsonWorkflowTransition { [JsonProperty] - public string Expression { get; set; } + public string? Expression { get; set; } [JsonProperty] - public string Role { get; set; } + public string? Role { get; set; } [JsonProperty] - public string[] Roles { get; } + public string[]? Roles { get; set; } public JsonWorkflowTransition() { @@ -27,7 +26,9 @@ namespace Squidex.Domain.Apps.Core.Contents.Json public JsonWorkflowTransition(WorkflowTransition transition) { - SimpleMapper.Map(transition, this); + Roles = transition.Roles?.ToArray(); + + Expression = transition.Expression; } public WorkflowTransition ToTransition() @@ -39,7 +40,7 @@ namespace Squidex.Domain.Apps.Core.Contents.Json roles = new[] { Role }; } - return new WorkflowTransition(Expression, roles); + return WorkflowTransition.When(Expression, roles); } } } \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/NoUpdate.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/NoUpdate.cs index 243931568..1426aac11 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/NoUpdate.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/NoUpdate.cs @@ -5,20 +5,35 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Collections.ObjectModel; +using Squidex.Infrastructure.Collections; + namespace Squidex.Domain.Apps.Core.Contents { + [Equals(DoNotAddEqualityOperators = true)] public sealed class NoUpdate : WorkflowCondition { public static readonly NoUpdate Always = new NoUpdate(null, null); - public NoUpdate(string? expression, params string[]? roles) + public NoUpdate(string? expression, ReadOnlyCollection? roles) : base(expression, 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; + } } } } \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Status.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Status.cs index 0f2f2ac8b..a275cf051 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Status.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Status.cs @@ -7,11 +7,12 @@ using System; using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; namespace Squidex.Domain.Apps.Core.Contents { [TypeConverter(typeof(StatusConverter))] - public struct Status : IEquatable + public struct Status : IEquatable, IComparable { public static readonly Status Archived = new Status("Archived"); public static readonly Status Draft = new Status("Draft"); @@ -49,6 +50,11 @@ namespace Squidex.Domain.Apps.Core.Contents return Name; } + public int CompareTo([AllowNull] Status other) + { + return string.Compare(name, other.name, StringComparison.Ordinal); + } + public static bool operator ==(Status lhs, Status rhs) { return lhs.Equals(rhs); diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Workflow.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Workflow.cs index 068a98b4b..311467aad 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Workflow.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Workflow.cs @@ -8,9 +8,13 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using Squidex.Infrastructure; + +#pragma warning disable IDE0051 // Remove unused private members namespace Squidex.Domain.Apps.Core.Contents { + [Equals(DoNotAddEqualityOperators = true)] public sealed class Workflow : Named { 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 Empty = new Workflow(default, EmptySteps); + [IgnoreDuringEquals] public IReadOnlyDictionary Steps { get; } = EmptySteps; public IReadOnlyList SchemaIds { get; } = EmptySchemaIds; @@ -122,5 +127,17 @@ namespace Squidex.Domain.Apps.Core.Contents { return (Initial, Steps[Initial]); } + + [CustomEqualsInternal] + private bool CustomEquals(Workflow other) + { + return Steps.EqualsDictionary(other.Steps); + } + + [CustomGetHashCode] + private int CustomHashCode() + { + return Steps.DictionaryHashCode(); + } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowCondition.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowCondition.cs index 51838284f..108c84561 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowCondition.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowCondition.cs @@ -6,7 +6,6 @@ // ========================================================================== using System.Collections.ObjectModel; -using Squidex.Infrastructure.Collections; namespace Squidex.Domain.Apps.Core.Contents { @@ -16,14 +15,18 @@ namespace Squidex.Domain.Apps.Core.Contents public ReadOnlyCollection? Roles { get; } - protected WorkflowCondition(string? expression, params string[]? roles) + protected WorkflowCondition(string? expression, ReadOnlyCollection? roles) { Expression = expression; - if (roles != null) - { - Roles = ReadOnlyCollection.Create(roles); - } + Roles = roles; + } + + public override string ToString() + { + var roles = Roles?.Count > 0 ? string.Join(", ", Roles) : "*"; + + return $"When=${Expression}, For={roles}"; } } } \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowStep.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowStep.cs index 6564cd597..8a8d61312 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowStep.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowStep.cs @@ -6,13 +6,18 @@ // ========================================================================== using System.Collections.Generic; +using Squidex.Infrastructure; + +#pragma warning disable IDE0051 // Remove unused private members namespace Squidex.Domain.Apps.Core.Contents { + [Equals(DoNotAddEqualityOperators = true)] public sealed class WorkflowStep { private static readonly IReadOnlyDictionary EmptyTransitions = new Dictionary(); + [IgnoreDuringEquals] public IReadOnlyDictionary Transitions { get; } public string? Color { get; } @@ -27,5 +32,17 @@ namespace Squidex.Domain.Apps.Core.Contents NoUpdate = noUpdate; } + + [CustomEqualsInternal] + private bool CustomEquals(WorkflowStep other) + { + return Transitions.EqualsDictionary(other.Transitions); + } + + [CustomGetHashCode] + private int CustomHashCode() + { + return Transitions.DictionaryHashCode(); + } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowTransition.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowTransition.cs index a970fc7fa..c70527f41 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowTransition.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowTransition.cs @@ -5,20 +5,35 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Collections.ObjectModel; +using Squidex.Infrastructure.Collections; + namespace Squidex.Domain.Apps.Core.Contents { + [Equals(DoNotAddEqualityOperators = true)] public sealed class WorkflowTransition : WorkflowCondition { public static readonly WorkflowTransition Always = new WorkflowTransition(null, null); - public WorkflowTransition(string? expression, params string[]? roles) + public WorkflowTransition(string? expression, ReadOnlyCollection? roles) : base(expression, 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; + } } } } \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Workflows.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Workflows.cs index a285b2ac5..3e404a2b7 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Workflows.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Workflows.cs @@ -46,7 +46,7 @@ namespace Squidex.Domain.Apps.Core.Contents { Guard.NotNull(workflow); - return With(Guid.Empty, workflow, DeepComparer.Instance); + return With(Guid.Empty, workflow); } [Pure] @@ -54,7 +54,7 @@ namespace Squidex.Domain.Apps.Core.Contents { Guard.NotNull(workflow); - return With(id, workflow, DeepComparer.Instance); + return With(id, workflow); } [Pure] @@ -72,7 +72,7 @@ namespace Squidex.Domain.Apps.Core.Contents return this; } - return With(id, workflow, DeepComparer.Instance); + return With(id, workflow); } public Workflow GetFirst() diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/FodyWeavers.xml b/backend/src/Squidex.Domain.Apps.Core.Model/FodyWeavers.xml index e3c32ed72..22bdfa38f 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/FodyWeavers.xml +++ b/backend/src/Squidex.Domain.Apps.Core.Model/FodyWeavers.xml @@ -1,4 +1,6 @@  + + \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/FodyWeavers.xsd b/backend/src/Squidex.Domain.Apps.Core.Model/FodyWeavers.xsd index 74e66c0cc..b87d8baba 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/FodyWeavers.xsd +++ b/backend/src/Squidex.Domain.Apps.Core.Model/FodyWeavers.xsd @@ -4,6 +4,7 @@ + diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Freezable.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Freezable.cs index 9fc282430..855b2741b 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Freezable.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Freezable.cs @@ -7,13 +7,17 @@ using System; using Squidex.Infrastructure; +using Squidex.Infrastructure.Reflection; namespace Squidex.Domain.Apps.Core { + [Equals(DoNotAddEquals = true, DoNotAddGetHashCode = true, DoNotAddEqualityOperators = true)] public abstract class Freezable : IFreezable { private bool isFrozen; + [IgnoreEquals] + [IgnoreDuringEquals] public bool IsFrozen { get { return isFrozen; } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/RuleAction.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/RuleAction.cs index fe526f783..bd4f0edf5 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/RuleAction.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/RuleAction.cs @@ -8,7 +8,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; -using DeepEqual.Syntax; +using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Validation; namespace Squidex.Domain.Apps.Core.Rules @@ -41,7 +41,7 @@ namespace Squidex.Domain.Apps.Core.Rules public bool DeepEquals(RuleAction action) { - return this.WithDeepEqual(action).IgnoreProperty(x => x.IsFrozen).Compare(); + return SimpleEquals.IsEquals(this, action); } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/RuleTrigger.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/RuleTrigger.cs index 0c95b27be..a2979512d 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/RuleTrigger.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/RuleTrigger.cs @@ -5,7 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using DeepEqual.Syntax; +using Squidex.Infrastructure.Reflection; namespace Squidex.Domain.Apps.Core.Rules { @@ -15,7 +15,7 @@ namespace Squidex.Domain.Apps.Core.Rules public bool DeepEquals(RuleTrigger action) { - return this.WithDeepEqual(action).IgnoreProperty(x => x.IsFrozen).Compare(); + return SimpleEquals.IsEquals(this, action); } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayFieldProperties.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayFieldProperties.cs index c0feda7df..c4d7f99f3 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayFieldProperties.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayFieldProperties.cs @@ -9,6 +9,7 @@ using System; namespace Squidex.Domain.Apps.Core.Schemas { + [Equals(DoNotAddEqualityOperators = true)] public sealed class ArrayFieldProperties : FieldProperties { public int? MinItems { get; set; } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/AssetsFieldProperties.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/AssetsFieldProperties.cs index 542b72439..6a60ab8d6 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/AssetsFieldProperties.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/AssetsFieldProperties.cs @@ -9,6 +9,7 @@ using System.Collections.ObjectModel; namespace Squidex.Domain.Apps.Core.Schemas { + [Equals(DoNotAddEqualityOperators = true)] public sealed class AssetsFieldProperties : FieldProperties { public bool MustBeImage { get; set; } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeFieldProperties.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeFieldProperties.cs index f3615d121..a65529f0a 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeFieldProperties.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeFieldProperties.cs @@ -9,6 +9,7 @@ using NodaTime; namespace Squidex.Domain.Apps.Core.Schemas { + [Equals(DoNotAddEqualityOperators = true)] public sealed class DateTimeFieldProperties : FieldProperties { public Instant? MaxValue { get; set; } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldNames.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldNames.cs index 69f410fbf..1df458c5e 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldNames.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldNames.cs @@ -8,7 +8,6 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; -using DeepEqual.Syntax; namespace Squidex.Domain.Apps.Core.Schemas { @@ -45,10 +44,5 @@ namespace Squidex.Domain.Apps.Core.Schemas return new FieldNames(list); } - - public bool DeepEquals(FieldNames names) - { - return this.IsDeepEqual(names); - } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldProperties.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldProperties.cs index 17f7d2422..e0af1b252 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldProperties.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldProperties.cs @@ -6,10 +6,10 @@ // ========================================================================== using System.Collections.ObjectModel; -using DeepEqual.Syntax; namespace Squidex.Domain.Apps.Core.Schemas { + [Equals(DoNotAddEqualityOperators = true)] public abstract class FieldProperties : NamedElementPropertiesBase { 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 NestedField CreateNestedField(long id, string name, IFieldSettings? settings = null); - - public bool DeepEquals(FieldProperties properties) - { - return this.WithDeepEqual(properties).IgnoreProperty(x => x.IsFrozen).Compare(); - } } } \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationFieldProperties.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationFieldProperties.cs index c28ce7b29..7d3a8d344 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationFieldProperties.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationFieldProperties.cs @@ -7,6 +7,7 @@ namespace Squidex.Domain.Apps.Core.Schemas { + [Equals(DoNotAddEqualityOperators = true)] public sealed class GeolocationFieldProperties : FieldProperties { public GeolocationFieldEditor Editor { get; set; } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/JsonFieldProperties.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/JsonFieldProperties.cs index ae850ec31..5e86df1e0 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/JsonFieldProperties.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/JsonFieldProperties.cs @@ -7,6 +7,7 @@ namespace Squidex.Domain.Apps.Core.Schemas { + [Equals(DoNotAddEqualityOperators = true)] public sealed class JsonFieldProperties : FieldProperties { public override T Accept(IFieldPropertiesVisitor visitor) diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NestedField{T}.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NestedField{T}.cs index 63595e4c2..589244d42 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NestedField{T}.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NestedField{T}.cs @@ -38,7 +38,7 @@ namespace Squidex.Domain.Apps.Core.Schemas typedProperties.Freeze(); - if (properties.DeepEquals(typedProperties)) + if (properties.Equals(typedProperties)) { return this; } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldProperties.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldProperties.cs index f38cbe0c3..9a85d5d1a 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldProperties.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldProperties.cs @@ -9,6 +9,7 @@ using System.Collections.ObjectModel; namespace Squidex.Domain.Apps.Core.Schemas { + [Equals(DoNotAddEqualityOperators = true)] public sealed class NumberFieldProperties : FieldProperties { public ReadOnlyCollection? AllowedValues { get; set; } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldProperties.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldProperties.cs index 6b5ae3e84..ce06e0135 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldProperties.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldProperties.cs @@ -12,6 +12,7 @@ using System.Linq; namespace Squidex.Domain.Apps.Core.Schemas { + [Equals(DoNotAddEqualityOperators = true)] public sealed class ReferencesFieldProperties : FieldProperties { public int? MinItems { get; set; } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/RootField{T}.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/RootField{T}.cs index ebff4b87c..01adb3a15 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/RootField{T}.cs +++ b/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); - if (properties.DeepEquals(typedProperties)) + if (properties.Equals(typedProperties)) { return this; } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Schema.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Schema.cs index eef72a2ac..ab40c47c1 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Schema.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Schema.cs @@ -135,7 +135,7 @@ namespace Squidex.Domain.Apps.Core.Schemas { newScripts ??= new SchemaScripts(); - if (scripts.DeepEquals(newScripts)) + if (scripts.Equals(newScripts)) { return this; } @@ -152,7 +152,7 @@ namespace Squidex.Domain.Apps.Core.Schemas { names ??= FieldNames.Empty; - if (fieldsInLists.DeepEquals(names)) + if (fieldsInLists.SetEquals(names)) { return this; } @@ -174,7 +174,7 @@ namespace Squidex.Domain.Apps.Core.Schemas { names ??= FieldNames.Empty; - if (fieldsInReferences.DeepEquals(names)) + if (fieldsInReferences.SetEquals(names)) { return this; } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaProperties.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaProperties.cs index f9870f80b..c93ed3c9d 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaProperties.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaProperties.cs @@ -6,17 +6,18 @@ // ========================================================================== using System.Collections.ObjectModel; -using DeepEqual.Syntax; +using Squidex.Infrastructure.Reflection; namespace Squidex.Domain.Apps.Core.Schemas { + [Equals(DoNotAddEqualityOperators = true)] public sealed class SchemaProperties : NamedElementPropertiesBase { public ReadOnlyCollection Tags { get; set; } public bool DeepEquals(SchemaProperties properties) { - return this.WithDeepEqual(properties).IgnoreProperty(x => x.IsFrozen).Compare(); + return SimpleEquals.IsEquals(this, properties); } } } \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaScripts.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaScripts.cs index cda84a58c..1b07b617f 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaScripts.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaScripts.cs @@ -5,10 +5,9 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using DeepEqual.Syntax; - namespace Squidex.Domain.Apps.Core.Schemas { + [Equals(DoNotAddEqualityOperators =true)] public sealed class SchemaScripts : Freezable { public static readonly SchemaScripts Empty = new SchemaScripts(); @@ -27,10 +26,5 @@ namespace Squidex.Domain.Apps.Core.Schemas public string Delete { get; set; } public string Query { get; set; } - - public bool DeepEquals(SchemaScripts scripts) - { - return this.WithDeepEqual(scripts).IgnoreProperty(x => x.IsFrozen).Compare(); - } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldProperties.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldProperties.cs index d58770d83..eb398504e 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldProperties.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldProperties.cs @@ -9,6 +9,7 @@ using System.Collections.ObjectModel; namespace Squidex.Domain.Apps.Core.Schemas { + [Equals(DoNotAddEqualityOperators = true)] public sealed class StringFieldProperties : FieldProperties { public ReadOnlyCollection? AllowedValues { get; set; } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/UIFieldProperties.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/UIFieldProperties.cs index cd7741e8c..0aaa23a7c 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/UIFieldProperties.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/UIFieldProperties.cs @@ -7,6 +7,7 @@ namespace Squidex.Domain.Apps.Core.Schemas { + [Equals(DoNotAddEqualityOperators = true)] public sealed class UIFieldProperties : FieldProperties { public UIFieldEditor Editor { get; set; } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj b/backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj index 35e70070b..f7b65e5d2 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj @@ -10,9 +10,13 @@ True - - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizer.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizer.cs index 68b6a6179..8dec562d9 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizer.cs +++ b/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 }); } - if (!source.Scripts.DeepEquals(target.Scripts)) + if (!source.Scripts.Equals(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 (!sourceField.RawProperties.DeepEquals(targetField.RawProperties)) + if (!sourceField.RawProperties.Equals(targetField.RawProperties)) { yield return E(new FieldUpdated { FieldId = id, Properties = targetField.RawProperties }); } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs index 6017edfc7..81d8404ee 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs +++ b/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.Log; using Squidex.Infrastructure.MongoDb; +using Squidex.Infrastructure.MongoDb.Queries; using Squidex.Infrastructure.Queries; 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 assetItems = Collection.Find(filter) - .AssetTake(query) - .AssetSkip(query) - .AssetSort(query) + .Take(query) + .Skip(query) + .Sort(query) .ToListAsync(); await Task.WhenAll(assetItems, assetCount); diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FindExtensions.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FindExtensions.cs index ce349a0fc..703cae70f 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FindExtensions.cs +++ b/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; } - public static IFindFluent AssetSort(this IFindFluent cursor, ClrQuery query) - { - return cursor.Sort(query.BuildSort()); - } - - public static IFindFluent AssetTake(this IFindFluent cursor, ClrQuery query) - { - return cursor.Take(query); - } - - public static IFindFluent AssetSkip(this IFindFluent cursor, ClrQuery query) - { - return cursor.Skip(query); - } - public static FilterDefinition BuildFilter(this ClrQuery query, Guid appId, Guid? parentId) { var filters = new List> diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs deleted file mode 100644 index 3d95fb581..000000000 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs +++ /dev/null @@ -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 - { - 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 collection, CancellationToken ct = default) - { - return collection.Indexes.CreateManyAsync(new[] - { - new CreateIndexModel(Index - .Ascending(x => x.IndexedAppId) - .Ascending(x => x.IsDeleted) - .Ascending(x => x.Status) - .Descending(x => x.LastModified)), - new CreateIndexModel(Index - .Ascending(x => x.IndexedAppId) - .Ascending(x => x.IsDeleted) - .Ascending(x => x.Status) - .Ascending(x => x.Id)), - new CreateIndexModel(Index - .Ascending(x => x.IndexedSchemaId) - .Ascending(x => x.IsDeleted) - .Ascending(x => x.Status) - .Descending(x => x.LastModified)), - new CreateIndexModel(Index - .Ascending(x => x.IndexedSchemaId) - .Ascending(x => x.IsDeleted) - .Ascending(x => x.Status) - .Ascending(x => x.Id)), - new CreateIndexModel(Index - .Ascending(x => x.IndexedAppId) - .Ascending(x => x.Id)), - new CreateIndexModel(Index - .Ascending(x => x.ScheduledAt) - .Ascending(x => x.IsDeleted)), - new CreateIndexModel(Index - .Ascending(x => x.ReferencedIds)) - }, ct); - } - - protected override string CollectionName() - { - return "State_Contents"; - } - - public async Task> QueryAsync(ISchemaEntity schema, ClrQuery query, List? 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(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> QueryAsync(IAppEntity app, HashSet 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> QueryAsync(ISchemaEntity schema, HashSet 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(contentItems.Count, contentItems); - } - - public async Task 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 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> QueryIdsAsync(ISchemaEntity schema, FilterNode 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> QueryIdsAsync(Guid appId, HashSet 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> 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> 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 callback, Func> 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); - } - } -} diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs index d4a923d30..1c84d4426 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading; using System.Threading.Tasks; 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.Repositories; 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.Events.Assets; using Squidex.Domain.Apps.Events.Contents; using Squidex.Infrastructure; using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.MongoDb; using Squidex.Infrastructure.Queries; using Squidex.Infrastructure.Reflection; namespace Squidex.Domain.Apps.Entities.MongoDb.Contents { - public partial class MongoContentRepository : IContentRepository, IInitializable + public partial class MongoContentRepository : MongoRepositoryBase, IContentRepository { - private static readonly List<(Guid SchemaId, Guid Id)> EmptyIds = new List<(Guid SchemaId, Guid Id)>(); private readonly IAppProvider appProvider; private readonly IJsonSerializer serializer; - private readonly ITextIndexer indexer; private readonly string typeAssetDeleted; 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() { @@ -43,77 +49,79 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents } public MongoContentRepository(IMongoDatabase database, IAppProvider appProvider, IJsonSerializer serializer, ITextIndexer indexer, TypeNameRegistry typeNameRegistry) + : base(database) { Guard.NotNull(appProvider); Guard.NotNull(serializer); - Guard.NotNull(indexer); Guard.NotNull(typeNameRegistry); this.appProvider = appProvider; - this.indexer = indexer; + 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(); typeContentDeleted = typeNameRegistry.GetName(); + } - contents = new MongoContentCollection(database, serializer, appProvider); + protected override async Task SetupCollectionAsync(IMongoCollection 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> 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("QueryAsyncByQuery")) { - var fullTextIds = await indexer.SearchAsync(query.FullText, app, schema.Id, inDraft ? Scope.Draft : Scope.Published); - - if (fullTextIds?.Count == 0) - { - return ResultList.CreateFrom(0); - } - - return await contents.QueryAsync(schema, query, fullTextIds, status, inDraft, includeDraft); + return await queryContentsByQuery.DoAsync(app, schema, query, status, inDraft, includeDraft); } } public async Task> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[]? status, HashSet ids, bool includeDraft = true) { Guard.NotNull(app); - Guard.NotNull(ids); - Guard.NotNull(schema); using (Profiler.TraceMethod("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> QueryAsync(IAppEntity app, Status[]? status, HashSet ids, bool includeDraft = true) { Guard.NotNull(app); - Guard.NotNull(ids); using (Profiler.TraceMethod("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 FindContentAsync(IAppEntity app, ISchemaEntity schema, Status[]? status, Guid id, bool includeDraft = true) { - Guard.NotNull(app); - Guard.NotNull(schema); - using (Profiler.TraceMethod()) { - 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()) { - await contents.QueryScheduledWithoutDataAsync(now, callback); + await queryScheduledItems.DoAsync(now, callback); } } @@ -129,7 +137,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents { using (Profiler.TraceMethod()) { - 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()) { - var schema = await appProvider.GetSchemaAsync(appId, schemaId); - - if (schema == null) - { - return EmptyIds; - } - - return await contents.QueryIdsAsync(schema, filterNode); + return await queryIdsAsync.DoAsync(appId, schemaId, filterNode); } } - - public Task ClearAsync() - { - return contents.ClearAsync(); - } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_EventHandling.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_EventHandling.cs index 1c3a90369..8f381868b 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_EventHandling.cs +++ b/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) { case AssetDeleted e: - return contents.CleanupAsync(e.AssetId); + return cleanupReferences.DoAsync(e.AssetId); case ContentDeleted e: - return contents.CleanupAsync(e.ContentId); + return cleanupReferences.DoAsync(e.ContentId); } return TaskHelper.Done; diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs index c9196b081..7e0c2dddf 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs @@ -8,10 +8,12 @@ using System; using System.Threading; using System.Threading.Tasks; +using MongoDB.Driver; using Squidex.Domain.Apps.Entities.Contents.State; using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Infrastructure; using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.MongoDb; using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.States; @@ -19,19 +21,16 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents { public partial class MongoContentRepository : ISnapshotStore { - async Task ISnapshotStore.RemoveAsync(Guid key) + Task ISnapshotStore.ReadAllAsync(Func callback, CancellationToken ct) { - using (Profiler.TraceMethod()) - { - await contents.RemoveAsync(key); - } + throw new NotSupportedException(); } - async Task ISnapshotStore.ReadAllAsync(Func callback, CancellationToken ct) + async Task ISnapshotStore.RemoveAsync(Guid key) { using (Profiler.TraceMethod()) { - 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()) { - 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 }); - await contents.UpsertAsync(content, oldVersion); + await Collection.UpsertVersionedAsync(content.Id, oldVersion, content); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/Adapt.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/Adapt.cs similarity index 68% rename from backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/Adapt.cs rename to backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/Adapt.cs index 3d3f706b4..4f344fcb3 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/Adapt.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/Adapt.cs @@ -10,17 +10,31 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; using MongoDB.Bson.Serialization.Attributes; +using MongoDB.Driver; using Squidex.Domain.Apps.Core.GenerateEdmSchema; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure.Queries; -namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors +namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations { public static class Adapt { private static readonly Dictionary PropertyMap = typeof(MongoContentEntity).GetProperties() - .ToDictionary(x => x.Name, x => x.GetCustomAttribute()?.ElementName ?? x.Name, StringComparer.OrdinalIgnoreCase); + .ToDictionary( + x => ToElementName(x), + x => ToName(x), + StringComparer.OrdinalIgnoreCase); + + private static string ToName(PropertyInfo x) + { + return x.GetCustomAttribute()?.ElementName ?? x.Name; + } + + private static string ToElementName(PropertyInfo x) + { + return x.Name; + } public static Func Path(Schema schema, bool inDraft) { @@ -79,5 +93,26 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors 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? AdjustToModel(this FilterNode filterNode, Schema schema, bool useDraft) + { + var pathConverter = Path(schema, useDraft); + + return filterNode.Accept(new AdaptionVisitor(pathConverter)); + } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/AdaptionVisitor.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/AdaptionVisitor.cs similarity index 96% rename from backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/AdaptionVisitor.cs rename to backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/AdaptionVisitor.cs index 88e0409ac..88b785f18 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/AdaptionVisitor.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/AdaptionVisitor.cs @@ -11,7 +11,7 @@ using System.Linq; using NodaTime; 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 { diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/CleanupReferences.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/CleanupReferences.cs new file mode 100644 index 000000000..1174035b1 --- /dev/null +++ b/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( + 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)); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Extensions.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/Extensions.cs similarity index 78% rename from backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Extensions.cs rename to backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/Extensions.cs index e28b1e9de..24492802a 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Extensions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/Extensions.cs @@ -8,11 +8,13 @@ using System; using System.Collections.Generic; using System.Linq; +using MongoDB.Driver; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.ConvertContent; using Squidex.Domain.Apps.Core.ExtractReferenceIds; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure.Json; +using Squidex.Infrastructure.MongoDb; namespace Squidex.Domain.Apps.Entities.MongoDb.Contents { @@ -42,5 +44,15 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents FieldConverters.ForNestedName2Id( ValueConverters.EncodeJson(serializer))); } + + public static bool HasStatus(this MongoContentEntity content, Status[]? status) + { + return status == null || status.Contains(content.Status); + } + + public static IFindFluent WithoutDraft(this IFindFluent cursor, bool includeDraft) + { + return !includeDraft ? cursor.Not(x => x.DataDraftByIds, x => x.IsDeleted) : cursor; + } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/OperationBase.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/OperationBase.cs new file mode 100644 index 000000000..127948ec1 --- /dev/null +++ b/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 Sort = Builders.Sort; + protected static readonly UpdateDefinitionBuilder Update = Builders.Update; + protected static readonly FieldDefinitionBuilder Fields = FieldDefinitionBuilder.Instance; + protected static readonly FilterDefinitionBuilder Filter = Builders.Filter; + protected static readonly IndexKeysDefinitionBuilder Index = Builders.IndexKeys; + protected static readonly ProjectionDefinitionBuilder Projection = Builders.Projection; + + public IMongoCollection Collection { get; private set; } + + public Task PrepareAsync(IMongoCollection collection, CancellationToken ct = default) + { + Collection = collection; + + return PrepareAsync(ct); + } + + protected virtual Task PrepareAsync(CancellationToken ct = default) + { + return TaskHelper.Done; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryContent.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryContent.cs new file mode 100644 index 000000000..9b2ed9a83 --- /dev/null +++ b/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 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; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryContentsByIds.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryContentsByIds.cs new file mode 100644 index 000000000..c69ae207b --- /dev/null +++ b/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(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> DoAsync(Guid appId, ISchemaEntity? schema, HashSet 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> GetSchemasAsync(Guid appId, ISchemaEntity? schema, List contentItems) + { + var schemas = new Dictionary(); + + 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 CreateFilter(Guid appId, ICollection ids, Status[]? status) + { + var filters = new List> + { + 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); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryContentsByQuery.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryContentsByQuery.cs new file mode 100644 index 000000000..aafeaa609 --- /dev/null +++ b/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(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(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> 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? 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(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(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 CreateFilter(Guid schemaId, ICollection? ids, Status[]? status, ClrQuery? query) + { + var filters = new List> + { + 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()); + } + + return Filter.And(filters); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryIdsAsync.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryIdsAsync.cs new file mode 100644 index 000000000..0ef6b79e2 --- /dev/null +++ b/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(Index + .Ascending(x => x.IndexedAppId) + .Ascending(x => x.Id) + .Ascending(x => x.ReferencedIds)); + + var index2 = + new CreateIndexModel(Index + .Ascending(x => x.IndexedSchemaId)); + + return Collection.Indexes.CreateManyAsync(new[] { index1, index2, }, ct); + } + + public async Task> DoAsync(Guid appId, HashSet 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> DoAsync(Guid appId, Guid schemaId, FilterNode 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 BuildFilter(FilterNode? filterNode, Guid schemaId) + { + var filters = new List> + { + Filter.Eq(x => x.IndexedSchemaId, schemaId), + Filter.Ne(x => x.IsDeleted, true), + }; + + if (filterNode != null) + { + filters.Add(filterNode.BuildFilter()); + } + + return Filter.And(filters); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryScheduledContents.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryScheduledContents.cs new file mode 100644 index 000000000..3748a9437 --- /dev/null +++ b/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(Index + .Ascending(x => x.ScheduledAt) + .Ascending(x => x.IsDeleted)); + + return Collection.Indexes.CreateOneAsync(index, cancellationToken: ct); + } + + public Task DoAsync(Instant now, Func 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); + }); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FilterFactory.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FilterFactory.cs deleted file mode 100644 index 18c95a436..000000000 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FilterFactory.cs +++ /dev/null @@ -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 Filter = Builders.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? AdjustToModel(this FilterNode filterNode, Schema schema, bool useDraft) - { - var pathConverter = Adapt.Path(schema, useDraft); - - return filterNode.Accept(new AdaptionVisitor(pathConverter)); - } - - public static IFindFluent ContentSort(this IFindFluent cursor, ClrQuery query) - { - return cursor.Sort(query.BuildSort()); - } - - public static IFindFluent ContentTake(this IFindFluent cursor, ClrQuery query) - { - return cursor.Take(query); - } - - public static IFindFluent ContentSkip(this IFindFluent cursor, ClrQuery query) - { - return cursor.Skip(query); - } - - public static IFindFluent WithoutDraft(this IFindFluent cursor, bool includeDraft) - { - return !includeDraft ? cursor.Not(x => x.DataDraftByIds, x => x.IsDeleted) : cursor; - } - - public static FilterDefinition IdsByApp(Guid appId, ICollection ids, Status[]? status) - { - return CreateFilter(appId, null, ids, status, null); - } - - public static FilterDefinition IdsBySchema(Guid schemaId, ICollection ids, Status[]? status) - { - return CreateFilter(null, schemaId, ids, status, null); - } - - public static FilterDefinition ToFilter(this ClrQuery query, Guid schemaId, ICollection? ids, Status[]? status) - { - return CreateFilter(null, schemaId, ids, status, query); - } - - private static FilterDefinition CreateFilter(Guid? appId, Guid? schemaId, ICollection? ids, Status[]? status, - ClrQuery? query) - { - var filters = new List>(); - - 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()); - } - - return Filter.And(filters); - } - - public static FilterDefinition ToFilter(this FilterNode filterNode, Guid schemaId) - { - var filters = new List> - { - Filter.Eq(x => x.IndexedSchemaId, schemaId), - Filter.Ne(x => x.IsDeleted, true), - filterNode.BuildFilter() - }; - - return Filter.And(filters); - } - } -} diff --git a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs index 5b6bc6fe8..da3b04cc5 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs @@ -29,11 +29,19 @@ namespace Squidex.Infrastructure.MongoDb protected static readonly ProjectionDefinitionBuilder Projection = Builders.Projection; private readonly IMongoDatabase mongoDatabase; - private readonly Lazy> mongoCollection; + private IMongoCollection mongoCollection; protected IMongoCollection Collection { - get { return mongoCollection.Value; } + get + { + if (mongoCollection == null) + { + throw new InvalidOperationException("Collection has not been initialized yet."); + } + + return mongoCollection; + } } protected IMongoDatabase Database @@ -48,12 +56,16 @@ namespace Squidex.Infrastructure.MongoDb InstantSerializer.Register(); } - protected MongoRepositoryBase(IMongoDatabase database) + protected MongoRepositoryBase(IMongoDatabase database, bool setup = false) { Guard.NotNull(database); mongoDatabase = database; - mongoCollection = CreateCollection(); + + if (setup) + { + CreateCollection(); + } } protected virtual MongoCollectionSettings CollectionSettings() @@ -66,14 +78,6 @@ namespace Squidex.Infrastructure.MongoDb return string.Format(CultureInfo.InvariantCulture, CollectionFormat, typeof(TEntity).Name); } - private Lazy> CreateCollection() - { - return new Lazy>(() => - mongoDatabase.GetCollection( - CollectionName(), - CollectionSettings() ?? new MongoCollectionSettings())); - } - protected virtual Task SetupCollectionAsync(IMongoCollection collection, CancellationToken ct = default) { return TaskHelper.Done; @@ -81,15 +85,27 @@ namespace Squidex.Infrastructure.MongoDb 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) { try { + CreateCollection(); + await SetupCollectionAsync(Collection, ct); } 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); } } + + private void CreateCollection() + { + mongoCollection = mongoDatabase.GetCollection( + CollectionName(), + CollectionSettings() ?? new MongoCollectionSettings()); + } } } \ No newline at end of file diff --git a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/LimitExtensions.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/LimitExtensions.cs index 977641387..61459c95f 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/LimitExtensions.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/LimitExtensions.cs @@ -31,5 +31,10 @@ namespace Squidex.Infrastructure.MongoDb.Queries return cursor; } + + public static IFindFluent Sort(this IFindFluent cursor, ClrQuery query) + { + return cursor.Sort(query.BuildSort()); + } } } diff --git a/backend/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs b/backend/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs index 647324ea7..351dea3c1 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs @@ -20,11 +20,17 @@ namespace Squidex.Infrastructure.States public class MongoSnapshotStore : MongoRepositoryBase>, ISnapshotStore where TKey : notnull { public MongoSnapshotStore(IMongoDatabase database, JsonSerializer jsonSerializer) - : base(database) + : base(database, Register(jsonSerializer)) + { + } + + private static bool Register(JsonSerializer jsonSerializer) { Guard.NotNull(jsonSerializer); BsonJsonConvention.Register(jsonSerializer); + + return true; } protected override string CollectionName() diff --git a/backend/src/Squidex.Infrastructure/CollectionExtensions.cs b/backend/src/Squidex.Infrastructure/CollectionExtensions.cs index 3d7ef6d9a..e7e1f0a84 100644 --- a/backend/src/Squidex.Infrastructure/CollectionExtensions.cs +++ b/backend/src/Squidex.Infrastructure/CollectionExtensions.cs @@ -138,12 +138,12 @@ namespace Squidex.Infrastructure return hashCode; } - public static int DictionaryHashCode(this IDictionary dictionary) where TKey : notnull + public static int DictionaryHashCode(this IReadOnlyDictionary dictionary) where TKey : notnull { return DictionaryHashCode(dictionary, EqualityComparer.Default, EqualityComparer.Default); } - public static int DictionaryHashCode(this IDictionary dictionary, IEqualityComparer keyComparer, IEqualityComparer valueComparer) where TKey : notnull + public static int DictionaryHashCode(this IReadOnlyDictionary dictionary, IEqualityComparer keyComparer, IEqualityComparer valueComparer) where TKey : notnull { var hashCode = 17; diff --git a/backend/src/Squidex.Infrastructure/Collections/ImmutableDictionary{TKey,TValue}.cs b/backend/src/Squidex.Infrastructure/Collections/ImmutableDictionary{TKey,TValue}.cs index 05dc0ce04..a7458eddf 100644 --- a/backend/src/Squidex.Infrastructure/Collections/ImmutableDictionary{TKey,TValue}.cs +++ b/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? comparer = null) { - return comparer == null || comparer.Equals(lhs, rhs); + comparer ??= EqualityComparer.Default; + + return comparer.Equals(lhs, rhs); } public ImmutableDictionary Without(TKey key) diff --git a/backend/src/Squidex.Infrastructure/FodyWeavers.xml b/backend/src/Squidex.Infrastructure/FodyWeavers.xml new file mode 100644 index 000000000..1b04e09ca --- /dev/null +++ b/backend/src/Squidex.Infrastructure/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/backend/src/Squidex.Infrastructure/FodyWeavers.xsd b/backend/src/Squidex.Infrastructure/FodyWeavers.xsd new file mode 100644 index 000000000..f9b7ba295 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/FodyWeavers.xsd @@ -0,0 +1,26 @@ + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/backend/src/Squidex.Infrastructure/Guard.cs b/backend/src/Squidex.Infrastructure/Guard.cs index f98164c84..62e9971e3 100644 --- a/backend/src/Squidex.Infrastructure/Guard.cs +++ b/backend/src/Squidex.Infrastructure/Guard.cs @@ -8,7 +8,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Runtime.CompilerServices; @@ -74,7 +73,7 @@ namespace Squidex.Infrastructure [DebuggerStepThrough] [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) { diff --git a/backend/src/Squidex.Infrastructure/Language.cs b/backend/src/Squidex.Infrastructure/Language.cs index 9a86fb6ba..f4497c89a 100644 --- a/backend/src/Squidex.Infrastructure/Language.cs +++ b/backend/src/Squidex.Infrastructure/Language.cs @@ -12,6 +12,7 @@ using System.Text.RegularExpressions; namespace Squidex.Infrastructure { + [Equals(DoNotAddEqualityOperators = true)] public sealed partial class Language { 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; } } + [IgnoreDuringEquals] public string EnglishName { get; } public string Iso2Code { get; } diff --git a/backend/src/Squidex.Infrastructure/NamedId{T}.cs b/backend/src/Squidex.Infrastructure/NamedId{T}.cs index 42f15e61d..f32893219 100644 --- a/backend/src/Squidex.Infrastructure/NamedId{T}.cs +++ b/backend/src/Squidex.Infrastructure/NamedId{T}.cs @@ -14,7 +14,8 @@ namespace Squidex.Infrastructure { public delegate bool Parser(string input, out T result); - public sealed class NamedId : IEquatable> where T : notnull + [Equals(DoNotAddEqualityOperators = true)] + public sealed class NamedId where T : notnull { private static readonly int GuidLength = Guid.Empty.ToString().Length; @@ -37,21 +38,6 @@ namespace Squidex.Infrastructure return $"{Id},{Name}"; } - public override bool Equals(object? obj) - { - return Equals(obj as NamedId); - } - - public bool Equals(NamedId? 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 parser, [MaybeNullWhen(false)] out NamedId result) { if (value != null) diff --git a/backend/src/Squidex.Infrastructure/RefToken.cs b/backend/src/Squidex.Infrastructure/RefToken.cs index 1316019ae..01c8e3e44 100644 --- a/backend/src/Squidex.Infrastructure/RefToken.cs +++ b/backend/src/Squidex.Infrastructure/RefToken.cs @@ -10,17 +10,20 @@ using System.Diagnostics.CodeAnalysis; namespace Squidex.Infrastructure { - public sealed class RefToken : IEquatable + [Equals(DoNotAddEqualityOperators = true)] + public sealed class RefToken { public string Type { get; } public string Identifier { get; } + [IgnoreDuringEquals] public bool IsClient { get { return string.Equals(Type, RefTokenType.Client, StringComparison.OrdinalIgnoreCase); } } + [IgnoreDuringEquals] public bool IsSubject { get { return string.Equals(Type, RefTokenType.Subject, StringComparison.OrdinalIgnoreCase); } @@ -41,16 +44,6 @@ namespace Squidex.Infrastructure 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() { return (Type.GetHashCode() * 397) ^ Identifier.GetHashCode(); diff --git a/backend/src/Squidex.Infrastructure/Reflection/Equality/ArrayComparer.cs b/backend/src/Squidex.Infrastructure/Reflection/Equality/ArrayComparer.cs new file mode 100644 index 000000000..cb2f0461c --- /dev/null +++ b/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 : 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; + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Reflection/Equality/CollectionComparer.cs b/backend/src/Squidex.Infrastructure/Reflection/Equality/CollectionComparer.cs new file mode 100644 index 000000000..646e64a47 --- /dev/null +++ b/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; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/DeepComparer.cs b/backend/src/Squidex.Infrastructure/Reflection/Equality/DeepEqualityComparer.cs similarity index 56% rename from backend/src/Squidex.Domain.Apps.Core.Model/DeepComparer.cs rename to backend/src/Squidex.Infrastructure/Reflection/Equality/DeepEqualityComparer.cs index 15d1a8f90..3bb04aef1 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/DeepComparer.cs +++ b/backend/src/Squidex.Infrastructure/Reflection/Equality/DeepEqualityComparer.cs @@ -6,21 +6,22 @@ // ========================================================================== using System.Collections.Generic; -using DeepEqual.Syntax; -namespace Squidex.Domain.Apps.Core +namespace Squidex.Infrastructure.Reflection.Equality { - public sealed class DeepComparer : IEqualityComparer + public sealed class DeepEqualityComparer : IEqualityComparer { - public static readonly DeepComparer Instance = new DeepComparer(); + public static readonly DeepEqualityComparer Default = new DeepEqualityComparer(); + 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) { - return x.IsDeepEqual(y); + return comparer.IsEquals(x, y); } public int GetHashCode(T obj) diff --git a/backend/src/Squidex.Infrastructure/Reflection/Equality/DefaultComparer.cs b/backend/src/Squidex.Infrastructure/Reflection/Equality/DefaultComparer.cs new file mode 100644 index 000000000..6a48c944e --- /dev/null +++ b/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); + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Reflection/Equality/DictionaryComparer{TKey,TValue}.cs b/backend/src/Squidex.Infrastructure/Reflection/Equality/DictionaryComparer{TKey,TValue}.cs new file mode 100644 index 000000000..00fefdb1e --- /dev/null +++ b/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 : IDeepComparer where TKey : notnull + { + private readonly IEqualityComparer> comparer; + + public DictionaryComparer(IDeepComparer comparer) + { + this.comparer = new CollectionExtensions.KeyValuePairComparer( + new DeepEqualityComparer(comparer), + new DeepEqualityComparer(comparer)); + } + + public bool IsEquals(object? x, object? y) + { + var lhs = (IReadOnlyDictionary)x!; + var rhs = (IReadOnlyDictionary)y!; + + if (lhs.Count != rhs.Count) + { + return false; + } + + return !lhs.Except(rhs, comparer).Any(); + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Reflection/Equality/IDeepComparer.cs b/backend/src/Squidex.Infrastructure/Reflection/Equality/IDeepComparer.cs new file mode 100644 index 000000000..c25fa57e0 --- /dev/null +++ b/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); + } +} diff --git a/backend/src/Squidex.Infrastructure/Reflection/Equality/NoopComparer.cs b/backend/src/Squidex.Infrastructure/Reflection/Equality/NoopComparer.cs new file mode 100644 index 000000000..138c1977d --- /dev/null +++ b/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; + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Reflection/Equality/ObjectComparer.cs b/backend/src/Squidex.Infrastructure/Reflection/Equality/ObjectComparer.cs new file mode 100644 index 000000000..de6f3aec6 --- /dev/null +++ b/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() == 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; + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Reflection/Equality/SetComparer.cs b/backend/src/Squidex.Infrastructure/Reflection/Equality/SetComparer.cs new file mode 100644 index 000000000..df933e763 --- /dev/null +++ b/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 : IDeepComparer + { + private readonly IEqualityComparer equalityComparer; + + public SetComparer(IDeepComparer comparer) + { + equalityComparer = new DeepEqualityComparer(comparer); + } + + public bool IsEquals(object? x, object? y) + { + var lhs = (ISet)x!; + var rhs = (ISet)y!; + + if (lhs.Count != rhs.Count) + { + return false; + } + + return lhs.Intersect(rhs, equalityComparer).Count() == rhs.Count; + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Reflection/IPropertyAccessor.cs b/backend/src/Squidex.Infrastructure/Reflection/IgnoreEqualsAttribute.cs similarity index 68% rename from backend/src/Squidex.Infrastructure/Reflection/IPropertyAccessor.cs rename to backend/src/Squidex.Infrastructure/Reflection/IgnoreEqualsAttribute.cs index 314921b2c..967360e4e 100644 --- a/backend/src/Squidex.Infrastructure/Reflection/IPropertyAccessor.cs +++ b/backend/src/Squidex.Infrastructure/Reflection/IgnoreEqualsAttribute.cs @@ -1,16 +1,16 @@ // ========================================================================== // Squidex Headless CMS // ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) +// Copyright (c) Squidex UG (haftungsbeschraenkt) // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; + 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); } } diff --git a/backend/src/Squidex.Infrastructure/Reflection/PropertyAccessor.cs b/backend/src/Squidex.Infrastructure/Reflection/Internal/PropertyAccessor.cs similarity index 85% rename from backend/src/Squidex.Infrastructure/Reflection/PropertyAccessor.cs rename to backend/src/Squidex.Infrastructure/Reflection/Internal/PropertyAccessor.cs index e73ce8816..01de599bb 100644 --- a/backend/src/Squidex.Infrastructure/Reflection/PropertyAccessor.cs +++ b/backend/src/Squidex.Infrastructure/Reflection/Internal/PropertyAccessor.cs @@ -8,10 +8,17 @@ using System; 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 : IPropertyAccessor { private readonly Func getMethod; @@ -56,7 +63,9 @@ namespace Squidex.Infrastructure.Reflection Guard.NotNull(targetType); 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) diff --git a/backend/src/Squidex.Infrastructure/Reflection/ReflectionExtensions.cs b/backend/src/Squidex.Infrastructure/Reflection/Internal/ReflectionExtensions.cs similarity index 100% rename from backend/src/Squidex.Infrastructure/Reflection/ReflectionExtensions.cs rename to backend/src/Squidex.Infrastructure/Reflection/Internal/ReflectionExtensions.cs diff --git a/backend/src/Squidex.Infrastructure/Reflection/PropertiesTypeAccessor.cs b/backend/src/Squidex.Infrastructure/Reflection/PropertiesTypeAccessor.cs deleted file mode 100644 index 9d3e2756b..000000000 --- a/backend/src/Squidex.Infrastructure/Reflection/PropertiesTypeAccessor.cs +++ /dev/null @@ -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 AccessorCache = new ConcurrentDictionary(); - private readonly Dictionary accessors = new Dictionary(); - private readonly List properties = new List(); - - public IEnumerable 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; - } - } -} diff --git a/backend/src/Squidex.Infrastructure/Reflection/SimpleCopier.cs b/backend/src/Squidex.Infrastructure/Reflection/SimpleCopier.cs deleted file mode 100644 index 4bb7acccb..000000000 --- a/backend/src/Squidex.Infrastructure/Reflection/SimpleCopier.cs +++ /dev/null @@ -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 converter; - - public PropertyMapper(IPropertyAccessor accessor, Func 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 where T : class, new() - { - private static readonly List Mappers = new List(); - - 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()) - { - 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(this T source) where T : class, new() - { - Guard.NotNull(source); - - return ClassCopier.CopyThis(source); - } - } -} diff --git a/backend/src/Squidex.Infrastructure/Reflection/SimpleEquals.cs b/backend/src/Squidex.Infrastructure/Reflection/SimpleEquals.cs new file mode 100644 index 000000000..63d928b21 --- /dev/null +++ b/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 Comparers = new ConcurrentDictionary(); + 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 x, T y) + { + return DefaultComparer.IsEquals(x, y); + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Reflection/SimpleMapper.cs b/backend/src/Squidex.Infrastructure/Reflection/SimpleMapper.cs index 5685b148c..ca51d78e3 100644 --- a/backend/src/Squidex.Infrastructure/Reflection/SimpleMapper.cs +++ b/backend/src/Squidex.Infrastructure/Reflection/SimpleMapper.cs @@ -9,6 +9,7 @@ using System; using System.Collections.Generic; using System.Globalization; using System.Linq; +using Squidex.Infrastructure.Reflection.Internal; #pragma warning disable RECS0108 // Warns about static fields in generic types @@ -19,8 +20,8 @@ namespace Squidex.Infrastructure.Reflection private sealed class StringConversionPropertyMapper : PropertyMapper { public StringConversionPropertyMapper( - IPropertyAccessor sourceAccessor, - IPropertyAccessor targetAccessor) + PropertyAccessor sourceAccessor, + PropertyAccessor targetAccessor) : base(sourceAccessor, targetAccessor) { } @@ -38,8 +39,8 @@ namespace Squidex.Infrastructure.Reflection private readonly Type targetType; public ConversionPropertyMapper( - IPropertyAccessor sourceAccessor, - IPropertyAccessor targetAccessor, + PropertyAccessor sourceAccessor, + PropertyAccessor targetAccessor, Type targetType) : base(sourceAccessor, targetAccessor) { @@ -70,10 +71,10 @@ namespace Squidex.Infrastructure.Reflection private class PropertyMapper { - private readonly IPropertyAccessor sourceAccessor; - private readonly IPropertyAccessor targetAccessor; + private readonly PropertyAccessor sourceAccessor; + private readonly PropertyAccessor targetAccessor; - public PropertyMapper(IPropertyAccessor sourceAccessor, IPropertyAccessor targetAccessor) + public PropertyMapper(PropertyAccessor sourceAccessor, PropertyAccessor targetAccessor) { this.sourceAccessor = sourceAccessor; this.targetAccessor = targetAccessor; diff --git a/backend/src/Squidex.Infrastructure/Security/Permission.cs b/backend/src/Squidex.Infrastructure/Security/Permission.cs index df2840802..27eb33d04 100644 --- a/backend/src/Squidex.Infrastructure/Security/Permission.cs +++ b/backend/src/Squidex.Infrastructure/Security/Permission.cs @@ -9,7 +9,8 @@ using System; namespace Squidex.Infrastructure.Security { - public sealed partial class Permission : IComparable, IEquatable + [Equals(DoNotAddEqualityOperators = true)] + public sealed partial class Permission : IComparable { public const string Any = "*"; public const string Exclude = "^"; @@ -22,6 +23,7 @@ namespace Squidex.Infrastructure.Security get { return id; } } + [IgnoreDuringEquals] private Part[] Path { get { return path ??= Part.ParsePath(id); } diff --git a/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj b/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj index 9fab91937..31412c5bc 100644 --- a/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj +++ b/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj @@ -9,7 +9,12 @@ True + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowTransitionDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowTransitionDto.cs index 4a556a53b..f6380f139 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowTransitionDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowTransitionDto.cs @@ -29,7 +29,7 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models public WorkflowTransition ToTransition() { - return new WorkflowTransition(Expression, Roles); + return WorkflowTransition.When(Expression, Roles); } } } \ No newline at end of file diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RoleTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RoleTests.cs index 2a3f984c0..2d458821c 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RoleTests.cs +++ b/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"); - Assert.False(role.Equals(null!)); + Assert.False(role.Equals((string)null!)); Assert.False(role.Equals("Other")); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowsJsonTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowsJsonTests.cs index 52074fee1..444fcbda2 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowsJsonTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowsJsonTests.cs @@ -5,6 +5,8 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; +using System.Collections.Generic; using FluentAssertions; using Squidex.Domain.Apps.Core.Contents; using Xunit; @@ -16,7 +18,17 @@ namespace Squidex.Domain.Apps.Core.Model.Contents [Fact] public void Should_serialize_and_deserialize() { - var workflow = Workflows.Empty.Set(Workflow.Default); + var workflow = new Workflow( + Status.Draft, new Dictionary + { + [Status.Draft] = new WorkflowStep( + new Dictionary + { + [Status.Published] = WorkflowTransition.When("Expression", "Role1", "Role2") + }, + "#00ff00", + NoUpdate.When("Expression", "Role1", "Role2")) + }, new List { Guid.NewGuid() }, "MyName"); var serialized = workflow.SerializeAndDeserialize(); diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaTests.cs index bec691665..3dc2ad272 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaTests.cs +++ b/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 rhs = (FieldProperties)Activator.CreateInstance(type)!; - Assert.True(lhs.DeepEquals(rhs)); + Assert.True(lhs.Equals(rhs)); } [Fact] diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppDomainObjectTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppDomainObjectTests.cs index 43748f092..36e161da5 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppDomainObjectTests.cs +++ b/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); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(AppName, sut.Snapshot.Name); @@ -114,7 +114,7 @@ namespace Squidex.Domain.Apps.Entities.Apps var result = await PublishAsync(command); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(AppName, sut.Snapshot.Name); @@ -135,7 +135,7 @@ namespace Squidex.Domain.Apps.Entities.Apps var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); Assert.Equal("my-label", sut.Snapshot.Label); Assert.Equal("my-description", sut.Snapshot.Description); @@ -155,7 +155,7 @@ namespace Squidex.Domain.Apps.Entities.Apps var result = await PublishAsync(command); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); Assert.Equal("image/png", sut.Snapshot.Image!.MimeType); @@ -175,7 +175,7 @@ namespace Squidex.Domain.Apps.Entities.Apps var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); Assert.Null(sut.Snapshot.Image); @@ -216,7 +216,7 @@ namespace Squidex.Domain.Apps.Entities.Apps var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent2(new EntitySavedResult(4)); + result.ShouldBeEquivalent(new EntitySavedResult(4)); Assert.Equal(planIdPaid, sut.Snapshot.Plan!.PlanId); @@ -242,7 +242,7 @@ namespace Squidex.Domain.Apps.Entities.Apps var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent2(new EntitySavedResult(5)); + result.ShouldBeEquivalent(new EntitySavedResult(5)); Assert.Null(sut.Snapshot.Plan); @@ -293,7 +293,7 @@ namespace Squidex.Domain.Apps.Entities.Apps 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); } @@ -307,7 +307,7 @@ namespace Squidex.Domain.Apps.Entities.Apps var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent2(new EntitySavedResult(4)); + result.ShouldBeEquivalent(new EntitySavedResult(4)); A.CallTo(() => appPlansBillingManager.ChangePlanAsync(Actor.Identifier, AppNamedId, planIdPaid)) .MustNotHaveHappened(); @@ -322,7 +322,7 @@ namespace Squidex.Domain.Apps.Entities.Apps var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(Role.Editor, sut.Snapshot.Contributors[contributorId]); @@ -342,7 +342,7 @@ namespace Squidex.Domain.Apps.Entities.Apps var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(Role.Owner, sut.Snapshot.Contributors[contributorId]); @@ -362,7 +362,7 @@ namespace Squidex.Domain.Apps.Entities.Apps var result = await PublishAsync(command); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); Assert.False(sut.Snapshot.Contributors.ContainsKey(contributorId)); @@ -381,7 +381,7 @@ namespace Squidex.Domain.Apps.Entities.Apps var result = await PublishAsync(command); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); Assert.True(sut.Snapshot.Clients.ContainsKey(clientId)); @@ -401,7 +401,7 @@ namespace Squidex.Domain.Apps.Entities.Apps var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(clientNewName, sut.Snapshot.Clients[clientId].Name); @@ -422,7 +422,7 @@ namespace Squidex.Domain.Apps.Entities.Apps var result = await PublishAsync(command); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); Assert.False(sut.Snapshot.Clients.ContainsKey(clientId)); @@ -441,7 +441,7 @@ namespace Squidex.Domain.Apps.Entities.Apps var result = await PublishAsync(command); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); Assert.NotEmpty(sut.Snapshot.Workflows); @@ -461,7 +461,7 @@ namespace Squidex.Domain.Apps.Entities.Apps var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); Assert.NotEmpty(sut.Snapshot.Workflows); @@ -481,7 +481,7 @@ namespace Squidex.Domain.Apps.Entities.Apps var result = await PublishAsync(command); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); Assert.Empty(sut.Snapshot.Workflows); @@ -500,7 +500,7 @@ namespace Squidex.Domain.Apps.Entities.Apps var result = await PublishAsync(command); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); Assert.True(sut.Snapshot.LanguagesConfig.Contains(Language.DE)); @@ -520,7 +520,7 @@ namespace Squidex.Domain.Apps.Entities.Apps var result = await PublishAsync(command); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); Assert.False(sut.Snapshot.LanguagesConfig.Contains(Language.DE)); @@ -540,7 +540,7 @@ namespace Squidex.Domain.Apps.Entities.Apps var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); Assert.True(sut.Snapshot.LanguagesConfig.Contains(Language.DE)); @@ -559,7 +559,7 @@ namespace Squidex.Domain.Apps.Entities.Apps var result = await PublishAsync(command); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(1, sut.Snapshot.Roles.CustomCount); @@ -579,7 +579,7 @@ namespace Squidex.Domain.Apps.Entities.Apps var result = await PublishAsync(command); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(0, sut.Snapshot.Roles.CustomCount); @@ -599,7 +599,7 @@ namespace Squidex.Domain.Apps.Entities.Apps var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); LastEvents .ShouldHaveSameEvents( @@ -616,7 +616,7 @@ namespace Squidex.Domain.Apps.Entities.Apps var result = await PublishAsync(command); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(initialPatterns.Count + 1, sut.Snapshot.Patterns.Count); @@ -636,7 +636,7 @@ namespace Squidex.Domain.Apps.Entities.Apps var result = await PublishAsync(command); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(initialPatterns.Count, sut.Snapshot.Patterns.Count); @@ -656,7 +656,7 @@ namespace Squidex.Domain.Apps.Entities.Apps var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); LastEvents .ShouldHaveSameEvents( @@ -673,7 +673,7 @@ namespace Squidex.Domain.Apps.Entities.Apps var result = await PublishAsync(command); - result.ShouldBeEquivalent2(new EntitySavedResult(4)); + result.ShouldBeEquivalent(new EntitySavedResult(4)); LastEvents .ShouldHaveSameEvents( diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetDomainObjectTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetDomainObjectTests.cs index f89a1987d..90523a6f3 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetDomainObjectTests.cs +++ b/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); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(0, sut.Snapshot.FileVersion); Assert.Equal(command.FileHash, sut.Snapshot.FileHash); @@ -97,7 +97,7 @@ namespace Squidex.Domain.Apps.Entities.Assets var result = await PublishAsync(command); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(1, sut.Snapshot.FileVersion); Assert.Equal(command.FileHash, sut.Snapshot.FileHash); @@ -124,7 +124,7 @@ namespace Squidex.Domain.Apps.Entities.Assets var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(command.FileName, sut.Snapshot.FileName); @@ -143,7 +143,7 @@ namespace Squidex.Domain.Apps.Entities.Assets var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(command.Slug, sut.Snapshot.Slug); @@ -162,7 +162,7 @@ namespace Squidex.Domain.Apps.Entities.Assets var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(command.IsProtected, sut.Snapshot.IsProtected); @@ -181,7 +181,7 @@ namespace Squidex.Domain.Apps.Entities.Assets var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(command.Metadata, sut.Snapshot.Metadata); @@ -200,7 +200,7 @@ namespace Squidex.Domain.Apps.Entities.Assets var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); LastEvents .ShouldHaveSameEvents( @@ -217,7 +217,7 @@ namespace Squidex.Domain.Apps.Entities.Assets var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(parentId, sut.Snapshot.ParentId); @@ -237,7 +237,7 @@ namespace Squidex.Domain.Apps.Entities.Assets var result = await PublishAsync(command); - result.ShouldBeEquivalent2(new EntitySavedResult(2)); + result.ShouldBeEquivalent(new EntitySavedResult(2)); Assert.True(sut.Snapshot.IsDeleted); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetFolderDomainObjectTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetFolderDomainObjectTests.cs index e253ea3c7..5a8842674 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetFolderDomainObjectTests.cs +++ b/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); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(command.FolderName, sut.Snapshot.FolderName); @@ -79,7 +79,7 @@ namespace Squidex.Domain.Apps.Entities.Assets var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(command.FolderName, sut.Snapshot.FolderName); @@ -98,7 +98,7 @@ namespace Squidex.Domain.Apps.Entities.Assets var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(parentId, sut.Snapshot.ParentId); @@ -117,7 +117,7 @@ namespace Squidex.Domain.Apps.Entities.Assets var result = await PublishAsync(command); - result.ShouldBeEquivalent2(new EntitySavedResult(1)); + result.ShouldBeEquivalent(new EntitySavedResult(1)); Assert.True(sut.Snapshot.IsDeleted); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/MongoDbQueryTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/MongoDbQueryTests.cs index 5181d9312..902f767f2 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/MongoDbQueryTests.cs +++ b/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 cursor = A.Fake>(); - cursor.AssetTake(query.AdjustToModel()); + cursor.Take(query.AdjustToModel()); A.CallTo(() => cursor.Limit(3)) .MustHaveHappened(); @@ -182,7 +182,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.MongoDb var query = new ClrQuery { Skip = 3 }; var cursor = A.Fake>(); - cursor.AssetSkip(query.AdjustToModel()); + cursor.Skip(query.AdjustToModel()); A.CallTo(() => cursor.Skip(3)) .MustHaveHappened(); @@ -210,7 +210,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.MongoDb i = sortDefinition.Render(Serializer, Registry).ToString(); }); - cursor.AssetSort(new ClrQuery { Sort = sorts.ToList() }.AdjustToModel()); + cursor.Sort(new ClrQuery { Sort = sorts.ToList() }.AdjustToModel()); return i; } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/CommentsGrainTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/CommentsGrainTests.cs index 8051b580e..2b516130a 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/CommentsGrainTests.cs +++ b/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)); - 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(-1).Result.Should().BeEquivalentTo(new CommentsResult @@ -82,7 +82,7 @@ namespace Squidex.Domain.Apps.Entities.Comments 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 { @@ -118,7 +118,7 @@ namespace Squidex.Domain.Apps.Entities.Comments 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(0).Result.Should().BeEquivalentTo(new CommentsResult diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentDomainObjectTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentDomainObjectTests.cs index 94aed07c1..5113d4883 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentDomainObjectTests.cs +++ b/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)); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(Status.Draft, sut.Snapshot.Status); @@ -148,7 +148,7 @@ namespace Squidex.Domain.Apps.Entities.Contents var result = await PublishAsync(CreateContentCommand(command)); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(Status.Published, sut.Snapshot.Status); @@ -181,7 +181,7 @@ namespace Squidex.Domain.Apps.Entities.Contents var result = await PublishAsync(CreateContentCommand(command)); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); LastEvents .ShouldHaveSameEvents( @@ -202,7 +202,7 @@ namespace Squidex.Domain.Apps.Entities.Contents var result = await PublishAsync(CreateContentCommand(command)); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); Assert.True(sut.Snapshot.IsPending); @@ -224,7 +224,7 @@ namespace Squidex.Domain.Apps.Entities.Contents var result = await PublishAsync(CreateContentCommand(command)); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); Assert.Single(LastEvents); @@ -251,7 +251,7 @@ namespace Squidex.Domain.Apps.Entities.Contents var result = await PublishAsync(CreateContentCommand(command)); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); LastEvents .ShouldHaveSameEvents( @@ -272,7 +272,7 @@ namespace Squidex.Domain.Apps.Entities.Contents var result = await PublishAsync(CreateContentCommand(command)); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); Assert.True(sut.Snapshot.IsPending); @@ -294,7 +294,7 @@ namespace Squidex.Domain.Apps.Entities.Contents var result = await PublishAsync(CreateContentCommand(command)); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); Assert.Single(LastEvents); @@ -311,7 +311,7 @@ namespace Squidex.Domain.Apps.Entities.Contents var result = await PublishAsync(CreateContentCommand(command)); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(Status.Published, sut.Snapshot.Status); @@ -333,7 +333,7 @@ namespace Squidex.Domain.Apps.Entities.Contents var result = await PublishAsync(CreateContentCommand(command)); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(Status.Archived, sut.Snapshot.Status); @@ -356,7 +356,7 @@ namespace Squidex.Domain.Apps.Entities.Contents var result = await PublishAsync(CreateContentCommand(command)); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(Status.Draft, sut.Snapshot.Status); @@ -379,7 +379,7 @@ namespace Squidex.Domain.Apps.Entities.Contents var result = await PublishAsync(CreateContentCommand(command)); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(Status.Draft, sut.Snapshot.Status); @@ -403,7 +403,7 @@ namespace Squidex.Domain.Apps.Entities.Contents var result = await PublishAsync(CreateContentCommand(command)); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); Assert.False(sut.Snapshot.IsPending); @@ -427,7 +427,7 @@ namespace Squidex.Domain.Apps.Entities.Contents var result = await PublishAsync(CreateContentCommand(command)); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(Status.Draft, sut.Snapshot.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)); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); Assert.Null(sut.Snapshot.ScheduleJob); @@ -477,7 +477,7 @@ namespace Squidex.Domain.Apps.Entities.Contents var result = await PublishAsync(CreateContentCommand(command)); - result.ShouldBeEquivalent2(new EntitySavedResult(1)); + result.ShouldBeEquivalent(new EntitySavedResult(1)); Assert.True(sut.Snapshot.IsDeleted); @@ -501,7 +501,7 @@ namespace Squidex.Domain.Apps.Entities.Contents var result = await PublishAsync(CreateContentCommand(command)); - result.ShouldBeEquivalent2(new EntitySavedResult(3)); + result.ShouldBeEquivalent(new EntitySavedResult(3)); Assert.False(sut.Snapshot.IsPending); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/MongoDbQueryTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/MongoDbQueryTests.cs index 439449429..6fdf17e19 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/MongoDbQueryTests.cs +++ b/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.Schemas; 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.Visitors; +using Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations; using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Infrastructure; using Squidex.Infrastructure.MongoDb; @@ -276,7 +277,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb var query = new ClrQuery { Take = 3 }; var cursor = A.Fake>(); - cursor.ContentTake(query.AdjustToModel(schemaDef, false)); + cursor.Take(query); A.CallTo(() => cursor.Limit(3)) .MustHaveHappened(); @@ -288,7 +289,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb var query = new ClrQuery { Skip = 3 }; var cursor = A.Fake>(); - cursor.ContentSkip(query.AdjustToModel(schemaDef, false)); + cursor.Skip(query); A.CallTo(() => cursor.Skip(3)) .MustHaveHappened(); @@ -316,7 +317,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb 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; } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDomainObjectTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDomainObjectTests.cs index 3e9e9c899..d15951eb4 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDomainObjectTests.cs +++ b/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); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(AppId, sut.Snapshot.AppId.Id); @@ -83,7 +83,7 @@ namespace Squidex.Domain.Apps.Entities.Rules 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.Action, sut.Snapshot.RuleDef.Action); @@ -106,7 +106,7 @@ namespace Squidex.Domain.Apps.Entities.Rules var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); Assert.True(sut.Snapshot.RuleDef.IsEnabled); @@ -125,7 +125,7 @@ namespace Squidex.Domain.Apps.Entities.Rules var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); Assert.False(sut.Snapshot.RuleDef.IsEnabled); @@ -144,7 +144,7 @@ namespace Squidex.Domain.Apps.Entities.Rules var result = await PublishAsync(command); - result.ShouldBeEquivalent2(new EntitySavedResult(1)); + result.ShouldBeEquivalent(new EntitySavedResult(1)); Assert.True(sut.Snapshot.IsDeleted); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaDomainObjectTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaDomainObjectTests.cs index 052f9f1ab..3c01275f0 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaDomainObjectTests.cs +++ b/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); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(AppId, sut.Snapshot.AppId.Id); @@ -101,7 +101,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas var result = await PublishAsync(command); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); var @event = (SchemaCreated)LastEvents.Single().Payload; @@ -121,7 +121,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(command.Properties, sut.Snapshot.SchemaDef.Properties); @@ -146,7 +146,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); LastEvents .ShouldHaveSameEvents( @@ -167,7 +167,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(command.FieldsInLists, sut.Snapshot.SchemaDef.FieldsInLists); @@ -190,7 +190,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(command.FieldsInReferences, sut.Snapshot.SchemaDef.FieldsInReferences); @@ -209,7 +209,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); Assert.True(sut.Snapshot.SchemaDef.IsPublished); @@ -229,7 +229,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); Assert.False(sut.Snapshot.SchemaDef.IsPublished); @@ -248,7 +248,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(command.Name, sut.Snapshot.SchemaDef.Category); @@ -273,7 +273,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(command.PreviewUrls, sut.Snapshot.SchemaDef.PreviewUrls); @@ -292,7 +292,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas var result = await PublishAsync(command); - result.ShouldBeEquivalent2(new EntitySavedResult(1)); + result.ShouldBeEquivalent(new EntitySavedResult(1)); Assert.True(sut.Snapshot.IsDeleted); @@ -313,7 +313,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); LastEvents .ShouldHaveSameEvents( @@ -333,7 +333,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); LastEvents .ShouldHaveSameEvents( @@ -350,7 +350,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas var result = await PublishAsync(command); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(command.Properties, GetField(1).RawProperties); @@ -370,7 +370,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas var result = await PublishAsync(command); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); Assert.Same(command.Properties, GetNestedField(1, 2).RawProperties); @@ -390,7 +390,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(command.Properties, GetField(1).RawProperties); @@ -411,7 +411,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); Assert.Same(command.Properties, GetNestedField(1, 2).RawProperties); @@ -431,7 +431,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); Assert.False(GetField(1).IsDisabled); @@ -452,7 +452,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); Assert.True(GetNestedField(1, 2).IsLocked); @@ -472,7 +472,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); Assert.True(GetField(1).IsHidden); @@ -493,7 +493,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); Assert.True(GetNestedField(1, 2).IsHidden); @@ -514,7 +514,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); Assert.False(GetField(1).IsHidden); @@ -536,7 +536,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); Assert.False(GetNestedField(1, 2).IsHidden); @@ -556,7 +556,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); Assert.True(GetField(1).IsDisabled); @@ -577,7 +577,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); Assert.True(GetNestedField(1, 2).IsDisabled); @@ -598,7 +598,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); Assert.False(GetField(1).IsDisabled); @@ -620,7 +620,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas var result = await PublishAsync(command); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); Assert.False(GetNestedField(1, 2).IsDisabled); @@ -640,7 +640,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas var result = await PublishAsync(command); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); Assert.Null(GetField(1)); @@ -661,7 +661,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas var result = await PublishAsync(command); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); Assert.Null(GetNestedField(1, 2)); @@ -683,7 +683,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas var result = await PublishIdempotentAsync(command); - result.ShouldBeEquivalent2(sut.Snapshot); + result.ShouldBeEquivalent(sut.Snapshot); Assert.Equal(command.Category, sut.Snapshot.SchemaDef.Category); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/AssertHelper.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/AssertHelper.cs index 46859d9a0..12ba05cca 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/AssertHelper.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/AssertHelper.cs @@ -37,7 +37,7 @@ namespace Squidex.Domain.Apps.Entities.TestHelpers ((object)lhs).Should().BeEquivalentTo(rhs, o => o.IncludingAllRuntimeProperties()); } - public static void ShouldBeEquivalent2(this T lhs, T rhs) + public static void ShouldBeEquivalent(this T lhs, T rhs) { lhs.Should().BeEquivalentTo(rhs, o => o.IncludingProperties()); } diff --git a/backend/tests/Squidex.Infrastructure.Tests/NamedIdTests.cs b/backend/tests/Squidex.Infrastructure.Tests/NamedIdTests.cs index add1d6d8e..3833a3bf7 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/NamedIdTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/NamedIdTests.cs @@ -34,31 +34,6 @@ namespace Squidex.Infrastructure Assert.Equal($"{id},my-name", namedId.ToString()); } - [Fact] - public void Should_make_correct_equal_comparisons() - { - var id1 = Guid.NewGuid(); - var id2 = Guid.NewGuid(); - - var named_id1_name1_a = NamedId.Of(id1, "name1"); - var named_id1_name1_b = NamedId.Of(id1, "name1"); - - var named_id2_name1 = NamedId.Of(id2, "name1"); - var named_id1_name2 = NamedId.Of(id1, "name2"); - - Assert.Equal(named_id1_name1_a, named_id1_name1_b); - Assert.Equal(named_id1_name1_a.GetHashCode(), named_id1_name1_b.GetHashCode()); - Assert.True(named_id1_name1_a.Equals((object)named_id1_name1_b)); - - Assert.NotEqual(named_id1_name1_a, named_id2_name1); - Assert.NotEqual(named_id1_name1_a.GetHashCode(), named_id2_name1.GetHashCode()); - Assert.False(named_id1_name1_a.Equals((object)named_id2_name1)); - - Assert.NotEqual(named_id1_name1_a, named_id1_name2); - Assert.NotEqual(named_id1_name1_a.GetHashCode(), named_id1_name2.GetHashCode()); - Assert.False(named_id1_name1_a.Equals((object)named_id1_name2)); - } - [Fact] public void Should_serialize_and_deserialize_null_guid_token() { diff --git a/backend/tests/Squidex.Infrastructure.Tests/RefTokenTests.cs b/backend/tests/Squidex.Infrastructure.Tests/RefTokenTests.cs index 9374b9845..503c0a479 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/RefTokenTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/RefTokenTests.cs @@ -77,28 +77,6 @@ namespace Squidex.Infrastructure Assert.Equal("client:client1", token.ToString()); } - [Fact] - public void Should_make_correct_equal_comparisons() - { - var token_type1_id1_a = RefToken.Parse("type1:client1"); - var token_type1_id1_b = RefToken.Parse("type1:client1"); - - var token_type2_id1 = RefToken.Parse("type2:client1"); - var token_type1_id2 = RefToken.Parse("type1:client2"); - - Assert.Equal(token_type1_id1_a, token_type1_id1_b); - Assert.Equal(token_type1_id1_a.GetHashCode(), token_type1_id1_b.GetHashCode()); - Assert.True(token_type1_id1_a.Equals((object)token_type1_id1_b)); - - Assert.NotEqual(token_type1_id1_a, token_type2_id1); - Assert.NotEqual(token_type1_id1_a.GetHashCode(), token_type2_id1.GetHashCode()); - Assert.False(token_type1_id1_a.Equals((object)token_type2_id1)); - - Assert.NotEqual(token_type1_id1_a, token_type1_id2); - Assert.NotEqual(token_type1_id1_a.GetHashCode(), token_type1_id2.GetHashCode()); - Assert.False(token_type1_id1_a.Equals((object)token_type1_id2)); - } - [Fact] public void Should_serialize_and_deserialize_null_token() { diff --git a/backend/tests/Squidex.Infrastructure.Tests/Reflection/PropertiesTypeAccessorTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Reflection/PropertiesTypeAccessorTests.cs index 383c556b4..270650c16 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Reflection/PropertiesTypeAccessorTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Reflection/PropertiesTypeAccessorTests.cs @@ -6,7 +6,7 @@ // ========================================================================== using System; -using System.Linq; +using Squidex.Infrastructure.Reflection.Internal; using Xunit; namespace Squidex.Infrastructure.Reflection @@ -41,20 +41,13 @@ namespace Squidex.Infrastructure.Reflection } private readonly TestClass target = new TestClass(); - private readonly PropertiesTypeAccessor accessor = PropertiesTypeAccessor.Create(typeof(TestClass)); - - [Fact] - public void Should_provide_properties() - { - var properties = accessor.Properties.Select(x => x.Name).ToArray(); - - Assert.Equal(new[] { "ReadWrite", "Read", "Write" }, properties); - } [Fact] public void Should_set_read_write_property() { - accessor.SetValue(target, "ReadWrite", 123); + var sut = new PropertyAccessor(typeof(TestClass), typeof(TestClass).GetProperty("ReadWrite")!); + + sut.Set(target, 123); Assert.Equal(123, target.Read); } @@ -62,49 +55,47 @@ namespace Squidex.Infrastructure.Reflection [Fact] public void Should_set_write_property() { - accessor.SetValue(target, "Write", 123); + var accessor = new PropertyAccessor(typeof(TestClass), typeof(TestClass).GetProperty("Write")!); - Assert.Equal(123, target.Read); - } + accessor.Set(target, 123); - [Fact] - public void Should_throw_exception_if_setting_unknown_property() - { - Assert.Throws(() => accessor.SetValue(target, "Unknown", 123)); + Assert.Equal(123, target.Read); } [Fact] public void Should_throw_exception_if_setting_readonly() { - Assert.Throws(() => accessor.SetValue(target, "Read", 123)); + var sut = new PropertyAccessor(typeof(TestClass), typeof(TestClass).GetProperty("Read")!); + + Assert.Throws(() => sut.Set(target, 123)); } [Fact] public void Should_get_read_write_property() { + var sut = new PropertyAccessor(typeof(TestClass), typeof(TestClass).GetProperty("ReadWrite")!); + target.Write = 123; - Assert.Equal(123, accessor.GetValue(target, "ReadWrite")); + Assert.Equal(123, sut.Get(target)); } [Fact] public void Should_get_read_property() { + var sut = new PropertyAccessor(typeof(TestClass), typeof(TestClass).GetProperty("Read")!); + target.Write = 123; - Assert.Equal(123, accessor.GetValue(target, "Read")); + Assert.Equal(123, sut.Get(target)); } [Fact] - public void Should_throw_exception_if_getting_unknown_property() + public void Should_throw_exception_if_getting_writeonly_property() { - Assert.Throws(() => accessor.GetValue(target, "Unknown")); - } + var sut = new PropertyAccessor(typeof(TestClass), typeof(TestClass).GetProperty("Write")!); - [Fact] - public void Should_throw_exception_if_getting_readonly_property() - { - Assert.Throws(() => accessor.GetValue(target, "Write")); + Assert.Throws(() => sut.Get(target)); } } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/Reflection/SimpleCopierTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Reflection/SimpleCopierTests.cs deleted file mode 100644 index 95d99f687..000000000 --- a/backend/tests/Squidex.Infrastructure.Tests/Reflection/SimpleCopierTests.cs +++ /dev/null @@ -1,74 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using Xunit; - -namespace Squidex.Infrastructure.Reflection -{ - public class SimpleCopierTests - { - public class Cloneable : ICloneable - { - public int Value { get; } - - public Cloneable(int value) - { - Value = value; - } - - public object Clone() - { - return new Cloneable(Value); - } - } - - public class MyClass1Base - { - public int Value1 { get; set; } - } - - public class MyClass1 : MyClass1Base - { - public int Value2 { get; set; } - - public int ValueReadOnly { get; } - - public Cloneable Cloneable { get; set; } - - public MyClass1() - { - } - - public MyClass1(int readValue) - { - ValueReadOnly = readValue; - } - } - - [Fact] - public void Should_copy_class() - { - var value = new MyClass1(100) - { - Value1 = 1, - Value2 = 2, - Cloneable = new Cloneable(4) - }; - - var copy = value.Copy(); - - Assert.Equal(value.Value1, copy.Value1); - Assert.Equal(value.Value2, copy.Value2); - - Assert.Equal(0, copy.ValueReadOnly); - - Assert.Equal(value.Cloneable.Value, copy.Cloneable.Value); - Assert.NotSame(value.Cloneable, copy.Cloneable); - } - } -} diff --git a/backend/tests/Squidex.Infrastructure.Tests/Reflection/SimpleEqualsTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Reflection/SimpleEqualsTests.cs new file mode 100644 index 000000000..31f9c09c6 --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/Reflection/SimpleEqualsTests.cs @@ -0,0 +1,259 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using Squidex.Infrastructure.Collections; +using Xunit; + +namespace Squidex.Infrastructure.Reflection +{ + public class SimpleEqualsTests + { + public class Class : ClassBase + { + public int Scalar { get; set; } + + public int ReadOnly + { + set { Debug.WriteLine(value); } + } + } + + public class CustomEquals : IEquatable + { + private readonly int value; + + public CustomEquals(int value) + { + this.value = value; + } + + public override bool Equals(object? obj) + { + return Equals(obj as CustomEquals); + } + + public bool Equals([AllowNull] CustomEquals other) + { + return other != null && other.value == value; + } + + public override int GetHashCode() + { + return value; + } + } + + public class ClassBase + { + [IgnoreEquals] + public int Ignored { get; set; } + + public List Complex { get; set; } + } + + public static IEnumerable RandomValues() + { + yield return new object[] + { + Guid.NewGuid(), + Guid.NewGuid(), + }; + + yield return new object[] + { + 12, + 22 + }; + + yield return new object[] + { + DateTime.UtcNow, + DateTime.UtcNow.AddSeconds(2) + }; + + yield return new object[] + { + TimeSpan.FromMilliseconds(123), + TimeSpan.FromMilliseconds(55) + }; + } + + [Theory] + [MemberData(nameof(RandomValues))] + public void Should_compare_values(object lhs, object rhs) + { + Assert.True(SimpleEquals.IsEquals(lhs, lhs)); + Assert.False(SimpleEquals.IsEquals(lhs, rhs)); + } + + [Fact] + public void Should_compare_equal_customs() + { + var customA_1 = new CustomEquals(1); + var customA_2 = new CustomEquals(1); + + Assert.True(SimpleEquals.IsEquals(customA_1, customA_1)); + Assert.True(SimpleEquals.IsEquals(customA_1, customA_2)); + } + + [Fact] + public void Should_compare_non_equal_customs() + { + var customA_1 = new CustomEquals(1); + var customB_1 = new CustomEquals(2); + + Assert.False(SimpleEquals.IsEquals(customA_1, customB_1)); + Assert.False(SimpleEquals.IsEquals(customA_1, null!)); + } + + [Fact] + public void Should_compare_equal_strings() + { + var stringA_1 = "a"; + var stringA_2 = new string(new char[] { 'a' }); + + Assert.True(SimpleEquals.IsEquals(stringA_1, stringA_1)); + Assert.True(SimpleEquals.IsEquals(stringA_1, stringA_2)); + } + + [Fact] + public void Should_compare_non_equal_strings() + { + var stringA_1 = "a"; + var stringB_2 = new string(new char[] { 'b' }); + + Assert.False(SimpleEquals.IsEquals(stringA_1, stringB_2)); + Assert.False(SimpleEquals.IsEquals(stringA_1, null!)); + } + + [Fact] + public void Should_compare_equal_lists() + { + var listA_1 = new List { "a" }; + var listA_2 = new List { "a" }; + + Assert.True(SimpleEquals.IsEquals(listA_1, listA_1)); + Assert.True(SimpleEquals.IsEquals(listA_1, listA_2)); + } + + [Fact] + public void Should_compare_non_equal_lists() + { + var listA_1 = new List { "a" }; + var listB_1 = new List { "b" }; + var listC_1 = new List { "b", "c" }; + + Assert.False(SimpleEquals.IsEquals(listA_1, listB_1)); + Assert.False(SimpleEquals.IsEquals(listA_1, listC_1)); + Assert.False(SimpleEquals.IsEquals(listA_1, null!)); + } + + [Fact] + public void Should_compare_equal_sets() + { + var setA_1 = new HashSet { "a", "b" }; + var setA_2 = new HashSet { "b", "a" }; + + Assert.True(SimpleEquals.IsEquals(setA_1, setA_1)); + Assert.True(SimpleEquals.IsEquals(setA_1, setA_2)); + } + + [Fact] + public void Should_compare_non_equal_sets() + { + var setA_1 = new HashSet { "a" }; + var setB_1 = new HashSet { "b" }; + + Assert.False(SimpleEquals.IsEquals(setA_1, setB_1)); + Assert.False(SimpleEquals.IsEquals(setA_1, null!)); + } + + [Fact] + public void Should_compare_equal_collections() + { + var listA_1 = ReadOnlyCollection.Create("a"); + var listA_2 = ReadOnlyCollection.Create("a"); + + Assert.True(SimpleEquals.IsEquals(listA_1, listA_1)); + Assert.True(SimpleEquals.IsEquals(listA_1, listA_2)); + } + + [Fact] + public void Should_compare_non_equal_collections() + { + var listA_1 = ReadOnlyCollection.Create("a"); + var listB_1 = ReadOnlyCollection.Create("b"); + var listC_1 = ReadOnlyCollection.Create("b"); + + Assert.False(SimpleEquals.IsEquals(listA_1, listB_1)); + Assert.False(SimpleEquals.IsEquals(listA_1, listC_1)); + Assert.False(SimpleEquals.IsEquals(listA_1, null!)); + } + + [Fact] + public void Should_compare_equal_dictionaries() + { + var dictionaryA_1 = new Dictionary { ["key1"] = 123 }; + var dictionaryA_2 = new Dictionary { ["key1"] = 123 }; + + Assert.True(SimpleEquals.IsEquals(dictionaryA_1, dictionaryA_1)); + Assert.True(SimpleEquals.IsEquals(dictionaryA_1, dictionaryA_2)); + } + + [Fact] + public void Should_compare_non_equal_dictionaries() + { + var listA_1 = new Dictionary { ["key1"] = 123 }; + var listB_1 = new Dictionary { ["key2"] = 123 }; + var listC_1 = new Dictionary { ["key1"] = 555 }; + var listD_1 = new Dictionary { ["key1"] = 123, ["key2"] = 55 }; + + Assert.False(SimpleEquals.IsEquals(listA_1, listB_1)); + Assert.False(SimpleEquals.IsEquals(listA_1, listC_1)); + Assert.False(SimpleEquals.IsEquals(listA_1, listD_1)); + Assert.False(SimpleEquals.IsEquals(listA_1, null!)); + } + + [Fact] + public void Should_compare_equal_objects() + { + var objectA_1 = new Class { Scalar = 1, Complex = new List { 1, 4 } }; + var objectA_2 = new Class { Scalar = 1, Complex = new List { 1, 4 } }; + + Assert.True(SimpleEquals.IsEquals(objectA_1, objectA_1)); + Assert.True(SimpleEquals.IsEquals(objectA_1, objectA_2)); + } + + [Fact] + public void Should_compare_equal_objects_with_ignored_properties() + { + var objectA_1 = new Class { Ignored = 1 }; + var objectA_2 = new Class { Ignored = 2 }; + + Assert.True(SimpleEquals.IsEquals(objectA_1, objectA_1)); + Assert.True(SimpleEquals.IsEquals(objectA_1, objectA_2)); + } + + [Fact] + public void Should_compare_non_equal_objects() + { + var objectA_1 = new Class { Scalar = 1, Complex = new List { 1, 4 } }; + var objectB_1 = new Class { Scalar = 1, Complex = new List { 1, 2 } }; + var objectC_1 = new Class { Scalar = 1, Complex = new List { 1, 2 } }; + var objectD_1 = new Class { Scalar = 2, Complex = null! }; + + Assert.False(SimpleEquals.IsEquals(objectA_1, objectB_1)); + Assert.False(SimpleEquals.IsEquals(objectA_1, objectC_1)); + Assert.False(SimpleEquals.IsEquals(objectA_1, objectD_1)); + Assert.False(SimpleEquals.IsEquals(objectA_1, null!)); + } + } +} diff --git a/backend/tests/Squidex.Infrastructure.Tests/Security/PermissionTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Security/PermissionTests.cs index e23c2e656..c3802ca81 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Security/PermissionTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Security/PermissionTests.cs @@ -152,32 +152,6 @@ namespace Squidex.Infrastructure.Security Assert.True(g.Includes(r)); } - [Fact] - public void Should_make_correct_object_equal_comparisons() - { - object permission1a = new Permission("app.1"); - object permission1b = new Permission("app.1"); - object permission2a = new Permission("app.2"); - - Assert.True(permission1a.Equals(permission1b)); - - Assert.False(permission1a.Equals(permission2a)); - Assert.False(permission1b.Equals(permission2a)); - } - - [Fact] - public void Should_provide_correct_hash_codes() - { - var permission1a = new Permission("app.1"); - var permission1b = new Permission("app.1"); - var permission2a = new Permission("app.2"); - - Assert.Equal(permission1a.GetHashCode(), permission1b.GetHashCode()); - - Assert.NotEqual(permission1a.GetHashCode(), permission2a.GetHashCode()); - Assert.NotEqual(permission1b.GetHashCode(), permission2a.GetHashCode()); - } - [Fact] public void Should_sort_by_name() {