diff --git a/XmlSchemaClassGenerator.Tests/XmlTests.cs b/XmlSchemaClassGenerator.Tests/XmlTests.cs index 33549254..21dd017f 100644 --- a/XmlSchemaClassGenerator.Tests/XmlTests.cs +++ b/XmlSchemaClassGenerator.Tests/XmlTests.cs @@ -1100,6 +1100,89 @@ public void ChoiceMembersAreNullable() Assert.Contains("Opt4Specified", content); } + [Fact] + public void NestedElementInChoiceIsNullable() + { + // Because nullability isn't directly exposed in the generated C#, we use "XXXSpecified" on a value type + // as a proxy. + const string xsd = @" + + + + + + + + + + + + + + + + + +"; + + var generator = new Generator + { + NamespaceProvider = new NamespaceProvider + { + GenerateNamespace = key => "Test" + } + }; + var contents = ConvertXml(nameof(NestedElementInChoiceIsNullable), xsd, generator); + var content = Assert.Single(contents); + + Assert.Contains("ElementASpecified", content); + Assert.Contains("ElementBSpecified", content); + } + + [Fact] + public void OnlyFirstElementOfNestedElementsIsForcedToNullableInChoice() + { + // Because nullability isn't directly exposed in the generated C#, we use the "RequiredAttribute" + // as a proxy. + const string xsd = @" + + + + + + + + + + + + + + +"; + + var generator = new Generator + { + NamespaceProvider = new NamespaceProvider + { + GenerateNamespace = key => "Test" + } + }; + var contents = ConvertXml(nameof(OnlyFirstElementOfNestedElementsIsForcedToNullableInChoice), xsd, generator).ToArray(); + var assembly = Compiler.Compile(nameof(OnlyFirstElementOfNestedElementsIsForcedToNullableInChoice), contents); + + var elementWithChildProperty = assembly.GetType("Test.Root")?.GetProperty("ElementWithChild"); + var nestedChildProperty = assembly.GetType("Test.RootElementWithChild")?.GetProperty("NestedChild"); + Assert.NotNull(elementWithChildProperty); + Assert.NotNull(nestedChildProperty); + + Type requiredType = typeof(System.ComponentModel.DataAnnotations.RequiredAttribute); + bool elementWithChildIsRequired = Attribute.GetCustomAttribute(elementWithChildProperty, requiredType) != null; + bool nestedChildIsRequired = Attribute.GetCustomAttribute(nestedChildProperty, requiredType) != null; + Assert.False(elementWithChildIsRequired); + Assert.True(nestedChildIsRequired); + } + [Fact] public void AssemblyVisibleIsInternalClass() { diff --git a/XmlSchemaClassGenerator/ModelBuilder.cs b/XmlSchemaClassGenerator/ModelBuilder.cs index cd87db16..d4d7c2aa 100644 --- a/XmlSchemaClassGenerator/ModelBuilder.cs +++ b/XmlSchemaClassGenerator/ModelBuilder.cs @@ -866,7 +866,7 @@ private PropertyModel PropertyFromAttribute(TypeModel owningTypeModel, XmlSchema IsRequired = attribute.Use == XmlSchemaUse.Required }; - property.SetFromNode(originalName, () => attribute.Use != XmlSchemaUse.Optional, attribute); + property.SetFromNode(originalName, attribute.Use != XmlSchemaUse.Optional, attribute); property.SetSchemaNameAndNamespace(owningTypeModel, attribute); property.Documentation.AddRange(GetDocumentation(attribute)); @@ -895,7 +895,7 @@ private IEnumerable CreatePropertiesForElements(Uri source, TypeM UseDataTypeAttribute = false }; property = new PropertyModel(_configuration, "Any", typeModel, owningTypeModel) { IsAny = true }; - property.SetFromParticles(particle, item); + property.SetFromParticles(particle, item, item.MinOccurs >= 1.0m && !IsNullableByChoice(item.XmlParent)); break; case XmlSchemaGroupRef groupRef: var group = Groups[groupRef.RefName]; @@ -932,6 +932,27 @@ private IEnumerable CreatePropertiesForElements(Uri source, TypeM return properties; } + private static bool IsNullableByChoice(XmlSchemaObject parent) + { + while (parent != null) + { + switch (parent) + { + case XmlSchemaChoice: + return true; + // Any ancestor element between the current item and the + // choice would already have been forced to nullable. + case XmlSchemaElement: + case XmlSchemaParticle p when p.MinOccurs < 1.0m: + return false; + default: + break; + } + parent = parent.Parent; + } + return false; + } + private PropertyModel PropertyFromElement(TypeModel owningTypeModel, XmlSchemaElementEx element, Particle particle, Particle item, Substitute substitute) { PropertyModel property; @@ -946,8 +967,9 @@ private PropertyModel PropertyFromElement(TypeModel owningTypeModel, XmlSchemaEl var typeModel = substitute?.Type ?? CreateTypeModel(GetQualifiedName(owningTypeModel, particle.XmlParticle, element), element.ElementSchemaType); property = new PropertyModel(_configuration, name, typeModel, owningTypeModel) { IsNillable = element.IsNillable }; - property.SetFromParticles(particle, item); - property.SetFromNode(originalName, () => item.MinOccurs >= 1.0m && item.XmlParent is not XmlSchemaChoice, element); + var isRequired = item.MinOccurs >= 1.0m && !IsNullableByChoice(item.XmlParent); + property.SetFromParticles(particle, item, isRequired); + property.SetFromNode(originalName, isRequired, element); property.SetSchemaNameAndNamespace(owningTypeModel, effectiveElement); if (property.IsArray && !_configuration.GenerateComplexTypesForCollections) diff --git a/XmlSchemaClassGenerator/TypeModel.cs b/XmlSchemaClassGenerator/TypeModel.cs index 13290093..583e1fd6 100644 --- a/XmlSchemaClassGenerator/TypeModel.cs +++ b/XmlSchemaClassGenerator/TypeModel.cs @@ -527,11 +527,11 @@ public PropertyModel(GeneratorConfiguration configuration, string name, TypeMode OwningType = owningType; } - public void SetFromNode(string originalName, Func useFixedIfNoDefault, IXmlSchemaNode xs) + public void SetFromNode(string originalName, bool useFixedIfNoDefault, IXmlSchemaNode xs) { OriginalPropertyName = originalName; - DefaultValue = xs.DefaultValue ?? (useFixedIfNoDefault() ? xs.FixedValue : null); + DefaultValue = xs.DefaultValue ?? (useFixedIfNoDefault ? xs.FixedValue : null); FixedValue = xs.FixedValue; Form = xs.Form switch { @@ -540,13 +540,13 @@ public void SetFromNode(string originalName, Func useFixedIfNoDefault, IXm }; } - public void SetFromParticles(Particle particle, Particle item) + public void SetFromParticles(Particle particle, Particle item, bool isRequired) { Particle = item; XmlParticle = item.XmlParticle; XmlParent = item.XmlParent; - IsRequired = item.MinOccurs >= 1.0m && (item.XmlParent is not XmlSchemaChoice); + IsRequired = isRequired; IsCollection = item.MaxOccurs > 1.0m || particle.MaxOccurs > 1.0m; // http://msdn.microsoft.com/en-us/library/vstudio/d3hx2s7e(v=vs.100).aspx } @@ -1230,9 +1230,21 @@ public override CodeTypeReference GetReferenceFor(NamespaceModel referencingName { var collectionType = forInit ? (Configuration.CollectionImplementationType ?? Configuration.CollectionType) : Configuration.CollectionType; - type = collectionType.IsGenericType ? collectionType.MakeGenericType(type) - : collectionType == typeof(Array) ? type.MakeArrayType() - : collectionType; + if (collectionType.IsGenericType) + { + type = collectionType.MakeGenericType(type); + } + else + { + if (collectionType == typeof(Array)) + { + type = type.MakeArrayType(); + } + else + { + type = collectionType; + } + } } return CodeUtilities.CreateTypeReference(type, Configuration);