Skip to content

Commit

Permalink
Add support for nested non-nullable choice members
Browse files Browse the repository at this point in the history
  • Loading branch information
jmatss authored and Michael Ganss committed Jun 16, 2022
1 parent ebe7649 commit c7c13a1
Show file tree
Hide file tree
Showing 3 changed files with 128 additions and 11 deletions.
83 changes: 83 additions & 0 deletions XmlSchemaClassGenerator.Tests/XmlTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 = @"<?xml version=""1.0"" encoding=""UTF-8""?>
<xs:schema xmlns:xs=""http://www.w3.org/2001/XMLSchema"">
<xs:element name=""Root"">
<xs:complexType>
<xs:choice>
<xs:sequence>
<xs:element name=""ElementA"" type=""xs:int""/>
</xs:sequence>
<xs:group ref=""Group""/>
</xs:choice>
</xs:complexType>
</xs:element>
<xs:group name=""Group"">
<xs:sequence>
<xs:element name=""ElementB"" type=""xs:int""/>
</xs:sequence>
</xs:group>
</xs:schema>";

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 = @"<?xml version=""1.0"" encoding=""UTF-8""?>
<xs:schema xmlns:xs=""http://www.w3.org/2001/XMLSchema"">
<xs:element name=""Root"">
<xs:complexType>
<xs:choice>
<xs:element name=""ElementWithChild"">
<xs:complexType>
<xs:sequence>
<xs:element name=""NestedChild"" type=""xs:int""/>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:choice>
</xs:complexType>
</xs:element>
</xs:schema>";

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()
{
Expand Down
30 changes: 26 additions & 4 deletions XmlSchemaClassGenerator/ModelBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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));

Expand Down Expand Up @@ -895,7 +895,7 @@ private IEnumerable<PropertyModel> 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];
Expand Down Expand Up @@ -932,6 +932,27 @@ private IEnumerable<PropertyModel> 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;
Expand All @@ -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)
Expand Down
26 changes: 19 additions & 7 deletions XmlSchemaClassGenerator/TypeModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -527,11 +527,11 @@ public PropertyModel(GeneratorConfiguration configuration, string name, TypeMode
OwningType = owningType;
}

public void SetFromNode(string originalName, Func<bool> 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
{
Expand All @@ -540,13 +540,13 @@ public void SetFromNode(string originalName, Func<bool> 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
}

Expand Down Expand Up @@ -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);
Expand Down

0 comments on commit c7c13a1

Please sign in to comment.