diff --git a/XmlSampleGenerator/Generator.cs b/XmlSampleGenerator/Generator.cs index dff86deb..cc4fde34 100644 --- a/XmlSampleGenerator/Generator.cs +++ b/XmlSampleGenerator/Generator.cs @@ -1319,7 +1319,7 @@ public override string GenerateValue() { return Convert.ToBase64String(enumValue as byte[]); } else { - return "base64Binary Content"; + return "cnRjbGNyZW9scg=="; } } } diff --git a/XmlSchemaClassGenerator.Tests/XmlSchemaClassGenerator.Tests.csproj b/XmlSchemaClassGenerator.Tests/XmlSchemaClassGenerator.Tests.csproj index 28c87204..4226be6e 100644 --- a/XmlSchemaClassGenerator.Tests/XmlSchemaClassGenerator.Tests.csproj +++ b/XmlSchemaClassGenerator.Tests/XmlSchemaClassGenerator.Tests.csproj @@ -18,18 +18,27 @@ - + + all + runtime; build; native; contentfiles; analyzers + - + - - - - - + + all + runtime; build; native; contentfiles; analyzers + + + all + runtime; build; native; contentfiles; analyzers + + + + true @@ -425,6 +434,12 @@ PreserveNewest + + PreserveNewest + + + PreserveNewest + PreserveNewest diff --git a/XmlSchemaClassGenerator.Tests/XmlTests.cs b/XmlSchemaClassGenerator.Tests/XmlTests.cs index 3278447e..2538efda 100644 --- a/XmlSchemaClassGenerator.Tests/XmlTests.cs +++ b/XmlSchemaClassGenerator.Tests/XmlTests.cs @@ -12,12 +12,20 @@ using System.Xml.Schema; using System.Xml.Serialization; using Xunit; +using Xunit.Abstractions; namespace XmlSchemaClassGenerator.Tests { [TestCaseOrderer("XmlSchemaClassGenerator.Tests.PriorityOrderer", "XmlSchemaClassGenerator.Tests")] public class XmlTests { + private readonly ITestOutputHelper Output; + + public XmlTests(ITestOutputHelper output) + { + Output = output; + } + private IEnumerable ConvertXml(string name, string xsd, Generator generatorPrototype = null) { var writer = new MemoryOutputWriter(); @@ -56,6 +64,8 @@ private IEnumerable ConvertXml(string name, string xsd, Generator genera const string IS24Pattern = @"xsd\is24\*\*.xsd"; const string IS24ImmoTransferPattern = @"xsd\is24immotransfer\is24immotransfer.xsd"; const string WadlPattern = @"xsd\wadl\*.xsd"; + const string ListPattern = @"xsd\list\list.xsd"; + const string SimplePattern = @"xsd\simple\simple.xsd"; const string ClientPattern = @"xsd\client\client.xsd"; const string IataPattern = @"xsd\iata\*.xsd"; const string TimePattern = @"xsd\time\time.xsd"; @@ -87,6 +97,22 @@ public void TestClient() TestSamples("Client", ClientPattern); } + [Fact, TestPriority(1)] + [UseCulture("en-US")] + public void TestList() + { + Compiler.Generate("List", ListPattern); + TestSamples("List", ListPattern); + } + + [Fact, TestPriority(1)] + [UseCulture("en-US")] + public void TestSimple() + { + Compiler.Generate("Simple", SimplePattern); + TestSamples("Simple", SimplePattern); + } + [Fact, TestPriority(1)] [UseCulture("en-US")] public void TestIS24RestApi() @@ -132,6 +158,16 @@ private void TestSamples(string name, string pattern) DeserializeSampleXml(pattern, assembly); } + private bool HandleValidationError(string xml, ValidationEventArgs e) + { + var line = xml.Split('\n')[e.Exception.LineNumber - 1].Substring(e.Exception.LinePosition - 1); + var severity = e.Severity == XmlSeverityType.Error ? "Error" : "Warning"; + Output.WriteLine($"{severity} at line {e.Exception.LineNumber}, column {e.Exception.LinePosition}: {e.Message}"); + Output.WriteLine(line); + return (e.Severity == XmlSeverityType.Error + && !e.Message.Contains("The Pattern constraint failed")); // generator doesn't generate valid values where pattern restrictions exist, e.g. email + } + private void DeserializeSampleXml(string pattern, Assembly assembly) { var files = Glob.ExpandNames(pattern); @@ -150,6 +186,8 @@ private void DeserializeSampleXml(string pattern, Assembly assembly) set.Compile(); + var anyValidXml = false; + foreach (var rootElement in set.GlobalElements.Values.Cast().Where(e => !e.IsAbstract && !(e.ElementSchemaType is XmlSchemaSimpleType))) { var type = FindType(assembly, rootElement.QualifiedName); @@ -162,6 +200,8 @@ private void DeserializeSampleXml(string pattern, Assembly assembly) generator.WriteXml(xw); var xml = sb.ToString(); + File.WriteAllText("xml.xml", xml); + // validate serialized xml var settings = new XmlReaderSettings { @@ -171,19 +211,21 @@ private void DeserializeSampleXml(string pattern, Assembly assembly) var invalid = false; - settings.ValidationEventHandler += (s, e) => - { - if (e.Severity == XmlSeverityType.Error) - invalid = true; - }; + void validate(object s, ValidationEventArgs e) + { + if (HandleValidationError(xml, e)) invalid = true; + } + + settings.ValidationEventHandler += validate; var reader = XmlReader.Create(new StringReader(xml), settings); while (reader.Read()) ; + settings.ValidationEventHandler -= validate; + // generated xml is not schema valid -> skip if (invalid) continue; - - File.WriteAllText("xml.xml", xml); + anyValidXml = true; // deserialize from sample var sr = new StringReader(xml); @@ -194,14 +236,18 @@ private void DeserializeSampleXml(string pattern, Assembly assembly) File.WriteAllText("xml2.xml", xml2); - settings.ValidationEventHandler += (s, e) => + void validate2(object s, ValidationEventArgs e) { - throw e.Exception; + if (HandleValidationError(xml2, e)) throw e.Exception; }; + settings.ValidationEventHandler += validate2; + reader = XmlReader.Create(new StringReader(xml2), settings); while (reader.Read()) ; + settings.ValidationEventHandler -= validate2; + // deserialize again sr = new StringReader(xml2); var o2 = serializer.Deserialize(sr); @@ -209,6 +255,8 @@ private void DeserializeSampleXml(string pattern, Assembly assembly) AssertEx.Equal(o, o2); } } + + Assert.True(anyValidXml, "No valid generated XML for this test"); } private Type FindType(Assembly assembly, XmlQualifiedName xmlQualifiedName) @@ -406,7 +454,7 @@ public partial class Group_Name /// Ruft den Text ab oder legt diesen fest. /// Gets or sets the text value. /// - [System.Xml.Serialization.XmlTextAttribute(DataType=""string"")] + [System.Xml.Serialization.XmlTextAttribute()] public string Value { get; set; } /// diff --git a/XmlSchemaClassGenerator.Tests/xsd/list/list.xsd b/XmlSchemaClassGenerator.Tests/xsd/list/list.xsd new file mode 100644 index 00000000..620414b5 --- /dev/null +++ b/XmlSchemaClassGenerator.Tests/xsd/list/list.xsd @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/XmlSchemaClassGenerator.Tests/xsd/simple/simple.xsd b/XmlSchemaClassGenerator.Tests/xsd/simple/simple.xsd new file mode 100644 index 00000000..03ddbb69 --- /dev/null +++ b/XmlSchemaClassGenerator.Tests/xsd/simple/simple.xsd @@ -0,0 +1,385 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/XmlSchemaClassGenerator/CodeUtilities.cs b/XmlSchemaClassGenerator/CodeUtilities.cs index 6e0237b8..1352e0cd 100644 --- a/XmlSchemaClassGenerator/CodeUtilities.cs +++ b/XmlSchemaClassGenerator/CodeUtilities.cs @@ -32,58 +32,38 @@ public static string ToBackingField(this string propertyName, bool doNotUseUnder return doNotUseUnderscoreInPrivateMemberNames ? propertyName.ToCamelCase() : string.Concat("_", propertyName.ToCamelCase()); } - private static bool? IsDataTypeAttributeAllowed(XmlTypeCode typeCode, GeneratorConfiguration configuration) + public static bool? IsDataTypeAttributeAllowed(this XmlSchemaDatatype type, GeneratorConfiguration configuration) { bool? result; - switch (typeCode) + switch (type.TypeCode) { case XmlTypeCode.AnyAtomicType: // union result = false; break; - case XmlTypeCode.Integer: - case XmlTypeCode.NegativeInteger: - case XmlTypeCode.NonNegativeInteger: - case XmlTypeCode.NonPositiveInteger: - case XmlTypeCode.PositiveInteger: - if (configuration.IntegerDataType != null && configuration.IntegerDataType != typeof(string)) - { - result = false; - } - else - { - result = null; - } - break; + case XmlTypeCode.DateTime: + case XmlTypeCode.Time: + case XmlTypeCode.Date: case XmlTypeCode.Base64Binary: case XmlTypeCode.HexBinary: result = true; break; default: - result = null; + result = false; break; } return result; } - public static bool? IsDataTypeAttributeAllowed(this XmlSchemaDatatype type, GeneratorConfiguration configuration) - { - return IsDataTypeAttributeAllowed(type.TypeCode, configuration); - } - - public static bool? IsDataTypeAttributeAllowed(this XmlSchemaType type, GeneratorConfiguration configuration) + public static Type GetEffectiveType(this XmlSchemaDatatype type, GeneratorConfiguration configuration, bool attribute = false) { - return IsDataTypeAttributeAllowed(type.TypeCode, configuration); - } + Type resultType; - private static Type GetEffectiveType(XmlTypeCode typeCode, XmlSchemaDatatypeVariety variety, GeneratorConfiguration configuration) - { - Type result; - switch (typeCode) + switch (type.TypeCode) { case XmlTypeCode.AnyAtomicType: // union - result = typeof(string); + resultType = typeof(string); break; case XmlTypeCode.AnyUri: case XmlTypeCode.Duration: @@ -92,43 +72,41 @@ private static Type GetEffectiveType(XmlTypeCode typeCode, XmlSchemaDatatypeVari case XmlTypeCode.GMonthDay: case XmlTypeCode.GYear: case XmlTypeCode.GYearMonth: - result = variety == XmlSchemaDatatypeVariety.List ? typeof(string[]) : typeof(string); + resultType = typeof(string); break; case XmlTypeCode.Time: - result = typeof(DateTime); + resultType = typeof(DateTime); break; case XmlTypeCode.Idref: - result = typeof(string); + resultType = typeof(string); break; case XmlTypeCode.Integer: case XmlTypeCode.NegativeInteger: case XmlTypeCode.NonNegativeInteger: case XmlTypeCode.NonPositiveInteger: case XmlTypeCode.PositiveInteger: - if (configuration.IntegerDataType == null || configuration.IntegerDataType == typeof(string)) - { - result = typeof(string); - } - else - { - result = configuration.IntegerDataType; - } + resultType = configuration.IntegerDataType ?? typeof(string); break; default: - result = null; + resultType = type.ValueType; break; } - return result; - } - public static Type GetEffectiveType(this XmlSchemaDatatype type, GeneratorConfiguration configuration) - { - return GetEffectiveType(type.TypeCode, type.Variety, configuration) ?? type.ValueType; - } + if (type.Variety == XmlSchemaDatatypeVariety.List) + { + if (resultType.IsArray) + resultType = resultType.GetElementType(); - public static Type GetEffectiveType(this XmlSchemaType type, GeneratorConfiguration configuration) - { - return GetEffectiveType(type.TypeCode, type.Datatype.Variety, configuration) ?? type.Datatype.ValueType; + // XmlSerializer doesn't support xsd:list for elements, only for attributes: + // https://docs.microsoft.com/en-us/previous-versions/dotnet/netframework-4.0/t84dzyst(v%3dvs.100) + + // Also, de/serialization fails when the XML schema type is ambiguous (DateTime -> date, datetime, or time) + + if (!attribute || resultType == typeof(DateTime)) + resultType = typeof(string); + } + + return resultType; } public static XmlQualifiedName GetQualifiedName(this XmlSchemaType schemaType) diff --git a/XmlSchemaClassGenerator/TypeModel.cs b/XmlSchemaClassGenerator/TypeModel.cs index f2d4a27d..fc88c817 100644 --- a/XmlSchemaClassGenerator/TypeModel.cs +++ b/XmlSchemaClassGenerator/TypeModel.cs @@ -170,7 +170,7 @@ protected void GenerateSerializableAttribute(CodeTypeDeclaration typeDeclaration } } - public virtual CodeTypeReference GetReferenceFor(NamespaceModel referencingNamespace, bool collection, bool forInit = false) + public virtual CodeTypeReference GetReferenceFor(NamespaceModel referencingNamespace, bool collection = false, bool forInit = false, bool attribute = false) { string name; if (referencingNamespace == Namespace) @@ -214,7 +214,7 @@ public override CodeTypeDeclaration Generate() foreach (var property in Properties) property.AddInterfaceMembersTo(interfaceDeclaration); - interfaceDeclaration.BaseTypes.AddRange(Interfaces.Select(i => i.GetReferenceFor(Namespace, false)).ToArray()); + interfaceDeclaration.BaseTypes.AddRange(Interfaces.Select(i => i.GetReferenceFor(Namespace)).ToArray()); return interfaceDeclaration; } @@ -309,11 +309,11 @@ public override CodeTypeDeclaration Generate() { if (BaseClass is ClassModel) { - classDeclaration.BaseTypes.Add(BaseClass.GetReferenceFor(Namespace, false)); + classDeclaration.BaseTypes.Add(BaseClass.GetReferenceFor(Namespace)); } else if (!string.IsNullOrEmpty(Configuration.TextValuePropertyName)) { - var typeReference = BaseClass.GetReferenceFor(Namespace, false); + var typeReference = BaseClass.GetReferenceFor(Namespace); var member = new CodeMemberField(typeReference, Configuration.TextValuePropertyName) { @@ -340,7 +340,7 @@ public override CodeTypeDeclaration Generate() member.Comments.AddRange(DocumentationModel.GetComments(docs).ToArray()); var attribute = new CodeAttributeDeclaration(new CodeTypeReference(typeof(XmlTextAttribute), Configuration.CodeTypeReferenceOptions)); - if (BaseClass is SimpleModel simpleModel && (simpleModel.XmlSchemaType.IsDataTypeAttributeAllowed(Configuration) ?? simpleModel.UseDataTypeAttribute)) + if (BaseClass is SimpleModel simpleModel && (simpleModel.XmlSchemaType.Datatype.IsDataTypeAttributeAllowed(Configuration) ?? simpleModel.UseDataTypeAttribute)) { var name = BaseClass.GetQualifiedName(); if (name.Namespace == XmlSchema.Namespace) @@ -419,11 +419,11 @@ public override CodeTypeDeclaration Generate() foreach (var derivedType in derivedTypes.OrderBy(t => t.Name)) { var includeAttribute = new CodeAttributeDeclaration(new CodeTypeReference(typeof(XmlIncludeAttribute), Configuration.CodeTypeReferenceOptions), - new CodeAttributeArgument(new CodeTypeOfExpression(derivedType.GetReferenceFor(Namespace, false)))); + new CodeAttributeArgument(new CodeTypeOfExpression(derivedType.GetReferenceFor(Namespace)))); classDeclaration.CustomAttributes.Add(includeAttribute); } - classDeclaration.BaseTypes.AddRange(Interfaces.Select(i => i.GetReferenceFor(Namespace, false)).ToArray()); + classDeclaration.BaseTypes.AddRange(Interfaces.Select(i => i.GetReferenceFor(Namespace)).ToArray()); return classDeclaration; } @@ -454,7 +454,7 @@ public override CodeExpression GetDefaultValueFor(string defaultString) using (var writer = new System.IO.StringWriter()) { - CSharpProvider.GenerateCodeFromExpression(new CodeTypeReferenceExpression(GetReferenceFor(null, false)), writer, new CodeGeneratorOptions()); + CSharpProvider.GenerateCodeFromExpression(new CodeTypeReferenceExpression(GetReferenceFor(referencingNamespace: null)), writer, new CodeGeneratorOptions()); reference = writer.ToString(); } @@ -594,7 +594,7 @@ private bool IsArray { get { - return !IsCollection && !IsAttribute && TypeClassModel != null + return !IsCollection && !IsAttribute && !IsList && TypeClassModel != null && TypeClassModel.BaseClass == null && TypeClassModel.Properties.Count == 1 && !TypeClassModel.Properties[0].IsAttribute && !TypeClassModel.Properties[0].IsAny @@ -612,7 +612,7 @@ private bool IsNullableValueType get { return DefaultValue == null - && IsNullable && !(IsCollection || IsArray) + && IsNullable && !(IsCollection || IsArray) && !IsList && ((PropertyType is EnumModel) || (PropertyType is SimpleModel && ((SimpleModel)PropertyType).ValueType.IsValueType)); } } @@ -628,9 +628,22 @@ private bool IsNillableValueType } } + private bool IsList + { + get + { + return Type.XmlSchemaType?.Datatype?.Variety == XmlSchemaDatatypeVariety.List; + } + } + private CodeTypeReference TypeReference { - get { return PropertyType.GetReferenceFor(OwningType.Namespace, IsCollection || IsArray); } + get + { + return PropertyType.GetReferenceFor(OwningType.Namespace, + collection: IsCollection || IsArray || (IsList && IsAttribute), + attribute: IsAttribute); + } } private void AddDocs(CodeTypeMember member) @@ -855,7 +868,7 @@ public void AddMembersTo(CodeTypeDeclaration typeDeclaration, bool withDataBindi if (Configuration.EntityFramework) { member.CustomAttributes.Add(notMappedAttribute); } } } - else if ((IsCollection || isArray) && IsNullable && !IsAttribute) + else if ((IsCollection || isArray || (IsList && IsAttribute)) && IsNullable) { var specifiedProperty = new CodeMemberProperty { @@ -887,7 +900,7 @@ public void AddMembersTo(CodeTypeDeclaration typeDeclaration, bool withDataBindi member.CustomAttributes.AddRange(attributes); // initialize List<> - if (IsCollection || isArray) + if (IsCollection || isArray || (IsList && IsAttribute)) { var constructor = typeDeclaration.Members.OfType().FirstOrDefault(); if (constructor == null) @@ -900,7 +913,7 @@ public void AddMembersTo(CodeTypeDeclaration typeDeclaration, bool withDataBindi } var listReference = requiresBackingField ? (CodeExpression)new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), backingField.Name) : new CodePropertyReferenceExpression(new CodeThisReferenceExpression(), Name); - var initTypeReference = propertyType.GetReferenceFor(OwningType.Namespace, true, true); + var initTypeReference = propertyType.GetReferenceFor(OwningType.Namespace, collection: true, forInit: true, attribute: IsAttribute); var initExpression = new CodeObjectCreateExpression(initTypeReference); constructor.Statements.Add(new CodeAssignStatement(listReference, initExpression)); } @@ -975,7 +988,7 @@ private IEnumerable GetAttributes(bool isArray) { var derivedAttribute = new CodeAttributeDeclaration(new CodeTypeReference(typeof(XmlElementAttribute), Configuration.CodeTypeReferenceOptions), new CodeAttributeArgument(new CodePrimitiveExpression(derivedType.XmlSchemaName.Name)), - new CodeAttributeArgument("Type", new CodeTypeOfExpression(derivedType.GetReferenceFor(OwningType.Namespace, false)))); + new CodeAttributeArgument("Type", new CodeTypeOfExpression(derivedType.GetReferenceFor(OwningType.Namespace)))); if (Order != null) { derivedAttribute.Arguments.Add(new CodeAttributeArgument("Order", @@ -1110,7 +1123,7 @@ public override CodeTypeDeclaration Generate() public override CodeExpression GetDefaultValueFor(string defaultString) { - return new CodeFieldReferenceExpression(new CodeTypeReferenceExpression(GetReferenceFor(null, false)), + return new CodeFieldReferenceExpression(new CodeTypeReferenceExpression(GetReferenceFor(referencingNamespace: null)), Values.First(v => v.Value == defaultString).Name); } } @@ -1160,7 +1173,7 @@ public override CodeTypeDeclaration Generate() return null; } - public override CodeTypeReference GetReferenceFor(NamespaceModel referencingNamespace, bool collection, bool forInit = false) + public override CodeTypeReference GetReferenceFor(NamespaceModel referencingNamespace, bool collection = false, bool forInit = false, bool attribute = false) { var type = ValueType; @@ -1171,8 +1184,8 @@ public override CodeTypeReference GetReferenceFor(NamespaceModel referencingName // http://msdn.microsoft.com/en-us/library/system.xml.serialization.xmlelementattribute.datatype(v=vs.110).aspx // XmlSerializer is inconsistent: maps xs:decimal to decimal but xs:integer to string, // even though xs:integer is a restriction of xs:decimal - type = XmlSchemaType.GetEffectiveType(Configuration); - UseDataTypeAttribute = XmlSchemaType.IsDataTypeAttributeAllowed(Configuration) ?? UseDataTypeAttribute; + type = XmlSchemaType.Datatype.GetEffectiveType(Configuration, attribute); + UseDataTypeAttribute = XmlSchemaType.Datatype.IsDataTypeAttributeAllowed(Configuration) ?? UseDataTypeAttribute; } if (collection) diff --git a/XmlSchemaClassGenerator/XmlSchemaClassGenerator.csproj b/XmlSchemaClassGenerator/XmlSchemaClassGenerator.csproj index 2f540556..1cdd7f36 100644 --- a/XmlSchemaClassGenerator/XmlSchemaClassGenerator.csproj +++ b/XmlSchemaClassGenerator/XmlSchemaClassGenerator.csproj @@ -33,12 +33,11 @@ - - + - +