diff --git a/XmlSchemaClassGenerator.Console/Program.cs b/XmlSchemaClassGenerator.Console/Program.cs index 15374ebf..1cdb594d 100644 --- a/XmlSchemaClassGenerator.Console/Program.cs +++ b/XmlSchemaClassGenerator.Console/Program.cs @@ -54,6 +54,7 @@ static void Main(string[] args) var uniqueTypeNamesAcrossNamespaces = false; var createGeneratedCodeAttributeVersion = true; var netCoreSpecificCode = false; + var nullableReferenceAttributes = false; var generateCommandLineArgs = true; var options = new OptionSet { @@ -126,7 +127,8 @@ A file name may be given by appending a pipe sign (|) followed by a file name (l v => commentLanguages = v.Split(',').Select(l => l.Trim()).ToArray() }, { "un|uniqueTypeNames", "generate type names that are unique across namespaces (default is false)", v => uniqueTypeNamesAcrossNamespaces = v != null }, { "gc|generatedCodeAttribute", "add version information to GeneratedCodeAttribute (default is true)", v => createGeneratedCodeAttributeVersion = v != null }, - { "nc|netCore", "generate .NET Core specific code that might not work with .NET Framework (default is false)", v => netCoreSpecificCode = v != null }, + { "nc|netCore", "generate .NET Core specific code that might not work with .NET Framework (default is false)", v => netCoreSpecificCode = v != null }, + { "nr|nullableReferenceAttributes", "generate attributes for nullable reference types (default is false)", v => nullableReferenceAttributes = 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 }, }; @@ -203,6 +205,7 @@ A file name may be given by appending a pipe sign (|) followed by a file name (l UniqueTypeNamesAcrossNamespaces = uniqueTypeNamesAcrossNamespaces, CreateGeneratedCodeAttributeVersion = createGeneratedCodeAttributeVersion, NetCoreSpecificCode = netCoreSpecificCode, + EnableNullableReferenceAttributes = nullableReferenceAttributes, GenerateCommandLineArgumentsComment = generateCommandLineArgs, }; diff --git a/XmlSchemaClassGenerator.Tests/Compiler.cs b/XmlSchemaClassGenerator.Tests/Compiler.cs index f7925a03..95453d4a 100644 --- a/XmlSchemaClassGenerator.Tests/Compiler.cs +++ b/XmlSchemaClassGenerator.Tests/Compiler.cs @@ -113,6 +113,7 @@ public static Assembly GenerateFiles(string name, IEnumerable files, Gen UniqueTypeNamesAcrossNamespaces = generatorPrototype.UniqueTypeNamesAcrossNamespaces, CreateGeneratedCodeAttributeVersion = generatorPrototype.CreateGeneratedCodeAttributeVersion, NetCoreSpecificCode = generatorPrototype.NetCoreSpecificCode, + EnableNullableReferenceAttributes = generatorPrototype.EnableNullableReferenceAttributes, NamingScheme = generatorPrototype.NamingScheme }; diff --git a/XmlSchemaClassGenerator.Tests/XmlTests.cs b/XmlSchemaClassGenerator.Tests/XmlTests.cs index 47e6eac4..f32c41d8 100644 --- a/XmlSchemaClassGenerator.Tests/XmlTests.cs +++ b/XmlSchemaClassGenerator.Tests/XmlTests.cs @@ -2,6 +2,7 @@ using System.CodeDom; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Reflection; @@ -14,6 +15,7 @@ using System.Xml.XPath; using Ganss.IO; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.Xml.XMLGen; using Xunit; using Xunit.Abstractions; @@ -105,6 +107,7 @@ private static IEnumerable ConvertXml(string name, string xsd, Generator const string DtsxPattern = "xsd/dtsx/dtsx2.xsd"; const string WfsPattern = "xsd/wfs/schemas.opengis.net/wfs/2.0/wfs.xsd"; const string EppPattern = "xsd/epp/*.xsd"; + const string NullableReferenceAttributesPattern = "xsd/nullablereferenceattributes/nullablereference.xsd"; // IATA test takes too long to perform every time @@ -2366,6 +2369,42 @@ void UnknownAttributeHandler(object sender, XmlAttributeEventArgs e) */ } + [Fact, TestPriority(1)] + [UseCulture("en-US")] + public void TestNullableReferenceAttributes() + { + var files = Glob.ExpandNames(NullableReferenceAttributesPattern).OrderByDescending(f => f); + var generator = new Generator + { + EnableNullableReferenceAttributes = true, + UseShouldSerializePattern = true, + NamespaceProvider = new NamespaceProvider + { + GenerateNamespace = key => "Test" + } + }; + var assembly = Compiler.Generate(nameof(TestNullableReferenceAttributes), NullableReferenceAttributesPattern, generator); + void assertNullable(string typename, bool nullable) + { + Type c = assembly.GetType(typename); + var property = c.GetProperty("Text"); + var setParameter = property.SetMethod.GetParameters(); + var getReturnParameter = property.GetMethod.ReturnParameter; + var allowNullableAttribute = setParameter.Single().CustomAttributes.SingleOrDefault(a => a.AttributeType == typeof(AllowNullAttribute)); + var maybeNullAttribute = getReturnParameter.CustomAttributes.SingleOrDefault(a => a.AttributeType == typeof(MaybeNullAttribute)); + var hasAllowNullAttribute = allowNullableAttribute != null; + var hasMaybeNullAttribute = maybeNullAttribute != null; + Assert.Equal(nullable, hasAllowNullAttribute); + Assert.Equal(nullable, hasMaybeNullAttribute); + } + assertNullable("Test.ElementReferenceNullable", true); + assertNullable("Test.ElementReferenceList", true); + assertNullable("Test.ElementReferenceNonNullable", false); + assertNullable("Test.AttributeReferenceNullable", true); + assertNullable("Test.AttributeReferenceNonNullable", false); + assertNullable("Test.AttributeValueNullableInt", false); + } + [Fact, TestPriority(1)] public void TestNetex() { diff --git a/XmlSchemaClassGenerator.Tests/xsd/nullablereferenceattributes/nullablereference.xsd b/XmlSchemaClassGenerator.Tests/xsd/nullablereferenceattributes/nullablereference.xsd new file mode 100644 index 00000000..05eb6261 --- /dev/null +++ b/XmlSchemaClassGenerator.Tests/xsd/nullablereferenceattributes/nullablereference.xsd @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/XmlSchemaClassGenerator/CodeUtilities.cs b/XmlSchemaClassGenerator/CodeUtilities.cs index 8abc4673..e76e4d78 100644 --- a/XmlSchemaClassGenerator/CodeUtilities.cs +++ b/XmlSchemaClassGenerator/CodeUtilities.cs @@ -351,7 +351,7 @@ public static KeyValuePair ParseNamespace(string nsArg, st var parts = nsArg.Split(new[] { '=' }, 2); if (parts.Length != 2) { - throw new ArgumentException("XML and C# namespaces should be separated by '='."); + throw new ArgumentException("XML and C# namespaces should be separated by '='. You entered: " + nsArg); } var xmlNs = parts[0]; diff --git a/XmlSchemaClassGenerator/Generator.cs b/XmlSchemaClassGenerator/Generator.cs index 0e790717..e0e8c215 100644 --- a/XmlSchemaClassGenerator/Generator.cs +++ b/XmlSchemaClassGenerator/Generator.cs @@ -115,6 +115,12 @@ public bool GenerateNullables set { _configuration.GenerateNullables = value; } } + public bool EnableNullableReferenceAttributes + { + get { return _configuration.EnableNullableReferenceAttributes; } + set { _configuration.EnableNullableReferenceAttributes = value; } + } + public bool UseShouldSerializePattern { get { return _configuration.UseShouldSerializePattern; } diff --git a/XmlSchemaClassGenerator/GeneratorConfiguration.cs b/XmlSchemaClassGenerator/GeneratorConfiguration.cs index fa71cfd0..0d4433ed 100644 --- a/XmlSchemaClassGenerator/GeneratorConfiguration.cs +++ b/XmlSchemaClassGenerator/GeneratorConfiguration.cs @@ -76,6 +76,10 @@ public GeneratorConfiguration() /// Use XElement instead of XmlElement for Any nodes? /// public bool UseXElementForAny { get; set; } + /// + /// Generate attributes for nullable references to avoid compiler-warnings in .NET Core and Standard with nullable-checks. + /// + public bool EnableNullableReferenceAttributes { get; set; } private NamingScheme namingScheme; diff --git a/XmlSchemaClassGenerator/TypeModel.cs b/XmlSchemaClassGenerator/TypeModel.cs index e0713f47..80b31f77 100644 --- a/XmlSchemaClassGenerator/TypeModel.cs +++ b/XmlSchemaClassGenerator/TypeModel.cs @@ -750,6 +750,15 @@ private bool IsNullableValueType && IsNullable && !(IsCollection || IsArray) && !IsList && ((PropertyType is EnumModel) || (PropertyType is SimpleModel model && model.ValueType.IsValueType)); } + } + + private bool IsNullableReferenceType + { + get + { + return DefaultValue == null + && IsNullable && (IsCollection || IsArray || IsList || PropertyType is ClassModel || PropertyType is SimpleModel model && !model.ValueType.IsValueType); + } } private bool IsNillableValueType @@ -860,6 +869,7 @@ public void AddMembersTo(CodeTypeDeclaration typeDeclaration, bool withDataBindi var isArray = IsArray; var propertyType = PropertyType; var isNullableValueType = IsNullableValueType; + var isNullableReferenceType = IsNullableReferenceType; var typeReference = TypeReference; var requiresBackingField = withDataBinding || DefaultValue != null || IsCollection || isArray; @@ -1114,6 +1124,12 @@ public void AddMembersTo(CodeTypeDeclaration typeDeclaration, bool withDataBindi typeDeclaration.Members.Add(specifiedProperty); } + if (isNullableReferenceType && Configuration.EnableNullableReferenceAttributes) + { + member.CustomAttributes.Add(new CodeAttributeDeclaration("System.Diagnostics.CodeAnalysis.AllowNullAttribute")); + member.CustomAttributes.Add(new CodeAttributeDeclaration("System.Diagnostics.CodeAnalysis.MaybeNullAttribute")); + } + var attributes = GetAttributes(isArray).ToArray(); member.CustomAttributes.AddRange(attributes);