diff --git a/compiler/Main.hs b/compiler/Main.hs index dfbed52d10..5e0565e6ec 100644 --- a/compiler/Main.hs +++ b/compiler/Main.hs @@ -112,11 +112,11 @@ csCodegen options@Cs {..} = do typeMapping = if collection_interfaces then csCollectionInterfacesTypeMapping else csTypeMapping - fieldMapping = if readonly_properties - then ReadOnlyProperties - else if fields - then PublicFields - else Properties + fieldMapping + | readonly_properties = ReadOnlyProperties + | init_only_properties = InitOnlyProperties + | fields = PublicFields + | otherwise = Properties constructorOptions = if constructor_parameters then ConstructorParameters else DefaultWithProtectedBase diff --git a/compiler/Options.hs b/compiler/Options.hs index 573c8d5c6d..236fe11412 100644 --- a/compiler/Options.hs +++ b/compiler/Options.hs @@ -53,6 +53,7 @@ data Options , namespace :: [String] , collection_interfaces :: Bool , readonly_properties :: Bool + , init_only_properties :: Bool , fields :: Bool , jobs :: Maybe Int , no_banner :: Bool @@ -106,9 +107,10 @@ cs :: Options cs = Cs { collection_interfaces = def &= name "c" &= help "Use interfaces rather than concrete collection types" , readonly_properties = def &= name "r" &= help "Generate private property setters" + , init_only_properties = def &= name "init-only" &= help "Generate init-only property setters" , fields = def &= name "f" &= help "Generate public fields rather than properties" , structs_enabled = True &= explicit &= name "structs" &= help "Generate C# types for Bond structs and enums (true by default, use \"--structs=false\" to disable)" - , constructor_parameters = def &= explicit &= name "preview-constructor-parameters" &= help "PREVIEW FEATURE: Generate a constructor that takes all the fields as parameters. Typically used with readonly-properties." + , constructor_parameters = def &= explicit &= name "preview-constructor-parameters" &= help "PREVIEW FEATURE: Generate a constructor that takes all the fields as parameters. Typically used with readonly-properties or init-only-properties." } &= name "c#" &= help "Generate C# code" diff --git a/compiler/src/Language/Bond/Codegen/Cs/Types_cs.hs b/compiler/src/Language/Bond/Codegen/Cs/Types_cs.hs index 50854607bf..a136d03d27 100644 --- a/compiler/src/Language/Bond/Codegen/Cs/Types_cs.hs +++ b/compiler/src/Language/Bond/Codegen/Cs/Types_cs.hs @@ -31,7 +31,8 @@ data StructMapping = data FieldMapping = PublicFields | -- ^ public fields Properties | -- ^ auto-properties - ReadOnlyProperties -- ^ auto-properties with private setter + ReadOnlyProperties | -- ^ auto-properties with private setter + InitOnlyProperties -- ^ auto-properties with init-only setter deriving Eq -- | Options for how constructors should be generated. @@ -186,6 +187,7 @@ namespace #{csNamespace} PublicFields -> [lt|#{optional fieldInitializer $ csDefault f};|] Properties -> [lt| { get; set; }|] ReadOnlyProperties -> [lt| { get; private set; }|] + InitOnlyProperties -> [lt| { get; init; }|] fieldInitializer x = [lt| = #{x}|] new = if isBaseField fieldName structBase then "new " else "" :: String diff --git a/compiler/tests/TestMain.hs b/compiler/tests/TestMain.hs index ca60f46b8b..aa1b745863 100644 --- a/compiler/tests/TestMain.hs +++ b/compiler/tests/TestMain.hs @@ -151,6 +151,13 @@ tests = testGroup "Compiler tests" ] "complex_inheritance" "constructor-parameters" + , verifyCodegenVariation + [ "c#" + , "--preview-constructor-parameters" + , "--init-only-properties" + ] + "complex_inheritance" + "constructor-parameters_init" , verifyCodegenVariation [ "c#" , "--preview-constructor-parameters" @@ -180,6 +187,13 @@ tests = testGroup "Compiler tests" ] "empty_struct" "constructor-parameters" + , verifyCodegenVariation + [ "c#" + , "--preview-constructor-parameters" + , "--init-only-properties" + ] + "empty_struct" + "constructor-parameters_init" ] , testGroup "Java" [ verifyJavaCodegen "attributes" diff --git a/compiler/tests/Tests/Codegen.hs b/compiler/tests/Tests/Codegen.hs index e997b1d7db..b174b8cf5f 100644 --- a/compiler/tests/Tests/Codegen.hs +++ b/compiler/tests/Tests/Codegen.hs @@ -91,11 +91,11 @@ verifyFiles options baseName variation = extra options where verify = verifyFile options baseName - fieldMapping Cs {..} = if readonly_properties - then ReadOnlyProperties - else if fields - then PublicFields - else Properties + fieldMapping Cs {..} + | readonly_properties = ReadOnlyProperties + | init_only_properties = InitOnlyProperties + | fields = PublicFields + | otherwise = Properties constructorOptions Cs {..} = if constructor_parameters then ConstructorParameters else DefaultWithProtectedBase diff --git a/compiler/tests/generated/constructor-parameters_init/collection-interfaces/complex_inheritance_types.cs b/compiler/tests/generated/constructor-parameters_init/collection-interfaces/complex_inheritance_types.cs new file mode 100644 index 0000000000..07b45e2a00 --- /dev/null +++ b/compiler/tests/generated/constructor-parameters_init/collection-interfaces/complex_inheritance_types.cs @@ -0,0 +1,210 @@ + + +// suppress "Missing XML comment for publicly visible type or member" +#pragma warning disable 1591 + + +#region ReSharper warnings +// ReSharper disable PartialTypeWithSinglePart +// ReSharper disable RedundantNameQualifier +// ReSharper disable InconsistentNaming +// ReSharper disable CheckNamespace +// ReSharper disable UnusedParameter.Local +// ReSharper disable RedundantUsingDirective +#endregion + +namespace Test +{ + using System.Collections.Generic; + + [System.CodeDom.Compiler.GeneratedCode("gbc", "0.12.1.0")] + public enum TestEnum + { + EnumVal1, + EnumVal2, + EnumVal3, + } + + [global::Bond.Schema] + [System.CodeDom.Compiler.GeneratedCode("gbc", "0.12.1.0")] + public partial class Simple + { + [global::Bond.Id(0)] + public int someInt { get; init; } + + [global::Bond.Id(1)] + public int anotherInt { get; init; } + + [global::Bond.Id(2), global::Bond.Type(typeof(global::Bond.Tag.wstring))] + public string someString { get; init; } + + public Simple( + int someInt, + int anotherInt, + string someString) + { + this.someInt = someInt; + this.anotherInt = anotherInt; + this.someString = someString; + } + + public Simple() + { + someString = ""; + } + } + + [global::Bond.Schema] + [System.CodeDom.Compiler.GeneratedCode("gbc", "0.12.1.0")] + public partial class Foo + { + [global::Bond.Id(0), global::Bond.Type(typeof(global::Bond.Tag.wstring))] + public string someText { get; init; } + + public Foo( + string someText) + { + this.someText = someText; + } + + public Foo() + { + someText = "BaseText1"; + } + } + + [global::Bond.Schema] + [System.CodeDom.Compiler.GeneratedCode("gbc", "0.12.1.0")] + public partial class Bar + : Foo + { + [global::Bond.Id(0)] + public TestEnum testEnum { get; init; } + + [global::Bond.Id(1), global::Bond.Type(typeof(global::Bond.Tag.wstring))] + new public string someText { get; init; } + + [global::Bond.Id(2)] + public int someInt { get; init; } + + [global::Bond.Id(3), global::Bond.Type(typeof(global::Bond.Tag.wstring))] + public string moreText { get; init; } + + [global::Bond.Id(4), global::Bond.Type(typeof(List))] + public IList someList { get; init; } + + [global::Bond.Id(5), global::Bond.Type(typeof(Dictionary))] + public IDictionary someMap { get; init; } + + [global::Bond.Id(6), global::Bond.Type(typeof(HashSet))] + public ISet someSet { get; init; } + + public Bar( + // Base class parameters + string someText, + + // This class parameters + TestEnum testEnum, + string someText0, + int someInt, + string moreText, + IList someList, + IDictionary someMap, + ISet someSet + ) : base( + someText) + { + this.testEnum = testEnum; + this.someText = someText0; + this.someInt = someInt; + this.moreText = moreText; + this.someList = someList; + this.someMap = someMap; + this.someSet = someSet; + } + + public Bar() + { + testEnum = TestEnum.Val2; + someText = "DerivedText1"; + moreText = ""; + someList = new List(); + someMap = new Dictionary(); + someSet = new HashSet(); + } + } + + [global::Bond.Schema] + [System.CodeDom.Compiler.GeneratedCode("gbc", "0.12.1.0")] + public partial class Baz + : Bar + { + [global::Bond.Id(0), global::Bond.Type(typeof(global::Bond.Tag.wstring))] + new public string someText { get; init; } + + [global::Bond.Id(1), global::Bond.Type(typeof(global::Bond.Tag.wstring))] + public string evenMoreText { get; init; } + + [global::Bond.Id(2), global::Bond.Type(typeof(global::Bond.Tag.wstring))] + public string someText1 { get; init; } + + public Baz( + // Base class parameters + string someText, + TestEnum testEnum, + string someText0, + int someInt, + string moreText, + IList someList, + IDictionary someMap, + ISet someSet, + + // This class parameters + string someText1, + string evenMoreText, + string someText10 + ) : base( + someText, + testEnum, + someText0, + someInt, + moreText, + someList, + someMap, + someSet) + { + this.someText = someText1; + this.evenMoreText = evenMoreText; + this.someText1 = someText10; + } + + public Baz() + { + someText = ""; + evenMoreText = ""; + someText1 = ""; + } + } + + [global::Bond.Schema] + [System.CodeDom.Compiler.GeneratedCode("gbc", "0.12.1.0")] + public partial class DerivedEmpty + : Foo + { + + + public DerivedEmpty( + // Base class parameters + string someText + ) : base( + someText) + { + + } + + public DerivedEmpty() + { + + } + } +} // Test diff --git a/compiler/tests/generated/constructor-parameters_init/collection-interfaces/empty_struct_types.cs b/compiler/tests/generated/constructor-parameters_init/collection-interfaces/empty_struct_types.cs new file mode 100644 index 0000000000..eddf7b0689 --- /dev/null +++ b/compiler/tests/generated/constructor-parameters_init/collection-interfaces/empty_struct_types.cs @@ -0,0 +1,30 @@ + + +// suppress "Missing XML comment for publicly visible type or member" +#pragma warning disable 1591 + + +#region ReSharper warnings +// ReSharper disable PartialTypeWithSinglePart +// ReSharper disable RedundantNameQualifier +// ReSharper disable InconsistentNaming +// ReSharper disable CheckNamespace +// ReSharper disable UnusedParameter.Local +// ReSharper disable RedundantUsingDirective +#endregion + +namespace tests +{ + using System.Collections.Generic; + + [global::Bond.Schema] + [System.CodeDom.Compiler.GeneratedCode("gbc", "0.12.1.0")] + public partial class Empty + { + + public Empty() + { + + } + } +} // tests diff --git a/compiler/tests/generated/constructor-parameters_init/complex_inheritance_types.cs b/compiler/tests/generated/constructor-parameters_init/complex_inheritance_types.cs new file mode 100644 index 0000000000..496858df55 --- /dev/null +++ b/compiler/tests/generated/constructor-parameters_init/complex_inheritance_types.cs @@ -0,0 +1,210 @@ + + +// suppress "Missing XML comment for publicly visible type or member" +#pragma warning disable 1591 + + +#region ReSharper warnings +// ReSharper disable PartialTypeWithSinglePart +// ReSharper disable RedundantNameQualifier +// ReSharper disable InconsistentNaming +// ReSharper disable CheckNamespace +// ReSharper disable UnusedParameter.Local +// ReSharper disable RedundantUsingDirective +#endregion + +namespace Test +{ + using System.Collections.Generic; + + [System.CodeDom.Compiler.GeneratedCode("gbc", "0.12.1.0")] + public enum TestEnum + { + EnumVal1, + EnumVal2, + EnumVal3, + } + + [global::Bond.Schema] + [System.CodeDom.Compiler.GeneratedCode("gbc", "0.12.1.0")] + public partial class Simple + { + [global::Bond.Id(0)] + public int someInt { get; init; } + + [global::Bond.Id(1)] + public int anotherInt { get; init; } + + [global::Bond.Id(2), global::Bond.Type(typeof(global::Bond.Tag.wstring))] + public string someString { get; init; } + + public Simple( + int someInt, + int anotherInt, + string someString) + { + this.someInt = someInt; + this.anotherInt = anotherInt; + this.someString = someString; + } + + public Simple() + { + someString = ""; + } + } + + [global::Bond.Schema] + [System.CodeDom.Compiler.GeneratedCode("gbc", "0.12.1.0")] + public partial class Foo + { + [global::Bond.Id(0), global::Bond.Type(typeof(global::Bond.Tag.wstring))] + public string someText { get; init; } + + public Foo( + string someText) + { + this.someText = someText; + } + + public Foo() + { + someText = "BaseText1"; + } + } + + [global::Bond.Schema] + [System.CodeDom.Compiler.GeneratedCode("gbc", "0.12.1.0")] + public partial class Bar + : Foo + { + [global::Bond.Id(0)] + public TestEnum testEnum { get; init; } + + [global::Bond.Id(1), global::Bond.Type(typeof(global::Bond.Tag.wstring))] + new public string someText { get; init; } + + [global::Bond.Id(2)] + public int someInt { get; init; } + + [global::Bond.Id(3), global::Bond.Type(typeof(global::Bond.Tag.wstring))] + public string moreText { get; init; } + + [global::Bond.Id(4)] + public List someList { get; init; } + + [global::Bond.Id(5), global::Bond.Type(typeof(Dictionary))] + public Dictionary someMap { get; init; } + + [global::Bond.Id(6), global::Bond.Type(typeof(HashSet))] + public HashSet someSet { get; init; } + + public Bar( + // Base class parameters + string someText, + + // This class parameters + TestEnum testEnum, + string someText0, + int someInt, + string moreText, + List someList, + Dictionary someMap, + HashSet someSet + ) : base( + someText) + { + this.testEnum = testEnum; + this.someText = someText0; + this.someInt = someInt; + this.moreText = moreText; + this.someList = someList; + this.someMap = someMap; + this.someSet = someSet; + } + + public Bar() + { + testEnum = TestEnum.Val2; + someText = "DerivedText1"; + moreText = ""; + someList = new List(); + someMap = new Dictionary(); + someSet = new HashSet(); + } + } + + [global::Bond.Schema] + [System.CodeDom.Compiler.GeneratedCode("gbc", "0.12.1.0")] + public partial class Baz + : Bar + { + [global::Bond.Id(0), global::Bond.Type(typeof(global::Bond.Tag.wstring))] + new public string someText { get; init; } + + [global::Bond.Id(1), global::Bond.Type(typeof(global::Bond.Tag.wstring))] + public string evenMoreText { get; init; } + + [global::Bond.Id(2), global::Bond.Type(typeof(global::Bond.Tag.wstring))] + public string someText1 { get; init; } + + public Baz( + // Base class parameters + string someText, + TestEnum testEnum, + string someText0, + int someInt, + string moreText, + List someList, + Dictionary someMap, + HashSet someSet, + + // This class parameters + string someText1, + string evenMoreText, + string someText10 + ) : base( + someText, + testEnum, + someText0, + someInt, + moreText, + someList, + someMap, + someSet) + { + this.someText = someText1; + this.evenMoreText = evenMoreText; + this.someText1 = someText10; + } + + public Baz() + { + someText = ""; + evenMoreText = ""; + someText1 = ""; + } + } + + [global::Bond.Schema] + [System.CodeDom.Compiler.GeneratedCode("gbc", "0.12.1.0")] + public partial class DerivedEmpty + : Foo + { + + + public DerivedEmpty( + // Base class parameters + string someText + ) : base( + someText) + { + + } + + public DerivedEmpty() + { + + } + } +} // Test diff --git a/compiler/tests/generated/constructor-parameters_init/empty_struct_types.cs b/compiler/tests/generated/constructor-parameters_init/empty_struct_types.cs new file mode 100644 index 0000000000..eddf7b0689 --- /dev/null +++ b/compiler/tests/generated/constructor-parameters_init/empty_struct_types.cs @@ -0,0 +1,30 @@ + + +// suppress "Missing XML comment for publicly visible type or member" +#pragma warning disable 1591 + + +#region ReSharper warnings +// ReSharper disable PartialTypeWithSinglePart +// ReSharper disable RedundantNameQualifier +// ReSharper disable InconsistentNaming +// ReSharper disable CheckNamespace +// ReSharper disable UnusedParameter.Local +// ReSharper disable RedundantUsingDirective +#endregion + +namespace tests +{ + using System.Collections.Generic; + + [global::Bond.Schema] + [System.CodeDom.Compiler.GeneratedCode("gbc", "0.12.1.0")] + public partial class Empty + { + + public Empty() + { + + } + } +} // tests diff --git a/cs/nuget/bond.csharp.test.csproj b/cs/nuget/bond.csharp.test.csproj index 9dc0522006..1c5bd5d90b 100644 --- a/cs/nuget/bond.csharp.test.csproj +++ b/cs/nuget/bond.csharp.test.csproj @@ -40,4 +40,9 @@ $(BondOptions) --using="DateTime=System.DateTime" + + + $(BondOptions) --init-only-properties + + diff --git a/cs/test/core/SerializationTests.cs b/cs/test/core/SerializationTests.cs index 7cae57a8b8..dc4c36a564 100644 --- a/cs/test/core/SerializationTests.cs +++ b/cs/test/core/SerializationTests.cs @@ -83,6 +83,20 @@ public void ReadonlySimpleContainers() TestSerialization(); } +#if NET5_0_OR_GREATER + [Test] + public void InitOnlyBasicTypes() + { + TestSerialization(); + } + + [Test] + public void InitOnlySimpleContainers() + { + TestSerialization(); + } +#endif + [Test] public void Nested() { diff --git a/cs/test/coreNS10/CoreNS10.csproj b/cs/test/coreNS10/CoreNS10.csproj index 1a362d6a13..4622cb7083 100644 --- a/cs/test/coreNS10/CoreNS10.csproj +++ b/cs/test/coreNS10/CoreNS10.csproj @@ -37,6 +37,10 @@ $(BondOptions) --readonly-properties --readonly-properties --collection-interfaces + + $(BondOptions) --init-only-properties + --init-only-properties --collection-interfaces + $(BondOptions) --collection-interfaces --fields @@ -53,6 +57,7 @@ + diff --git a/doc/src/bond_cs.md b/doc/src/bond_cs.md index 3c7a3a01e8..044f5bb7da 100644 --- a/doc/src/bond_cs.md +++ b/doc/src/bond_cs.md @@ -127,11 +127,16 @@ Schema fields are represented by properties with public getter and private setter and initialized to the default values in the default constructor. Classes with read-only properties are fully supported by all Bond APIs. +`--init-only-properties` + +Schema fields are represented by properties with public getter and +[C# 9 init-only setter](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/init) and initialized to the default values in the default constructor. Classes with init-only properties are fully supported by all Bond APIs. + `--preview-constructor-parameters` A constructor is generated with a parameter to initialize each of the schema fields. This option is typically used in conjunction with -`--readonly-properties`. This functionailty is in preview and may change. +`--readonly-properties` or `--init-only-properties`. This functionailty is in preview and may change. `--collection-interfaces`