From e79de92a8264d1645746a1c4ac856850c7d2f076 Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Tue, 8 Sep 2020 13:05:30 +0200 Subject: [PATCH 1/4] Added Equals/GetHashCode to QueryExpression concrete types --- .../CollectionNotEmptyExpression.cs | 22 ++++++++++++++ .../Expressions/ComparisonExpression.cs | 22 ++++++++++++++ .../Queries/Expressions/CountExpression.cs | 22 ++++++++++++++ .../Expressions/EqualsAnyOfExpression.cs | 30 +++++++++++++++++++ .../Expressions/IncludeElementExpression.cs | 30 +++++++++++++++++++ .../Queries/Expressions/IncludeExpression.cs | 29 ++++++++++++++++++ .../Expressions/LiteralConstantExpression.cs | 22 ++++++++++++++ .../Queries/Expressions/LogicalExpression.cs | 30 +++++++++++++++++++ .../Expressions/MatchTextExpression.cs | 22 ++++++++++++++ .../Queries/Expressions/NotExpression.cs | 22 ++++++++++++++ .../Expressions/NullConstantExpression.cs | 21 +++++++++++++ ...nationElementQueryStringValueExpression.cs | 24 +++++++++++++++ .../Expressions/PaginationExpression.cs | 22 ++++++++++++++ .../PaginationQueryStringValueExpression.cs | 29 ++++++++++++++++++ .../QueryStringParameterScopeExpression.cs | 22 ++++++++++++++ .../Expressions/QueryableHandlerExpression.cs | 22 ++++++++++++++ .../ResourceFieldChainExpression.cs | 28 +++++++++-------- .../Expressions/SortElementExpression.cs | 22 ++++++++++++++ .../Queries/Expressions/SortExpression.cs | 29 ++++++++++++++++++ .../Expressions/SparseFieldSetExpression.cs | 29 ++++++++++++++++++ .../Resources/Annotations/AttrAttribute.cs | 22 ++++++++++++++ .../Annotations/HasManyThroughAttribute.cs | 28 ++++++++++++++++- .../Annotations/RelationshipAttribute.cs | 13 +++++--- .../Annotations/ResourceFieldAttribute.cs | 22 ++++++++++++++ 24 files changed, 567 insertions(+), 17 deletions(-) diff --git a/src/JsonApiDotNetCore/Queries/Expressions/CollectionNotEmptyExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/CollectionNotEmptyExpression.cs index 2642874112..66ceabe09c 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/CollectionNotEmptyExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/CollectionNotEmptyExpression.cs @@ -24,5 +24,27 @@ public override string ToString() { return $"{Keywords.Has}({TargetCollection})"; } + + public override bool Equals(object obj) + { + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj is null || GetType() != obj.GetType()) + { + return false; + } + + var other = (CollectionNotEmptyExpression) obj; + + return TargetCollection.Equals(other.TargetCollection); + } + + public override int GetHashCode() + { + return TargetCollection.GetHashCode(); + } } } diff --git a/src/JsonApiDotNetCore/Queries/Expressions/ComparisonExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/ComparisonExpression.cs index c017c38afe..318d61fe68 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/ComparisonExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/ComparisonExpression.cs @@ -28,5 +28,27 @@ public override string ToString() { return $"{Operator.ToString().Camelize()}({Left},{Right})"; } + + public override bool Equals(object obj) + { + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj is null || GetType() != obj.GetType()) + { + return false; + } + + var other = (ComparisonExpression) obj; + + return Operator == other.Operator && Left.Equals(other.Left) && Right.Equals(other.Right); + } + + public override int GetHashCode() + { + return HashCode.Combine(Operator, Left, Right); + } } } diff --git a/src/JsonApiDotNetCore/Queries/Expressions/CountExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/CountExpression.cs index 80473bf3c7..3f6b83bb8e 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/CountExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/CountExpression.cs @@ -24,5 +24,27 @@ public override string ToString() { return $"{Keywords.Count}({TargetCollection})"; } + + public override bool Equals(object obj) + { + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj is null || GetType() != obj.GetType()) + { + return false; + } + + var other = (CountExpression) obj; + + return TargetCollection.Equals(other.TargetCollection); + } + + public override int GetHashCode() + { + return TargetCollection.GetHashCode(); + } } } diff --git a/src/JsonApiDotNetCore/Queries/Expressions/EqualsAnyOfExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/EqualsAnyOfExpression.cs index 18671bb8b0..f219241af6 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/EqualsAnyOfExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/EqualsAnyOfExpression.cs @@ -39,5 +39,35 @@ public override string ToString() return builder.ToString(); } + + public override bool Equals(object obj) + { + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj is null || GetType() != obj.GetType()) + { + return false; + } + + var other = (EqualsAnyOfExpression) obj; + + return TargetAttribute.Equals(other.TargetAttribute) && Constants.SequenceEqual(other.Constants); + } + + public override int GetHashCode() + { + var hashCode = new HashCode(); + hashCode.Add(TargetAttribute); + + foreach (var constant in Constants) + { + hashCode.Add(constant); + } + + return hashCode.ToHashCode(); + } } } diff --git a/src/JsonApiDotNetCore/Queries/Expressions/IncludeElementExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/IncludeElementExpression.cs index c8ca8c51f1..c24390bce1 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/IncludeElementExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/IncludeElementExpression.cs @@ -44,5 +44,35 @@ public override string ToString() return builder.ToString(); } + + public override bool Equals(object obj) + { + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj is null || GetType() != obj.GetType()) + { + return false; + } + + var other = (IncludeElementExpression) obj; + + return Relationship.Equals(other.Relationship) == Children.SequenceEqual(other.Children); + } + + public override int GetHashCode() + { + var hashCode = new HashCode(); + hashCode.Add(Relationship); + + foreach (var child in Children) + { + hashCode.Add(child); + } + + return hashCode.ToHashCode(); + } } } diff --git a/src/JsonApiDotNetCore/Queries/Expressions/IncludeExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/IncludeExpression.cs index 8f2a879952..d580f6c18d 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/IncludeExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/IncludeExpression.cs @@ -38,5 +38,34 @@ public override string ToString() var chains = IncludeChainConverter.GetRelationshipChains(this); return string.Join(",", chains.Select(child => child.ToString())); } + + public override bool Equals(object obj) + { + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj is null || GetType() != obj.GetType()) + { + return false; + } + + var other = (IncludeExpression) obj; + + return Elements.SequenceEqual(other.Elements); + } + + public override int GetHashCode() + { + var hashCode = new HashCode(); + + foreach (var element in Elements) + { + hashCode.Add(element); + } + + return hashCode.ToHashCode(); + } } } diff --git a/src/JsonApiDotNetCore/Queries/Expressions/LiteralConstantExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/LiteralConstantExpression.cs index c625f507f2..21876cf1e0 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/LiteralConstantExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/LiteralConstantExpression.cs @@ -24,5 +24,27 @@ public override string ToString() string value = Value.Replace("\'", "\'\'"); return $"'{value}'"; } + + public override bool Equals(object obj) + { + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj is null || GetType() != obj.GetType()) + { + return false; + } + + var other = (LiteralConstantExpression) obj; + + return Value == other.Value; + } + + public override int GetHashCode() + { + return Value.GetHashCode(); + } } } diff --git a/src/JsonApiDotNetCore/Queries/Expressions/LogicalExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/LogicalExpression.cs index c97855dc8a..474ed177d0 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/LogicalExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/LogicalExpression.cs @@ -46,5 +46,35 @@ public override string ToString() return builder.ToString(); } + + public override bool Equals(object obj) + { + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj is null || GetType() != obj.GetType()) + { + return false; + } + + var other = (LogicalExpression) obj; + + return Operator == other.Operator && Terms.SequenceEqual(other.Terms); + } + + public override int GetHashCode() + { + var hashCode = new HashCode(); + hashCode.Add(Operator); + + foreach (var term in Terms) + { + hashCode.Add(term); + } + + return hashCode.ToHashCode(); + } } } diff --git a/src/JsonApiDotNetCore/Queries/Expressions/MatchTextExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/MatchTextExpression.cs index 915dcc16d5..099546c033 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/MatchTextExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/MatchTextExpression.cs @@ -37,5 +37,27 @@ public override string ToString() return builder.ToString(); } + + public override bool Equals(object obj) + { + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj is null || GetType() != obj.GetType()) + { + return false; + } + + var other = (MatchTextExpression) obj; + + return TargetAttribute.Equals(other.TargetAttribute) && TextValue.Equals(other.TextValue) && MatchKind == other.MatchKind; + } + + public override int GetHashCode() + { + return HashCode.Combine(TargetAttribute, TextValue, MatchKind); + } } } diff --git a/src/JsonApiDotNetCore/Queries/Expressions/NotExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/NotExpression.cs index 14216c0dce..e9fd9bb06d 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/NotExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/NotExpression.cs @@ -24,5 +24,27 @@ public override string ToString() { return $"{Keywords.Not}({Child})"; } + + public override bool Equals(object obj) + { + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj is null || GetType() != obj.GetType()) + { + return false; + } + + var other = (NotExpression) obj; + + return Child.Equals(other.Child); + } + + public override int GetHashCode() + { + return Child.GetHashCode(); + } } } diff --git a/src/JsonApiDotNetCore/Queries/Expressions/NullConstantExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/NullConstantExpression.cs index 3e69ec907c..7344518ef1 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/NullConstantExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/NullConstantExpression.cs @@ -1,3 +1,4 @@ +using System; using JsonApiDotNetCore.Queries.Internal.Parsing; namespace JsonApiDotNetCore.Queries.Expressions @@ -16,5 +17,25 @@ public override string ToString() { return Keywords.Null; } + + public override bool Equals(object obj) + { + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj is null || GetType() != obj.GetType()) + { + return false; + } + + return true; + } + + public override int GetHashCode() + { + return new HashCode().ToHashCode(); + } } } diff --git a/src/JsonApiDotNetCore/Queries/Expressions/PaginationElementQueryStringValueExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/PaginationElementQueryStringValueExpression.cs index 8d08968d05..9c1bc03c0b 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/PaginationElementQueryStringValueExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/PaginationElementQueryStringValueExpression.cs @@ -1,3 +1,5 @@ +using System; + namespace JsonApiDotNetCore.Queries.Expressions { /// @@ -23,5 +25,27 @@ public override string ToString() { return Scope == null ? Value.ToString() : $"{Scope}: {Value}"; } + + public override bool Equals(object obj) + { + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj is null || GetType() != obj.GetType()) + { + return false; + } + + var other = (PaginationElementQueryStringValueExpression) obj; + + return Equals(Scope, other.Scope) && Value == other.Value; + } + + public override int GetHashCode() + { + return HashCode.Combine(Scope, Value); + } } } diff --git a/src/JsonApiDotNetCore/Queries/Expressions/PaginationExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/PaginationExpression.cs index 3e43d5c429..a035b25167 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/PaginationExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/PaginationExpression.cs @@ -26,5 +26,27 @@ public override string ToString() { return PageSize != null ? $"Page number: {PageNumber}, size: {PageSize}" : "(none)"; } + + public override bool Equals(object obj) + { + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj is null || GetType() != obj.GetType()) + { + return false; + } + + var other = (PaginationExpression) obj; + + return PageNumber.Equals(other.PageNumber) && Equals(PageSize, other.PageSize); + } + + public override int GetHashCode() + { + return HashCode.Combine(PageNumber, PageSize); + } } } diff --git a/src/JsonApiDotNetCore/Queries/Expressions/PaginationQueryStringValueExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/PaginationQueryStringValueExpression.cs index 848ad6fe12..7bd17ce921 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/PaginationQueryStringValueExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/PaginationQueryStringValueExpression.cs @@ -32,5 +32,34 @@ public override string ToString() { return string.Join(",", Elements.Select(constant => constant.ToString())); } + + public override bool Equals(object obj) + { + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj is null || GetType() != obj.GetType()) + { + return false; + } + + var other = (PaginationQueryStringValueExpression) obj; + + return Elements.SequenceEqual(other.Elements); + } + + public override int GetHashCode() + { + var hashCode = new HashCode(); + + foreach (var element in Elements) + { + hashCode.Add(element); + } + + return hashCode.ToHashCode(); + } } } diff --git a/src/JsonApiDotNetCore/Queries/Expressions/QueryStringParameterScopeExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/QueryStringParameterScopeExpression.cs index 7f21d21e90..8c9279259a 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/QueryStringParameterScopeExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/QueryStringParameterScopeExpression.cs @@ -25,5 +25,27 @@ public override string ToString() { return Scope == null ? ParameterName.ToString() : $"{ParameterName}: {Scope}"; } + + public override bool Equals(object obj) + { + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj is null || GetType() != obj.GetType()) + { + return false; + } + + var other = (QueryStringParameterScopeExpression) obj; + + return ParameterName.Equals(other.ParameterName) && Equals(Scope, other.Scope); + } + + public override int GetHashCode() + { + return HashCode.Combine(ParameterName, Scope); + } } } diff --git a/src/JsonApiDotNetCore/Queries/Expressions/QueryableHandlerExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/QueryableHandlerExpression.cs index 153630a4e0..4f7e3d41f2 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/QueryableHandlerExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/QueryableHandlerExpression.cs @@ -35,5 +35,27 @@ public override string ToString() { return $"handler('{_parameterValue}')"; } + + public override bool Equals(object obj) + { + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj is null || GetType() != obj.GetType()) + { + return false; + } + + var other = (QueryableHandlerExpression) obj; + + return _queryableHandler == other._queryableHandler && _parameterValue.Equals(other._parameterValue); + } + + public override int GetHashCode() + { + return HashCode.Combine(_queryableHandler, _parameterValue); + } } } diff --git a/src/JsonApiDotNetCore/Queries/Expressions/ResourceFieldChainExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/ResourceFieldChainExpression.cs index 7ef9442241..03de34a8f0 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/ResourceFieldChainExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/ResourceFieldChainExpression.cs @@ -8,7 +8,7 @@ namespace JsonApiDotNetCore.Queries.Expressions /// /// Represents a chain of fields (relationships and attributes), resulting from text such as: articles.revisions.author /// - public class ResourceFieldChainExpression : IdentifierExpression, IEquatable + public class ResourceFieldChainExpression : IdentifierExpression { public IReadOnlyCollection Fields { get; } @@ -43,29 +43,33 @@ public override string ToString() return string.Join(".", Fields.Select(field => field.PublicName)); } - public bool Equals(ResourceFieldChainExpression other) + public override bool Equals(object obj) { - if (other is null) + if (ReferenceEquals(this, obj)) { - return false; + return true; } - if (ReferenceEquals(this, other)) + if (obj is null || GetType() != obj.GetType()) { - return true; + return false; } - return Fields.SequenceEqual(other.Fields); - } + var other = (ResourceFieldChainExpression) obj; - public override bool Equals(object other) - { - return Equals(other as ResourceFieldChainExpression); + return Fields.SequenceEqual(other.Fields); } public override int GetHashCode() { - return Fields.Aggregate(0, HashCode.Combine); + var hashCode = new HashCode(); + + foreach (var field in Fields) + { + hashCode.Add(field); + } + + return hashCode.ToHashCode(); } } } diff --git a/src/JsonApiDotNetCore/Queries/Expressions/SortElementExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/SortElementExpression.cs index a7c24fca21..186a85013f 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/SortElementExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/SortElementExpression.cs @@ -49,5 +49,27 @@ public override string ToString() return builder.ToString(); } + + public override bool Equals(object obj) + { + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj is null || GetType() != obj.GetType()) + { + return false; + } + + var other = (SortElementExpression) obj; + + return Equals(TargetAttribute, other.TargetAttribute) && Equals(Count, other.Count) && IsAscending == other.IsAscending; + } + + public override int GetHashCode() + { + return HashCode.Combine(TargetAttribute, Count, IsAscending); + } } } diff --git a/src/JsonApiDotNetCore/Queries/Expressions/SortExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/SortExpression.cs index 65584c1cd1..61683a95a6 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/SortExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/SortExpression.cs @@ -30,5 +30,34 @@ public override string ToString() { return string.Join(",", Elements.Select(child => child.ToString())); } + + public override bool Equals(object obj) + { + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj is null || GetType() != obj.GetType()) + { + return false; + } + + var other = (SortExpression) obj; + + return Elements.SequenceEqual(other.Elements); + } + + public override int GetHashCode() + { + var hashCode = new HashCode(); + + foreach (var element in Elements) + { + hashCode.Add(element); + } + + return hashCode.ToHashCode(); + } } } diff --git a/src/JsonApiDotNetCore/Queries/Expressions/SparseFieldSetExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/SparseFieldSetExpression.cs index 4e3ae04e73..e9bb37b014 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/SparseFieldSetExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/SparseFieldSetExpression.cs @@ -31,5 +31,34 @@ public override string ToString() { return string.Join(",", Attributes.Select(child => child.PublicName)); } + + public override bool Equals(object obj) + { + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj is null || GetType() != obj.GetType()) + { + return false; + } + + var other = (SparseFieldSetExpression) obj; + + return Attributes.SequenceEqual(other.Attributes); + } + + public override int GetHashCode() + { + var hashCode = new HashCode(); + + foreach (var attribute in Attributes) + { + hashCode.Add(attribute); + } + + return hashCode.ToHashCode(); + } } } diff --git a/src/JsonApiDotNetCore/Resources/Annotations/AttrAttribute.cs b/src/JsonApiDotNetCore/Resources/Annotations/AttrAttribute.cs index 4f379bcab8..fce07fa160 100644 --- a/src/JsonApiDotNetCore/Resources/Annotations/AttrAttribute.cs +++ b/src/JsonApiDotNetCore/Resources/Annotations/AttrAttribute.cs @@ -70,5 +70,27 @@ public void SetValue(object resource, object newValue) var convertedValue = TypeHelper.ConvertType(newValue, Property.PropertyType); Property.SetValue(resource, convertedValue); } + + public override bool Equals(object obj) + { + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj is null || GetType() != obj.GetType()) + { + return false; + } + + var other = (AttrAttribute) obj; + + return Capabilities == other.Capabilities && base.Equals(other); + } + + public override int GetHashCode() + { + return HashCode.Combine(Capabilities, base.GetHashCode()); + } } } diff --git a/src/JsonApiDotNetCore/Resources/Annotations/HasManyThroughAttribute.cs b/src/JsonApiDotNetCore/Resources/Annotations/HasManyThroughAttribute.cs index 409e4d639a..16d8cd1075 100644 --- a/src/JsonApiDotNetCore/Resources/Annotations/HasManyThroughAttribute.cs +++ b/src/JsonApiDotNetCore/Resources/Annotations/HasManyThroughAttribute.cs @@ -47,7 +47,7 @@ public sealed class HasManyThroughAttribute : HasManyAttribute /// The name of the join property on the parent resource. /// In the example described above, this would be "ArticleTags". /// - internal string ThroughPropertyName { get; } + public string ThroughPropertyName { get; } /// /// The join type. @@ -147,5 +147,31 @@ public override void SetValue(object resource, object newValue, IResourceFactory ThroughProperty.SetValue(resource, typedCollection); } } + + public override bool Equals(object obj) + { + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj is null || GetType() != obj.GetType()) + { + return false; + } + + var other = (HasManyThroughAttribute) obj; + + return ThroughPropertyName == other.ThroughPropertyName && ThroughType == other.ThroughType && + LeftProperty == other.LeftProperty && LeftIdProperty == other.LeftIdProperty && + RightProperty == other.RightProperty && RightIdProperty == other.RightIdProperty && + ThroughProperty == other.ThroughProperty && base.Equals(other); + } + + public override int GetHashCode() + { + return HashCode.Combine(ThroughPropertyName, ThroughType, LeftProperty, LeftIdProperty, RightProperty, + RightIdProperty, ThroughProperty, base.GetHashCode()); + } } } diff --git a/src/JsonApiDotNetCore/Resources/Annotations/RelationshipAttribute.cs b/src/JsonApiDotNetCore/Resources/Annotations/RelationshipAttribute.cs index b13f8dd852..11dffec12d 100644 --- a/src/JsonApiDotNetCore/Resources/Annotations/RelationshipAttribute.cs +++ b/src/JsonApiDotNetCore/Resources/Annotations/RelationshipAttribute.cs @@ -89,20 +89,25 @@ public virtual void SetValue(object resource, object newValue, IResourceFactory public override bool Equals(object obj) { - if (obj == null || GetType() != obj.GetType()) + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj is null || GetType() != obj.GetType()) { return false; } var other = (RelationshipAttribute) obj; - return PublicName == other.PublicName && LeftType == other.LeftType && - RightType == other.RightType; + return LeftType == other.LeftType && RightType == other.RightType && Links == other.Links && + CanInclude == other.CanInclude && base.Equals(other); } public override int GetHashCode() { - return HashCode.Combine(PublicName, LeftType, RightType); + return HashCode.Combine(LeftType, RightType, Links, CanInclude, base.GetHashCode()); } } } diff --git a/src/JsonApiDotNetCore/Resources/Annotations/ResourceFieldAttribute.cs b/src/JsonApiDotNetCore/Resources/Annotations/ResourceFieldAttribute.cs index d9688ae52e..654d501853 100644 --- a/src/JsonApiDotNetCore/Resources/Annotations/ResourceFieldAttribute.cs +++ b/src/JsonApiDotNetCore/Resources/Annotations/ResourceFieldAttribute.cs @@ -37,5 +37,27 @@ public override string ToString() { return PublicName ?? (Property != null ? Property.Name : base.ToString()); } + + public override bool Equals(object obj) + { + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj is null || GetType() != obj.GetType()) + { + return false; + } + + var other = (ResourceFieldAttribute) obj; + + return PublicName == other.PublicName && Property == other.Property; + } + + public override int GetHashCode() + { + return HashCode.Combine(PublicName, Property); + } } } From 40cd81c04faf55750f6f23caf0027d5e7b0d6c4e Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Tue, 8 Sep 2020 15:52:58 +0200 Subject: [PATCH 2/4] Added expression rewriter --- .../Expressions/QueryExpressionRewriter.cs | 323 ++++++++++++++++++ 1 file changed, 323 insertions(+) create mode 100644 src/JsonApiDotNetCore/Queries/Expressions/QueryExpressionRewriter.cs diff --git a/src/JsonApiDotNetCore/Queries/Expressions/QueryExpressionRewriter.cs b/src/JsonApiDotNetCore/Queries/Expressions/QueryExpressionRewriter.cs new file mode 100644 index 0000000000..c6e4f9229c --- /dev/null +++ b/src/JsonApiDotNetCore/Queries/Expressions/QueryExpressionRewriter.cs @@ -0,0 +1,323 @@ +using System.Collections.Generic; +using System.Linq; + +namespace JsonApiDotNetCore.Queries.Expressions +{ + /// + /// Building block for rewriting trees. It walks nested expressions and updates parent on changes. + /// + public class QueryExpressionRewriter : QueryExpressionVisitor + { + public override QueryExpression Visit(QueryExpression expression, TArgument argument) + { + return expression.Accept(this, argument); + } + + public override QueryExpression DefaultVisit(QueryExpression expression, TArgument argument) + { + return expression; + } + + public override QueryExpression VisitComparison(ComparisonExpression expression, TArgument argument) + { + if (expression == null) + { + return null; + } + + var newLeft = Visit(expression.Left, argument); + var newRight = Visit(expression.Right, argument); + + var newExpression = new ComparisonExpression(expression.Operator, newLeft, newRight); + return newExpression.Equals(expression) ? expression : newExpression; + } + + public override QueryExpression VisitResourceFieldChain(ResourceFieldChainExpression expression, TArgument argument) + { + return expression; + } + + public override QueryExpression VisitLiteralConstant(LiteralConstantExpression expression, TArgument argument) + { + return expression; + } + + public override QueryExpression VisitNullConstant(NullConstantExpression expression, TArgument argument) + { + return expression; + } + + public override QueryExpression VisitLogical(LogicalExpression expression, TArgument argument) + { + if (expression == null) + { + return null; + } + + var newTerms = VisitSequence(expression.Terms, argument); + + if (newTerms.Count == 0) + { + return null; + } + + if (newTerms.Count == 1) + { + return newTerms.First(); + } + + var newExpression = new LogicalExpression(expression.Operator, newTerms); + return newExpression.Equals(expression) ? expression : newExpression; + } + + public override QueryExpression VisitNot(NotExpression expression, TArgument argument) + { + if (expression == null) + { + return null; + } + + var newChild = Visit(expression.Child, argument); + + if (newChild == null) + { + return null; + } + + var newExpression = new NotExpression(newChild); + return newExpression.Equals(expression) ? expression : newExpression; + } + + public override QueryExpression VisitCollectionNotEmpty(CollectionNotEmptyExpression expression, TArgument argument) + { + if (expression == null) + { + return null; + } + + var newTargetCollection = Visit(expression.TargetCollection, argument) as ResourceFieldChainExpression; + + if (newTargetCollection == null) + { + return null; + } + + var newExpression = new CollectionNotEmptyExpression(newTargetCollection); + return newExpression.Equals(expression) ? expression : newExpression; + } + + public override QueryExpression VisitSortElement(SortElementExpression expression, TArgument argument) + { + if (expression == null) + { + return null; + } + + SortElementExpression newExpression; + + if (expression.Count != null) + { + var newCount = Visit(expression.Count, argument) as CountExpression; + + if (newCount == null) + { + return null; + } + + newExpression = new SortElementExpression(newCount, expression.IsAscending); + } + else if (expression.TargetAttribute != null) + { + var newTargetAttribute = Visit(expression.TargetAttribute, argument) as ResourceFieldChainExpression; + + if (newTargetAttribute == null) + { + return null; + } + + newExpression = new SortElementExpression(newTargetAttribute, expression.IsAscending); + } + else + { + return null; + } + + return newExpression.Equals(expression) ? expression : newExpression; + } + + public override QueryExpression VisitSort(SortExpression expression, TArgument argument) + { + if (expression == null) + { + return null; + } + + var newElements = VisitSequence(expression.Elements, argument); + + if (newElements.Count == 0) + { + return null; + } + + var newExpression = new SortExpression(newElements); + return newExpression.Equals(expression) ? expression : newExpression; + } + + public override QueryExpression VisitPagination(PaginationExpression expression, TArgument argument) + { + return expression; + } + + public override QueryExpression VisitCount(CountExpression expression, TArgument argument) + { + if (expression == null) + { + return null; + } + + var newTargetCollection = Visit(expression.TargetCollection, argument) as ResourceFieldChainExpression; + + if (newTargetCollection == null) + { + return null; + } + + var newExpression = new CountExpression(newTargetCollection); + return newExpression.Equals(expression) ? expression : newExpression; + } + + public override QueryExpression VisitMatchText(MatchTextExpression expression, TArgument argument) + { + if (expression == null) + { + return null; + } + + var newTargetAttribute = Visit(expression.TargetAttribute, argument) as ResourceFieldChainExpression; + var newTextValue = Visit(expression.TextValue, argument) as LiteralConstantExpression; + + var newExpression = new MatchTextExpression(newTargetAttribute, newTextValue, expression.MatchKind); + return newExpression.Equals(expression) ? expression : newExpression; + } + + public override QueryExpression VisitEqualsAnyOf(EqualsAnyOfExpression expression, TArgument argument) + { + if (expression == null) + { + return null; + } + + var newTargetAttribute = Visit(expression.TargetAttribute, argument) as ResourceFieldChainExpression; + var newConstants = VisitSequence(expression.Constants, argument); + + var newExpression = new EqualsAnyOfExpression(newTargetAttribute, newConstants); + return newExpression.Equals(expression) ? expression : newExpression; + } + + public override QueryExpression VisitSparseFieldSet(SparseFieldSetExpression expression, TArgument argument) + { + return expression; + } + + public override QueryExpression VisitQueryStringParameterScope(QueryStringParameterScopeExpression expression, TArgument argument) + { + if (expression == null) + { + return null; + } + + var newParameterName = Visit(expression.ParameterName, argument) as LiteralConstantExpression; + + var newScope = expression.Scope != null + ? Visit(expression.Scope, argument) as ResourceFieldChainExpression + : null; + + var newExpression = new QueryStringParameterScopeExpression(newParameterName, newScope); + return newExpression.Equals(expression) ? expression : newExpression; + } + + public override QueryExpression PaginationQueryStringValue(PaginationQueryStringValueExpression expression, TArgument argument) + { + if (expression == null) + { + return null; + } + + var newElements = VisitSequence(expression.Elements, argument); + + var newExpression = new PaginationQueryStringValueExpression(newElements); + return newExpression.Equals(expression) ? expression : newExpression; + } + + public override QueryExpression PaginationElementQueryStringValue(PaginationElementQueryStringValueExpression expression, + TArgument argument) + { + if (expression == null) + { + return null; + } + + var newScope = expression.Scope != null + ? Visit(expression.Scope, argument) as ResourceFieldChainExpression + : null; + + var newExpression = new PaginationElementQueryStringValueExpression(newScope, expression.Value); + return newExpression.Equals(expression) ? expression : newExpression; + } + + public override QueryExpression VisitInclude(IncludeExpression expression, TArgument argument) + { + if (expression == null) + { + return null; + } + + var newElements = VisitSequence(expression.Elements, argument); + + if (newElements.Count == 0) + { + return IncludeExpression.Empty; + } + + var newExpression = new IncludeExpression(newElements); + return newExpression.Equals(expression) ? expression : newExpression; + } + + public override QueryExpression VisitIncludeElement(IncludeElementExpression expression, TArgument argument) + { + if (expression == null) + { + return null; + } + + var newElements = VisitSequence(expression.Children, argument); + + var newExpression = new IncludeElementExpression(expression.Relationship, newElements); + return newExpression.Equals(expression) ? expression : newExpression; + } + + public override QueryExpression VisitQueryableHandler(QueryableHandlerExpression expression, TArgument argument) + { + return expression; + } + + protected virtual IReadOnlyCollection VisitSequence(IEnumerable elements, + TArgument argument) + where TExpression : QueryExpression + { + var newElements = new List(); + + foreach (TExpression element in elements) + { + var newElement = Visit(element, argument) as TExpression; + + if (newElement != null) + { + newElements.Add(newElement); + } + } + + return newElements; + } + } +} From e76776c47487bc4c5aa2b456ffb7f0648851c7fb Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Wed, 9 Sep 2020 12:43:27 +0200 Subject: [PATCH 3/4] Replaced 'as' expressions with pattern matching, which let to many more nested `return null`s, so I rewrote them for single entry/exit. --- .../Expressions/QueryExpressionRewriter.cs | 249 ++++++++---------- 1 file changed, 112 insertions(+), 137 deletions(-) diff --git a/src/JsonApiDotNetCore/Queries/Expressions/QueryExpressionRewriter.cs b/src/JsonApiDotNetCore/Queries/Expressions/QueryExpressionRewriter.cs index c6e4f9229c..b1a42bd886 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/QueryExpressionRewriter.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/QueryExpressionRewriter.cs @@ -49,118 +49,99 @@ public override QueryExpression VisitNullConstant(NullConstantExpression express public override QueryExpression VisitLogical(LogicalExpression expression, TArgument argument) { - if (expression == null) + if (expression != null) { - return null; - } - - var newTerms = VisitSequence(expression.Terms, argument); + var newTerms = VisitSequence(expression.Terms, argument); - if (newTerms.Count == 0) - { - return null; - } + if (newTerms.Count == 1) + { + return newTerms.First(); + } - if (newTerms.Count == 1) - { - return newTerms.First(); + if (newTerms.Count != 0) + { + var newExpression = new LogicalExpression(expression.Operator, newTerms); + return newExpression.Equals(expression) ? expression : newExpression; + } } - var newExpression = new LogicalExpression(expression.Operator, newTerms); - return newExpression.Equals(expression) ? expression : newExpression; + return null; } public override QueryExpression VisitNot(NotExpression expression, TArgument argument) { - if (expression == null) + if (expression != null) { - return null; - } + var newChild = Visit(expression.Child, argument); - var newChild = Visit(expression.Child, argument); - - if (newChild == null) - { - return null; + if (newChild != null) + { + var newExpression = new NotExpression(newChild); + return newExpression.Equals(expression) ? expression : newExpression; + } } - var newExpression = new NotExpression(newChild); - return newExpression.Equals(expression) ? expression : newExpression; + return null; } public override QueryExpression VisitCollectionNotEmpty(CollectionNotEmptyExpression expression, TArgument argument) { - if (expression == null) - { - return null; - } - - var newTargetCollection = Visit(expression.TargetCollection, argument) as ResourceFieldChainExpression; - - if (newTargetCollection == null) + if (expression != null) { - return null; + if (Visit(expression.TargetCollection, argument) is ResourceFieldChainExpression newTargetCollection) + { + var newExpression = new CollectionNotEmptyExpression(newTargetCollection); + return newExpression.Equals(expression) ? expression : newExpression; + } } - var newExpression = new CollectionNotEmptyExpression(newTargetCollection); - return newExpression.Equals(expression) ? expression : newExpression; + return null; } public override QueryExpression VisitSortElement(SortElementExpression expression, TArgument argument) { - if (expression == null) - { - return null; - } - - SortElementExpression newExpression; - - if (expression.Count != null) + if (expression != null) { - var newCount = Visit(expression.Count, argument) as CountExpression; + SortElementExpression newExpression = null; - if (newCount == null) + if (expression.Count != null) { - return null; + if (Visit(expression.Count, argument) is CountExpression newCount) + { + newExpression = new SortElementExpression(newCount, expression.IsAscending); + } } - - newExpression = new SortElementExpression(newCount, expression.IsAscending); - } - else if (expression.TargetAttribute != null) - { - var newTargetAttribute = Visit(expression.TargetAttribute, argument) as ResourceFieldChainExpression; - - if (newTargetAttribute == null) + else if (expression.TargetAttribute != null) { - return null; + if (Visit(expression.TargetAttribute, argument) is ResourceFieldChainExpression newTargetAttribute) + { + newExpression = new SortElementExpression(newTargetAttribute, expression.IsAscending); + } } - newExpression = new SortElementExpression(newTargetAttribute, expression.IsAscending); - } - else - { - return null; + if (newExpression != null) + { + return newExpression.Equals(expression) ? expression : newExpression; + } } - return newExpression.Equals(expression) ? expression : newExpression; + return null; } public override QueryExpression VisitSort(SortExpression expression, TArgument argument) { - if (expression == null) + if (expression != null) { - return null; - } - - var newElements = VisitSequence(expression.Elements, argument); + var newElements = VisitSequence(expression.Elements, argument); - if (newElements.Count == 0) - { - return null; + if (newElements.Count != 0) + { + var newExpression = new SortExpression(newElements); + return newExpression.Equals(expression) ? expression : newExpression; + } } - var newExpression = new SortExpression(newElements); - return newExpression.Equals(expression) ? expression : newExpression; + return null; } public override QueryExpression VisitPagination(PaginationExpression expression, TArgument argument) @@ -170,48 +151,44 @@ public override QueryExpression VisitPagination(PaginationExpression expression, public override QueryExpression VisitCount(CountExpression expression, TArgument argument) { - if (expression == null) - { - return null; - } - - var newTargetCollection = Visit(expression.TargetCollection, argument) as ResourceFieldChainExpression; - - if (newTargetCollection == null) + if (expression != null) { - return null; + if (Visit(expression.TargetCollection, argument) is ResourceFieldChainExpression newTargetCollection) + { + var newExpression = new CountExpression(newTargetCollection); + return newExpression.Equals(expression) ? expression : newExpression; + } } - var newExpression = new CountExpression(newTargetCollection); - return newExpression.Equals(expression) ? expression : newExpression; + return null; } public override QueryExpression VisitMatchText(MatchTextExpression expression, TArgument argument) { - if (expression == null) + if (expression != null) { - return null; - } + var newTargetAttribute = Visit(expression.TargetAttribute, argument) as ResourceFieldChainExpression; + var newTextValue = Visit(expression.TextValue, argument) as LiteralConstantExpression; - var newTargetAttribute = Visit(expression.TargetAttribute, argument) as ResourceFieldChainExpression; - var newTextValue = Visit(expression.TextValue, argument) as LiteralConstantExpression; + var newExpression = new MatchTextExpression(newTargetAttribute, newTextValue, expression.MatchKind); + return newExpression.Equals(expression) ? expression : newExpression; + } - var newExpression = new MatchTextExpression(newTargetAttribute, newTextValue, expression.MatchKind); - return newExpression.Equals(expression) ? expression : newExpression; + return null; } public override QueryExpression VisitEqualsAnyOf(EqualsAnyOfExpression expression, TArgument argument) { - if (expression == null) + if (expression != null) { - return null; - } + var newTargetAttribute = Visit(expression.TargetAttribute, argument) as ResourceFieldChainExpression; + var newConstants = VisitSequence(expression.Constants, argument); - var newTargetAttribute = Visit(expression.TargetAttribute, argument) as ResourceFieldChainExpression; - var newConstants = VisitSequence(expression.Constants, argument); + var newExpression = new EqualsAnyOfExpression(newTargetAttribute, newConstants); + return newExpression.Equals(expression) ? expression : newExpression; + } - var newExpression = new EqualsAnyOfExpression(newTargetAttribute, newConstants); - return newExpression.Equals(expression) ? expression : newExpression; + return null; } public override QueryExpression VisitSparseFieldSet(SparseFieldSetExpression expression, TArgument argument) @@ -221,79 +198,79 @@ public override QueryExpression VisitSparseFieldSet(SparseFieldSetExpression exp public override QueryExpression VisitQueryStringParameterScope(QueryStringParameterScopeExpression expression, TArgument argument) { - if (expression == null) + if (expression != null) { - return null; - } + var newParameterName = Visit(expression.ParameterName, argument) as LiteralConstantExpression; - var newParameterName = Visit(expression.ParameterName, argument) as LiteralConstantExpression; + var newScope = expression.Scope != null + ? Visit(expression.Scope, argument) as ResourceFieldChainExpression + : null; - var newScope = expression.Scope != null - ? Visit(expression.Scope, argument) as ResourceFieldChainExpression - : null; + var newExpression = new QueryStringParameterScopeExpression(newParameterName, newScope); + return newExpression.Equals(expression) ? expression : newExpression; + } - var newExpression = new QueryStringParameterScopeExpression(newParameterName, newScope); - return newExpression.Equals(expression) ? expression : newExpression; + return null; } public override QueryExpression PaginationQueryStringValue(PaginationQueryStringValueExpression expression, TArgument argument) { - if (expression == null) + if (expression != null) { - return null; - } + var newElements = VisitSequence(expression.Elements, argument); - var newElements = VisitSequence(expression.Elements, argument); + var newExpression = new PaginationQueryStringValueExpression(newElements); + return newExpression.Equals(expression) ? expression : newExpression; + } - var newExpression = new PaginationQueryStringValueExpression(newElements); - return newExpression.Equals(expression) ? expression : newExpression; + return null; } public override QueryExpression PaginationElementQueryStringValue(PaginationElementQueryStringValueExpression expression, TArgument argument) { - if (expression == null) + if (expression != null) { - return null; - } + var newScope = expression.Scope != null + ? Visit(expression.Scope, argument) as ResourceFieldChainExpression + : null; - var newScope = expression.Scope != null - ? Visit(expression.Scope, argument) as ResourceFieldChainExpression - : null; + var newExpression = new PaginationElementQueryStringValueExpression(newScope, expression.Value); + return newExpression.Equals(expression) ? expression : newExpression; + } - var newExpression = new PaginationElementQueryStringValueExpression(newScope, expression.Value); - return newExpression.Equals(expression) ? expression : newExpression; + return null; } public override QueryExpression VisitInclude(IncludeExpression expression, TArgument argument) { - if (expression == null) + if (expression != null) { - return null; - } + var newElements = VisitSequence(expression.Elements, argument); - var newElements = VisitSequence(expression.Elements, argument); + if (newElements.Count == 0) + { + return IncludeExpression.Empty; + } - if (newElements.Count == 0) - { - return IncludeExpression.Empty; + var newExpression = new IncludeExpression(newElements); + return newExpression.Equals(expression) ? expression : newExpression; } - var newExpression = new IncludeExpression(newElements); - return newExpression.Equals(expression) ? expression : newExpression; + return null; } public override QueryExpression VisitIncludeElement(IncludeElementExpression expression, TArgument argument) { - if (expression == null) + if (expression != null) { - return null; - } + var newElements = VisitSequence(expression.Children, argument); - var newElements = VisitSequence(expression.Children, argument); + var newExpression = new IncludeElementExpression(expression.Relationship, newElements); + return newExpression.Equals(expression) ? expression : newExpression; + } - var newExpression = new IncludeElementExpression(expression.Relationship, newElements); - return newExpression.Equals(expression) ? expression : newExpression; + return null; } public override QueryExpression VisitQueryableHandler(QueryableHandlerExpression expression, TArgument argument) @@ -309,9 +286,7 @@ protected virtual IReadOnlyCollection VisitSequence(IE foreach (TExpression element in elements) { - var newElement = Visit(element, argument) as TExpression; - - if (newElement != null) + if (Visit(element, argument) is TExpression newElement) { newElements.Add(newElement); } From e5aca62c014c1142b96d4963612e930c01a7cd03 Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Wed, 9 Sep 2020 18:27:39 +0200 Subject: [PATCH 4/4] Review feedback --- .../Queries/Expressions/QueryExpressionRewriter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/JsonApiDotNetCore/Queries/Expressions/QueryExpressionRewriter.cs b/src/JsonApiDotNetCore/Queries/Expressions/QueryExpressionRewriter.cs index b1a42bd886..5d2babea1e 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/QueryExpressionRewriter.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/QueryExpressionRewriter.cs @@ -4,7 +4,7 @@ namespace JsonApiDotNetCore.Queries.Expressions { /// - /// Building block for rewriting trees. It walks nested expressions and updates parent on changes. + /// Building block for rewriting trees. It walks through nested expressions and updates parent on changes. /// public class QueryExpressionRewriter : QueryExpressionVisitor {