From bbef9ea41b4e935fd2b1951b26a730423767e2bb Mon Sep 17 00:00:00 2001 From: Michael Ganss Date: Mon, 19 Feb 2024 16:23:31 +0100 Subject: [PATCH] Add configuration option to serialize empty collections (fixes #490) --- XmlSchemaClassGenerator.Console/Program.cs | 5 +- XmlSchemaClassGenerator.Tests/XmlTests.cs | 32 +++++++ XmlSchemaClassGenerator/Generator.cs | 6 ++ .../GeneratorConfiguration.cs | 5 ++ XmlSchemaClassGenerator/TypeModel.cs | 83 ++++++++++--------- 5 files changed, 93 insertions(+), 38 deletions(-) diff --git a/XmlSchemaClassGenerator.Console/Program.cs b/XmlSchemaClassGenerator.Console/Program.cs index 05f4241..5edc1f4 100644 --- a/XmlSchemaClassGenerator.Console/Program.cs +++ b/XmlSchemaClassGenerator.Console/Program.cs @@ -65,6 +65,7 @@ static int Main(string[] args) var nameSubstituteFiles = new List(); var unionCommonType = false; var separateNamespaceHierarchy = false; + var serializeEmptyCollections = false; var options = new OptionSet { { "h|help", "show this message and exit", v => showHelp = v != null }, @@ -158,6 +159,7 @@ A file name may be given by appending a pipe sign (|) followed by a file name (l { "es|enumAsString", "Use string instead of enum for enumeration", v => enumAsString = v != null }, { "ca|commandArgs", "generate a comment with the exact command line arguments that were used to generate the source code (default is true)", v => generateCommandLineArgs = v != null }, { "uc|unionCommonType", "generate a common type for unions if possible (default is false)", v => unionCommonType = v != null }, + { "ec|serializeEmptyCollections", "serialize empty collections (default is false)", v => serializeEmptyCollections = v != null }, }; var globsAndUris = options.Parse(args); @@ -244,7 +246,8 @@ A file name may be given by appending a pipe sign (|) followed by a file name (l UseArrayItemAttribute = useArrayItemAttribute, EnumAsString = enumAsString, MapUnionToWidestCommonType = unionCommonType, - SeparateNamespaceHierarchy = separateNamespaceHierarchy + SeparateNamespaceHierarchy = separateNamespaceHierarchy, + SerializeEmptyCollections = serializeEmptyCollections, }; if (nameSubstituteMap.Any()) diff --git a/XmlSchemaClassGenerator.Tests/XmlTests.cs b/XmlSchemaClassGenerator.Tests/XmlTests.cs index 6597e42..236f18f 100644 --- a/XmlSchemaClassGenerator.Tests/XmlTests.cs +++ b/XmlSchemaClassGenerator.Tests/XmlTests.cs @@ -614,6 +614,38 @@ public void TestArray() SharedTestFunctions.TestSamples(Output, "Tableau.Array", TableauPattern); } + [Fact, TestPriority(1)] + [UseCulture("en-US")] + public void TestSerializeEmptyCollection() + { + var output = new FileWatcherOutputWriter(Path.Combine("output", "Tableau.EmptyCollection")); + Compiler.Generate("Tableau.EmptyCollection", TableauPattern, + new Generator + { + OutputWriter = output, + EnableDataBinding = true, + CollectionSettersMode = CollectionSettersMode.Private, + SerializeEmptyCollections = true + }); + SharedTestFunctions.TestSamples(Output, "Tableau.EmptyCollection", TableauPattern); + } + + [Fact, TestPriority(1)] + [UseCulture("en-US")] + public void TestSerializeEmptyPublicCollection() + { + var output = new FileWatcherOutputWriter(Path.Combine("output", "Tableau.EmptyPublicCollection")); + Compiler.Generate("Tableau.EmptyPublicCollection", TableauPattern, + new Generator + { + OutputWriter = output, + EnableDataBinding = true, + CollectionSettersMode = CollectionSettersMode.Public, + SerializeEmptyCollections = true + }); + SharedTestFunctions.TestSamples(Output, "Tableau.EmptyPublicCollection", TableauPattern); + } + [Fact, TestPriority(1)] [UseCulture("en-US")] public void TestDtsx() diff --git a/XmlSchemaClassGenerator/Generator.cs b/XmlSchemaClassGenerator/Generator.cs index 6f3ce81..c543b1c 100644 --- a/XmlSchemaClassGenerator/Generator.cs +++ b/XmlSchemaClassGenerator/Generator.cs @@ -336,6 +336,12 @@ public bool SeparateNamespaceHierarchy set { _configuration.SeparateNamespaceHierarchy = value; } } + public bool SerializeEmptyCollections + { + get { return _configuration.SerializeEmptyCollections; } + set { _configuration.SerializeEmptyCollections = value; } + } + public bool ValidationError { get; private set; } static Generator() diff --git a/XmlSchemaClassGenerator/GeneratorConfiguration.cs b/XmlSchemaClassGenerator/GeneratorConfiguration.cs index 781a264..6cd6692 100644 --- a/XmlSchemaClassGenerator/GeneratorConfiguration.cs +++ b/XmlSchemaClassGenerator/GeneratorConfiguration.cs @@ -343,5 +343,10 @@ public void WriteLog(string message) /// Separates namespace hierarchy by folder. Default is false. /// public bool SeparateNamespaceHierarchy { get; set; } = false; + + /// + /// Determines whether empty collections should be serialized as empty elements. Default is false. + /// + public bool SerializeEmptyCollections { get; set; } = false; } } diff --git a/XmlSchemaClassGenerator/TypeModel.cs b/XmlSchemaClassGenerator/TypeModel.cs index f5923a7..824a9a5 100644 --- a/XmlSchemaClassGenerator/TypeModel.cs +++ b/XmlSchemaClassGenerator/TypeModel.cs @@ -869,56 +869,65 @@ [new CodeMethodReturnStatement(valueExpression)], } else if (isEnumerable && !IsRequired) { - var listReference = new CodePropertyReferenceExpression(new CodeThisReferenceExpression(), Name); - var collectionType = Configuration.CollectionImplementationType ?? Configuration.CollectionType; - var countProperty = collectionType == typeof(Array) ? nameof(Array.Length) : nameof(List.Count); - var countReference = new CodePropertyReferenceExpression(listReference, countProperty); - var notZeroExpression = new CodeBinaryOperatorExpression(countReference, CodeBinaryOperatorType.IdentityInequality, new CodePrimitiveExpression(0)); - if (Configuration.CollectionSettersMode is CollectionSettersMode.PublicWithoutConstructorInitialization or CollectionSettersMode.Public or CollectionSettersMode.Init or CollectionSettersMode.InitWithoutConstructorInitialization) - { - var notNullExpression = new CodeBinaryOperatorExpression(listReference, CodeBinaryOperatorType.IdentityInequality, new CodePrimitiveExpression(null)); - notZeroExpression = new CodeBinaryOperatorExpression(notNullExpression, CodeBinaryOperatorType.BooleanAnd, notZeroExpression); - } - var returnStatement = new CodeMethodReturnStatement(notZeroExpression); + var canBeNull = Configuration.CollectionSettersMode is CollectionSettersMode.PublicWithoutConstructorInitialization or CollectionSettersMode.Public or CollectionSettersMode.Init or CollectionSettersMode.InitWithoutConstructorInitialization; - if (Configuration.UseShouldSerializePattern) + if (canBeNull || !Configuration.SerializeEmptyCollections) { - var shouldSerializeMethod = new CodeMemberMethod + var listReference = new CodePropertyReferenceExpression(new CodeThisReferenceExpression(), Name); + var collectionType = Configuration.CollectionImplementationType ?? Configuration.CollectionType; + var countProperty = collectionType == typeof(Array) ? nameof(Array.Length) : nameof(List.Count); + var countReference = new CodePropertyReferenceExpression(listReference, countProperty); + var notZeroExpression = new CodeBinaryOperatorExpression(countReference, CodeBinaryOperatorType.IdentityInequality, new CodePrimitiveExpression(0)); + var returnExpression = notZeroExpression; + + if (canBeNull) { - Attributes = MemberAttributes.Public, - Name = "ShouldSerialize" + Name, - ReturnType = new CodeTypeReference(typeof(bool)), - Statements = { returnStatement } - }; + var notNullExpression = new CodeBinaryOperatorExpression(listReference, CodeBinaryOperatorType.IdentityInequality, new CodePrimitiveExpression(null)); + var notNullOrEmptyExpression = new CodeBinaryOperatorExpression(notNullExpression, CodeBinaryOperatorType.BooleanAnd, notZeroExpression); + returnExpression = Configuration.SerializeEmptyCollections ? notNullExpression : notNullOrEmptyExpression; + } - Configuration.MemberVisitor(shouldSerializeMethod, this); + var returnStatement = new CodeMethodReturnStatement(returnExpression); - typeDeclaration.Members.Add(shouldSerializeMethod); - } - else - { - var specifiedProperty = new CodeMemberProperty + if (Configuration.UseShouldSerializePattern) { - Type = TypeRef(), - Name = Name + Specified, - HasSet = false, - HasGet = true, - }; - specifiedProperty.CustomAttributes.Add(ignoreAttribute); - if (Configuration.EntityFramework) { specifiedProperty.CustomAttributes.Add(notMappedAttribute); } - specifiedProperty.Attributes = MemberAttributes.Public | MemberAttributes.Final; + var shouldSerializeMethod = new CodeMemberMethod + { + Attributes = MemberAttributes.Public, + Name = "ShouldSerialize" + Name, + ReturnType = new CodeTypeReference(typeof(bool)), + Statements = { returnStatement } + }; - specifiedProperty.GetStatements.Add(returnStatement); + Configuration.MemberVisitor(shouldSerializeMethod, this); - var specifiedDocs = new DocumentationModel[] { + typeDeclaration.Members.Add(shouldSerializeMethod); + } + else + { + var specifiedProperty = new CodeMemberProperty + { + Type = TypeRef(), + Name = Name + Specified, + HasSet = false, + HasGet = true, + }; + specifiedProperty.CustomAttributes.Add(ignoreAttribute); + if (Configuration.EntityFramework) { specifiedProperty.CustomAttributes.Add(notMappedAttribute); } + specifiedProperty.Attributes = MemberAttributes.Public | MemberAttributes.Final; + + specifiedProperty.GetStatements.Add(returnStatement); + + var specifiedDocs = new DocumentationModel[] { new() { Language = English, Text = $"Gets a value indicating whether the {Name} collection is empty." }, new() { Language = German, Text = $"Ruft einen Wert ab, der angibt, ob die {Name}-Collection leer ist." } }; - specifiedProperty.Comments.AddRange(GetComments(specifiedDocs).ToArray()); + specifiedProperty.Comments.AddRange(GetComments(specifiedDocs).ToArray()); - Configuration.MemberVisitor(specifiedProperty, this); + Configuration.MemberVisitor(specifiedProperty, this); - typeDeclaration.Members.Add(specifiedProperty); + typeDeclaration.Members.Add(specifiedProperty); + } } }