From 969e60785de0dbf4d912368c4d71c4e532ff35a8 Mon Sep 17 00:00:00 2001 From: Wojciech Klusek Date: Mon, 11 Dec 2023 13:49:27 +0100 Subject: [PATCH 01/10] Hide IEquatable and EqualityContract from record dtos --- .../Compilation/ContractTypes.cs | 29 +++++++++++++++++++ .../Generation/ContractsGenerator.cs | 10 ++++--- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/src/LeanCode.ContractsGenerator/Compilation/ContractTypes.cs b/src/LeanCode.ContractsGenerator/Compilation/ContractTypes.cs index 24f0b8a..dfa61c5 100644 --- a/src/LeanCode.ContractsGenerator/Compilation/ContractTypes.cs +++ b/src/LeanCode.ContractsGenerator/Compilation/ContractTypes.cs @@ -9,6 +9,8 @@ namespace LeanCode.ContractsGenerator.Compilation; public sealed class ContractTypes { + private const string RecordEqualityContractPropertyName = "EqualityContract"; + private HashSet QueryType { get; } private HashSet CommandType { get; } private HashSet OperationType { get; } @@ -26,6 +28,9 @@ public sealed class ContractTypes private HashSet ReadOnlyDictionary { get; } private HashSet Dictionary { get; } + private HashSet Equatable { get; } + private HashSet Type { get; } + public ContractTypes(IReadOnlyCollection compilations) { QueryType = GetUnboundTypeSymbols(compilations, typeof(IQuery<>)); @@ -51,6 +56,9 @@ public ContractTypes(IReadOnlyCollection compilations) AttributeUsageAttribute = GetTypeSymbols(compilations); ReadOnlyDictionary = GetUnboundTypeSymbols(compilations, typeof(IReadOnlyDictionary<,>)); Dictionary = GetUnboundTypeSymbols(compilations, typeof(IDictionary<,>)); + + Equatable = GetUnboundTypeSymbols(compilations, typeof(IEquatable<>)); + Type = GetTypeSymbols(compilations); } private static HashSet GetTypeSymbols( @@ -222,4 +230,25 @@ public bool IsReadOnlyDictionary(ITypeSymbol i) || Dictionary.Contains(ns.ConstructUnboundGenericType()) ); } + + public bool IsRecordEquatable(ITypeSymbol i) + { + return i is INamedTypeSymbol ns + && ns.IsGenericType + && ns.TypeArguments.FirstOrDefault() is INamedTypeSymbol namedType + && namedType.IsRecord + && Equatable.Contains(ns.ConstructUnboundGenericType()); + } + + /// + /// `ContainingType` is not accessible from an `ITypeSymbol`. + /// Hence, passing the `IPropertySymbol`. + /// + public bool IsRecordEqualityContract(IPropertySymbol i) + { + return i.ContainingType is INamedTypeSymbol ns + && i.Name == RecordEqualityContractPropertyName + && ns.IsRecord + && Type.Contains(i.Type); + } } diff --git a/src/LeanCode.ContractsGenerator/Generation/ContractsGenerator.cs b/src/LeanCode.ContractsGenerator/Generation/ContractsGenerator.cs index ace3610..f370ebb 100644 --- a/src/LeanCode.ContractsGenerator/Generation/ContractsGenerator.cs +++ b/src/LeanCode.ContractsGenerator/Generation/ContractsGenerator.cs @@ -176,14 +176,16 @@ private bool IsIgnored( || symbol.SpecialType == SpecialType.System_Enum || ErrorCodes.IsErrorCode(symbol) || contracts.Types.IsProduceNotificationType(symbol) - || contracts.Types.IsAttributeUsageType(symbol); + || contracts.Types.IsAttributeUsageType(symbol) + || contracts.Types.IsRecordEquatable(symbol); } private bool IsExcluded(ISymbol symbol) { - return symbol - .GetAttributes() - .Any(a => contracts.Types.IsExcludeFromContractsGenerationType(a.AttributeClass)); + return (symbol is IPropertySymbol ps && contracts.Types.IsRecordEqualityContract(ps)) + || symbol + .GetAttributes() + .Any(a => contracts.Types.IsExcludeFromContractsGenerationType(a.AttributeClass)); } private static HashSet GatherBaseProperties(INamedTypeSymbol ns) From 355fa51e26ff372c9fc7bd73e6114c35ba8bcf39 Mon Sep 17 00:00:00 2001 From: Wojciech Klusek Date: Mon, 11 Dec 2023 13:49:40 +0100 Subject: [PATCH 02/10] Add tests --- examples/properties/inner_record.cs | 7 ++++++ examples/properties/record.cs | 1 + examples/simple/inheritance.cs | 4 +++ examples/simple/record.cs | 1 + examples/simple/record_class.cs | 1 + examples/simple/record_struct.cs | 1 + .../ExampleBased/Properties.cs | 16 ++++++++++++ .../ExampleBased/Simple.cs | 25 ++++++++++++++++++- 8 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 examples/properties/inner_record.cs create mode 100644 examples/properties/record.cs create mode 100644 examples/simple/record.cs create mode 100644 examples/simple/record_class.cs create mode 100644 examples/simple/record_struct.cs diff --git a/examples/properties/inner_record.cs b/examples/properties/inner_record.cs new file mode 100644 index 0000000..e151829 --- /dev/null +++ b/examples/properties/inner_record.cs @@ -0,0 +1,7 @@ +public class DTO +{ + InnerDTO A { get; set; } + InnerDTO? B { get; set; } +} + +public record InnerDTO(); diff --git a/examples/properties/record.cs b/examples/properties/record.cs new file mode 100644 index 0000000..9b9f12a --- /dev/null +++ b/examples/properties/record.cs @@ -0,0 +1 @@ +public record DTO(int A); diff --git a/examples/simple/inheritance.cs b/examples/simple/inheritance.cs index 4f72042..ae23ca6 100644 --- a/examples/simple/inheritance.cs +++ b/examples/simple/inheritance.cs @@ -13,3 +13,7 @@ public class C : B, A public int PropA { get; set; } public int PropC { get; set; } } + +public record D(int PropD); + +public record E(int PropD, int PropE) : D(PropD); diff --git a/examples/simple/record.cs b/examples/simple/record.cs new file mode 100644 index 0000000..3f4d039 --- /dev/null +++ b/examples/simple/record.cs @@ -0,0 +1 @@ +public record DTO(); diff --git a/examples/simple/record_class.cs b/examples/simple/record_class.cs new file mode 100644 index 0000000..13a80fd --- /dev/null +++ b/examples/simple/record_class.cs @@ -0,0 +1 @@ +public record class DTO(); diff --git a/examples/simple/record_struct.cs b/examples/simple/record_struct.cs new file mode 100644 index 0000000..4efb97a --- /dev/null +++ b/examples/simple/record_struct.cs @@ -0,0 +1 @@ +public record struct DTO(); diff --git a/src/LeanCode.ContractsGenerator.Tests/ExampleBased/Properties.cs b/src/LeanCode.ContractsGenerator.Tests/ExampleBased/Properties.cs index a42ade5..418edf6 100644 --- a/src/LeanCode.ContractsGenerator.Tests/ExampleBased/Properties.cs +++ b/src/LeanCode.ContractsGenerator.Tests/ExampleBased/Properties.cs @@ -68,4 +68,20 @@ public void Properties_with_struct_types() .WithProperty("A", TypeRefExtensions.Internal("InnerDTO")) .WithProperty("B", TypeRefExtensions.Internal("InnerDTO").Nullable()); } + + [Fact] + public void Properties_inside_record_types() + { + "properties/record.cs".Compiles().WithDto("DTO").WithProperty("A", Known(KnownType.Int32)); + } + + [Fact] + public void Properties_with_record_types() + { + "properties/inner_record.cs" + .Compiles() + .WithDto("DTO") + .WithProperty("A", TypeRefExtensions.Internal("InnerDTO")) + .WithProperty("B", TypeRefExtensions.Internal("InnerDTO").Nullable()); + } } diff --git a/src/LeanCode.ContractsGenerator.Tests/ExampleBased/Simple.cs b/src/LeanCode.ContractsGenerator.Tests/ExampleBased/Simple.cs index 8d0f1e5..5b06a9c 100644 --- a/src/LeanCode.ContractsGenerator.Tests/ExampleBased/Simple.cs +++ b/src/LeanCode.ContractsGenerator.Tests/ExampleBased/Simple.cs @@ -64,6 +64,24 @@ public void Simple_Struct() "simple/struct.cs".Compiles().WithSingle().Dto("DTO"); } + [Fact] + public void Simple_Record() + { + "simple/record.cs".Compiles().WithSingle().Dto("DTO"); + } + + [Fact] + public void Record_Struct() + { + "simple/record_struct.cs".Compiles().WithSingle().Dto("DTO"); + } + + [Fact] + public void Record_Class() + { + "simple/record_class.cs".Compiles().WithSingle().Dto("DTO"); + } + [Fact] public void Simple_Enum() { @@ -88,7 +106,12 @@ public void Inherited_properties() .WithDto("C") .WithProperty("PropC", Known(KnownType.Int32)) .WithoutProperty("PropA") - .WithoutProperty("PropB"); + .WithoutProperty("PropB") + .WithDto("D") + .WithProperty("PropD", Known(KnownType.Int32)) + .WithDto("E") + .WithProperty("PropE", Known(KnownType.Int32)) + .WithoutProperty("PropD"); } [Fact] From fbfdbb035210562b5774eb08ea4bb470727abb43 Mon Sep 17 00:00:00 2001 From: Wojciech Klusek Date: Mon, 11 Dec 2023 13:56:19 +0100 Subject: [PATCH 03/10] Reorder conditions --- src/LeanCode.ContractsGenerator/Compilation/ContractTypes.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LeanCode.ContractsGenerator/Compilation/ContractTypes.cs b/src/LeanCode.ContractsGenerator/Compilation/ContractTypes.cs index dfa61c5..054f9e0 100644 --- a/src/LeanCode.ContractsGenerator/Compilation/ContractTypes.cs +++ b/src/LeanCode.ContractsGenerator/Compilation/ContractTypes.cs @@ -247,8 +247,8 @@ public bool IsRecordEquatable(ITypeSymbol i) public bool IsRecordEqualityContract(IPropertySymbol i) { return i.ContainingType is INamedTypeSymbol ns - && i.Name == RecordEqualityContractPropertyName && ns.IsRecord + && i.Name == RecordEqualityContractPropertyName && Type.Contains(i.Type); } } From 8933828b4eb278b1c28937f249102bdecc61598b Mon Sep 17 00:00:00 2001 From: Wojciech Klusek Date: Mon, 11 Dec 2023 14:20:22 +0100 Subject: [PATCH 04/10] Test for absence of EqualityContract property --- .../ExampleBased/Properties.cs | 6 +++++- .../Compilation/ContractTypes.cs | 11 ++--------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/LeanCode.ContractsGenerator.Tests/ExampleBased/Properties.cs b/src/LeanCode.ContractsGenerator.Tests/ExampleBased/Properties.cs index 418edf6..314311e 100644 --- a/src/LeanCode.ContractsGenerator.Tests/ExampleBased/Properties.cs +++ b/src/LeanCode.ContractsGenerator.Tests/ExampleBased/Properties.cs @@ -72,7 +72,11 @@ public void Properties_with_struct_types() [Fact] public void Properties_inside_record_types() { - "properties/record.cs".Compiles().WithDto("DTO").WithProperty("A", Known(KnownType.Int32)); + "properties/record.cs" + .Compiles() + .WithDto("DTO") + .WithProperty("A", Known(KnownType.Int32)) + .WithoutProperty("EqualityContract"); } [Fact] diff --git a/src/LeanCode.ContractsGenerator/Compilation/ContractTypes.cs b/src/LeanCode.ContractsGenerator/Compilation/ContractTypes.cs index 054f9e0..182c03c 100644 --- a/src/LeanCode.ContractsGenerator/Compilation/ContractTypes.cs +++ b/src/LeanCode.ContractsGenerator/Compilation/ContractTypes.cs @@ -29,7 +29,6 @@ public sealed class ContractTypes private HashSet Dictionary { get; } private HashSet Equatable { get; } - private HashSet Type { get; } public ContractTypes(IReadOnlyCollection compilations) { @@ -58,7 +57,6 @@ public ContractTypes(IReadOnlyCollection compilations) Dictionary = GetUnboundTypeSymbols(compilations, typeof(IDictionary<,>)); Equatable = GetUnboundTypeSymbols(compilations, typeof(IEquatable<>)); - Type = GetTypeSymbols(compilations); } private static HashSet GetTypeSymbols( @@ -235,20 +233,15 @@ public bool IsRecordEquatable(ITypeSymbol i) { return i is INamedTypeSymbol ns && ns.IsGenericType - && ns.TypeArguments.FirstOrDefault() is INamedTypeSymbol namedType + && ns.TypeArguments is [INamedTypeSymbol namedType] && namedType.IsRecord && Equatable.Contains(ns.ConstructUnboundGenericType()); } - /// - /// `ContainingType` is not accessible from an `ITypeSymbol`. - /// Hence, passing the `IPropertySymbol`. - /// public bool IsRecordEqualityContract(IPropertySymbol i) { return i.ContainingType is INamedTypeSymbol ns && ns.IsRecord - && i.Name == RecordEqualityContractPropertyName - && Type.Contains(i.Type); + && i.Name == RecordEqualityContractPropertyName; } } From 8acdef6d51bc2c0c2d5d058e73449fb32d82b66e Mon Sep 17 00:00:00 2001 From: Wojciech Klusek Date: Mon, 11 Dec 2023 14:20:41 +0100 Subject: [PATCH 05/10] Remove `Do not use records` paragaph from docs --- docs/guidelines.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/docs/guidelines.md b/docs/guidelines.md index 840267b..805b4df 100644 --- a/docs/guidelines.md +++ b/docs/guidelines.md @@ -7,11 +7,6 @@ The properties are generated as-is, even if they don't have getter/setter or if getter/setter is not public. Using anything else results in C#/contracts mismatch because client types _will_ have public getters and setters. -### Do not use records - -Records introduce logic (structural equality) to your code, which cannot be translated. This might lead to broken -contracts usage. - ### Prefer `List` over array ### Prefer concrete types instead of interfaces From 54912232742c043f5005935bb54ba4032767bc6d Mon Sep 17 00:00:00 2001 From: Wojciech Klusek Date: Tue, 12 Dec 2023 08:53:07 +0100 Subject: [PATCH 06/10] Add records in cqrs test --- .../supported_use_cases/records_in_cqrs.cs | 19 +++++++++++++++++++ .../ExampleBased/SupportedUseCases.cs | 12 ++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 examples/supported_use_cases/records_in_cqrs.cs diff --git a/examples/supported_use_cases/records_in_cqrs.cs b/examples/supported_use_cases/records_in_cqrs.cs new file mode 100644 index 0000000..e7ec363 --- /dev/null +++ b/examples/supported_use_cases/records_in_cqrs.cs @@ -0,0 +1,19 @@ +using LeanCode.Contracts; + +public record DTO1(int Property); +public record DTO2(string Property); + +public class Command : ICommand +{ + public DTO1 DTO1 { get; set; } +} + +public class Query : IQuery +{ + public DTO1 DTO1 { get; set; } +} + +public class Operation : IOperation +{ + public DTO1 DTO1 { get; set; } +} diff --git a/src/LeanCode.ContractsGenerator.Tests/ExampleBased/SupportedUseCases.cs b/src/LeanCode.ContractsGenerator.Tests/ExampleBased/SupportedUseCases.cs index e62ab79..051619c 100644 --- a/src/LeanCode.ContractsGenerator.Tests/ExampleBased/SupportedUseCases.cs +++ b/src/LeanCode.ContractsGenerator.Tests/ExampleBased/SupportedUseCases.cs @@ -177,4 +177,16 @@ public void Produce_notification_from_inherited_type() ) ); } + + [Fact] + public void Records_in_cqrs() + { + "supported_use_cases/records_in_cqrs.cs" + .Compiles() + .WithDto("DTO1") + .WithDto("DTO2") + .WithQuery("Query") + .WithCommand("Command") + .WithOperation("Operation"); + } } From b50047e67ed47e174fe4fcb157582dff8e71c593 Mon Sep 17 00:00:00 2001 From: Wojciech Klusek Date: Tue, 12 Dec 2023 08:54:29 +0100 Subject: [PATCH 07/10] Update CHANGELOG --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7472b0c..571267b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 #### Added -- `ITopic` and `IProduceNotification<>` interfaces and their support in the generator. +- `ITopic` and `IProduceNotification<>` interfaces and their support in the generator, +- Support for `records`. #### Breaking changes From efdd38f0b5c321e5b4b5a8640e197a4aba38c953 Mon Sep 17 00:00:00 2001 From: Wojciech Klusek Date: Tue, 12 Dec 2023 08:55:21 +0100 Subject: [PATCH 08/10] Fix format --- examples/supported_use_cases/records_in_cqrs.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/supported_use_cases/records_in_cqrs.cs b/examples/supported_use_cases/records_in_cqrs.cs index e7ec363..24cdba7 100644 --- a/examples/supported_use_cases/records_in_cqrs.cs +++ b/examples/supported_use_cases/records_in_cqrs.cs @@ -1,6 +1,7 @@ using LeanCode.Contracts; public record DTO1(int Property); + public record DTO2(string Property); public class Command : ICommand From 1306349f90ac10c45e6d16120d2415a1c281b3d7 Mon Sep 17 00:00:00 2001 From: Wojciech Klusek Date: Tue, 12 Dec 2023 09:00:44 +0100 Subject: [PATCH 09/10] Make `IsRecordEqualityContract` static --- src/LeanCode.ContractsGenerator/Compilation/ContractTypes.cs | 2 +- .../Generation/ContractsGenerator.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/LeanCode.ContractsGenerator/Compilation/ContractTypes.cs b/src/LeanCode.ContractsGenerator/Compilation/ContractTypes.cs index 182c03c..17fb897 100644 --- a/src/LeanCode.ContractsGenerator/Compilation/ContractTypes.cs +++ b/src/LeanCode.ContractsGenerator/Compilation/ContractTypes.cs @@ -238,7 +238,7 @@ public bool IsRecordEquatable(ITypeSymbol i) && Equatable.Contains(ns.ConstructUnboundGenericType()); } - public bool IsRecordEqualityContract(IPropertySymbol i) + public static bool IsRecordEqualityContract(IPropertySymbol i) { return i.ContainingType is INamedTypeSymbol ns && ns.IsRecord diff --git a/src/LeanCode.ContractsGenerator/Generation/ContractsGenerator.cs b/src/LeanCode.ContractsGenerator/Generation/ContractsGenerator.cs index f370ebb..a1e6fae 100644 --- a/src/LeanCode.ContractsGenerator/Generation/ContractsGenerator.cs +++ b/src/LeanCode.ContractsGenerator/Generation/ContractsGenerator.cs @@ -182,7 +182,7 @@ private bool IsIgnored( private bool IsExcluded(ISymbol symbol) { - return (symbol is IPropertySymbol ps && contracts.Types.IsRecordEqualityContract(ps)) + return (symbol is IPropertySymbol ps && ContractTypes.IsRecordEqualityContract(ps)) || symbol .GetAttributes() .Any(a => contracts.Types.IsExcludeFromContractsGenerationType(a.AttributeClass)); From c24a606f8cd119666ea8e1233a42967dfb17f8cb Mon Sep 17 00:00:00 2001 From: Wojciech Klusek Date: Tue, 12 Dec 2023 10:52:18 +0100 Subject: [PATCH 10/10] Test records as cqrs --- .../supported_use_cases/records_as_cqrs.cs | 11 ++++++++++ .../supported_use_cases/records_in_cqrs.cs | 20 ------------------- .../ExampleBased/SupportedUseCases.cs | 4 ++-- 3 files changed, 13 insertions(+), 22 deletions(-) create mode 100644 examples/supported_use_cases/records_as_cqrs.cs delete mode 100644 examples/supported_use_cases/records_in_cqrs.cs diff --git a/examples/supported_use_cases/records_as_cqrs.cs b/examples/supported_use_cases/records_as_cqrs.cs new file mode 100644 index 0000000..3f41635 --- /dev/null +++ b/examples/supported_use_cases/records_as_cqrs.cs @@ -0,0 +1,11 @@ +using LeanCode.Contracts; + +public record DTO1(int Property); + +public record DTO2(string Property); + +public record Command(DTO1 DTO1) : ICommand; + +public record Query(DTO1 DTO1) : IQuery; + +public record Operation(DTO1 DTO1) : IOperation; diff --git a/examples/supported_use_cases/records_in_cqrs.cs b/examples/supported_use_cases/records_in_cqrs.cs deleted file mode 100644 index 24cdba7..0000000 --- a/examples/supported_use_cases/records_in_cqrs.cs +++ /dev/null @@ -1,20 +0,0 @@ -using LeanCode.Contracts; - -public record DTO1(int Property); - -public record DTO2(string Property); - -public class Command : ICommand -{ - public DTO1 DTO1 { get; set; } -} - -public class Query : IQuery -{ - public DTO1 DTO1 { get; set; } -} - -public class Operation : IOperation -{ - public DTO1 DTO1 { get; set; } -} diff --git a/src/LeanCode.ContractsGenerator.Tests/ExampleBased/SupportedUseCases.cs b/src/LeanCode.ContractsGenerator.Tests/ExampleBased/SupportedUseCases.cs index 051619c..f64775b 100644 --- a/src/LeanCode.ContractsGenerator.Tests/ExampleBased/SupportedUseCases.cs +++ b/src/LeanCode.ContractsGenerator.Tests/ExampleBased/SupportedUseCases.cs @@ -179,9 +179,9 @@ public void Produce_notification_from_inherited_type() } [Fact] - public void Records_in_cqrs() + public void Records_as_cqrs() { - "supported_use_cases/records_in_cqrs.cs" + "supported_use_cases/records_as_cqrs.cs" .Compiles() .WithDto("DTO1") .WithDto("DTO2")