Skip to content

Commit 86d8bd9

Browse files
authored
Merge pull request #573 from Lanayx/master
DateOnly and TimeOnly implementation
2 parents 6f928ee + 78ff980 commit 86d8bd9

8 files changed

Lines changed: 293 additions & 36 deletions

File tree

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,10 @@ Options:
9090
Lines starting with # and empty lines are
9191
ignored.
9292
-o, --output=FOLDER the FOLDER to write the resulting .cs files to
93-
-d, --datetime-offset map xs:datetime and derived types to System.
94-
DateTimeOffset instead of System.DateTime
93+
-d, --datetime-offset map xs:datetime, xs:date and xs:time to System.
94+
DateTimeOffset instead of System.DateTime
95+
--do, --dateOnly map xs:date and xs:time to System.DateOnly and
96+
System.TimeOnly instead of System.DateTime
9597
-i, --integer=TYPE map xs:integer and derived types to TYPE instead
9698
of automatic approximation
9799
TYPE can be i[nt], l[ong], or d[ecimal]

XmlSchemaClassGenerator.Console/Program.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ static int Main(string[] args)
2424
var nameSubstitutes = new List<string>();
2525
var outputFolder = (string)null;
2626
bool dateTimeWithTimeZone = false;
27+
bool useDateOnly = false;
2728
Type integerType = null;
2829
var useIntegerTypeAsFallback = false;
2930
var namespacePrefix = "";
@@ -93,6 +94,7 @@ A file name may be given by appending a pipe sign (|) followed by a file name (l
9394
Lines starting with # and empty lines are ignored.", v => nameSubstituteFiles.Add(v) },
9495
{ "o|output=", "the {FOLDER} to write the resulting .cs files to", v => outputFolder = v },
9596
{ "d|datetime-offset", "map xs:datetime and derived types to System.DateTimeOffset instead of System.DateTime", v => dateTimeWithTimeZone = v != null },
97+
{ "do|dateOnly", "map xs:date to System.DateOnly and xs:time to System.TimeOnly", v => useDateOnly = v != null },
9698
{ "i|integer=", @"map xs:integer and derived types to {TYPE} instead of automatic approximation
9799
{TYPE} can be i[nt], l[ong], or d[ecimal]", v => {
98100
switch (v)
@@ -238,6 +240,7 @@ A file name may be given by appending a pipe sign (|) followed by a file name (l
238240
IntegerDataType = integerType,
239241
UseIntegerDataTypeAsFallback = useIntegerTypeAsFallback,
240242
DateTimeWithTimeZone = dateTimeWithTimeZone,
243+
UseDateOnly = useDateOnly,
241244
EntityFramework = entityFramework,
242245
GenerateInterfaces = interfaces,
243246
AssemblyVisible = assembly,
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.Linq;
5+
using System.Xml.Schema;
6+
using Xunit;
7+
8+
namespace XmlSchemaClassGenerator.Tests;
9+
10+
public sealed class DateOnlyTimeOnlyTests
11+
{
12+
private static IEnumerable<string> ConvertXml(string xsd, Generator generatorPrototype)
13+
{
14+
var writer = new MemoryOutputWriter();
15+
16+
var gen = new Generator
17+
{
18+
OutputWriter = writer,
19+
Version = new("Tests", "1.0.0.1"),
20+
NamespaceProvider = generatorPrototype.NamespaceProvider,
21+
GenerateNullables = generatorPrototype.GenerateNullables,
22+
DateTimeWithTimeZone = generatorPrototype.DateTimeWithTimeZone,
23+
UseDateOnly = generatorPrototype.UseDateOnly,
24+
DataAnnotationMode = generatorPrototype.DataAnnotationMode,
25+
GenerateDesignerCategoryAttribute = generatorPrototype.GenerateDesignerCategoryAttribute,
26+
GenerateComplexTypesForCollections = generatorPrototype.GenerateComplexTypesForCollections,
27+
EntityFramework = generatorPrototype.EntityFramework,
28+
AssemblyVisible = generatorPrototype.AssemblyVisible,
29+
GenerateInterfaces = generatorPrototype.GenerateInterfaces,
30+
MemberVisitor = generatorPrototype.MemberVisitor,
31+
CodeTypeReferenceOptions = generatorPrototype.CodeTypeReferenceOptions
32+
};
33+
34+
var set = new XmlSchemaSet();
35+
36+
using (var stringReader = new StringReader(xsd))
37+
{
38+
var schema = XmlSchema.Read(stringReader, (_, e) => throw new InvalidOperationException($"{e.Severity}: {e.Message}", e.Exception));
39+
ArgumentNullException.ThrowIfNull(schema);
40+
set.Add(schema);
41+
}
42+
43+
gen.Generate(set);
44+
45+
return writer.Content;
46+
}
47+
48+
[Fact]
49+
public void WhenUseDateOnlyIsTrue_DateOnlyAndTimeOnlyAreGenerated()
50+
{
51+
var xsd = @$"<?xml version=""1.0"" encoding=""UTF-8""?>
52+
<xs:schema elementFormDefault=""qualified"" xmlns:xs=""http://www.w3.org/2001/XMLSchema"">
53+
<xs:complexType name=""document"">
54+
<xs:sequence>
55+
<xs:element name=""someDate"" type=""xs:date"" />
56+
<xs:element name=""someTime"" type=""xs:time"" />
57+
</xs:sequence>
58+
</xs:complexType>
59+
</xs:schema>";
60+
61+
var generatedType = ConvertXml(
62+
xsd, new()
63+
{
64+
NamespaceProvider = new()
65+
{
66+
GenerateNamespace = _ => "Test"
67+
},
68+
UseDateOnly = true
69+
});
70+
71+
var code = string.Join(Environment.NewLine, generatedType);
72+
73+
Assert.Contains("public System.DateOnly SomeDate", code);
74+
Assert.Contains("public System.TimeOnly SomeTime", code);
75+
Assert.Contains("DataType=\"date\"", code);
76+
Assert.Contains("DataType=\"time\"", code);
77+
}
78+
79+
[Fact]
80+
public void WhenUseDateOnlyIsFalse_DateTimeIsGenerated()
81+
{
82+
var xsd = @$"<?xml version=""1.0"" encoding=""UTF-8""?>
83+
<xs:schema elementFormDefault=""qualified"" xmlns:xs=""http://www.w3.org/2001/XMLSchema"">
84+
<xs:complexType name=""document"">
85+
<xs:sequence>
86+
<xs:element name=""someDate"" type=""xs:date"" />
87+
<xs:element name=""someTime"" type=""xs:time"" />
88+
</xs:sequence>
89+
</xs:complexType>
90+
</xs:schema>";
91+
92+
var generatedType = ConvertXml(
93+
xsd, new()
94+
{
95+
NamespaceProvider = new()
96+
{
97+
GenerateNamespace = _ => "Test"
98+
},
99+
UseDateOnly = false
100+
});
101+
102+
var code = string.Join(Environment.NewLine, generatedType);
103+
104+
Assert.Contains("public System.DateTime SomeDate", code);
105+
Assert.Contains("public System.DateTime SomeTime", code);
106+
Assert.Contains("DataType=\"date\"", code);
107+
Assert.Contains("DataType=\"time\"", code);
108+
}
109+
[Fact]
110+
public void WhenDefaultValueIsPresent_CodeIsGenerated()
111+
{
112+
var xsd = @$"<?xml version=""1.0"" encoding=""UTF-8""?>
113+
<xs:schema elementFormDefault=""qualified"" xmlns:xs=""http://www.w3.org/2001/XMLSchema"">
114+
<xs:complexType name=""document"">
115+
<xs:sequence>
116+
<xs:element name=""someDate"" type=""xs:date"" default=""2023-10-27"" />
117+
<xs:element name=""someTime"" type=""xs:time"" default=""12:34:56"" />
118+
</xs:sequence>
119+
</xs:complexType>
120+
</xs:schema>";
121+
122+
var generatedType = ConvertXml(
123+
xsd, new()
124+
{
125+
NamespaceProvider = new()
126+
{
127+
GenerateNamespace = _ => "Test"
128+
},
129+
UseDateOnly = true
130+
});
131+
132+
var code = string.Join(Environment.NewLine, generatedType);
133+
134+
Assert.Contains("System.DateOnly.Parse(\"2023-10-27\")", code);
135+
Assert.Contains("System.TimeOnly.Parse(\"12:34:56\")", code);
136+
}
137+
[Fact]
138+
public void WhenUseDateOnlyIsFalse_AndDateTimeWithTimeZoneIsTrue_DateTimeOffsetIsGeneratedForTime()
139+
{
140+
var xsd = @$"<?xml version=""1.0"" encoding=""UTF-8""?>
141+
<xs:schema elementFormDefault=""qualified"" xmlns:xs=""http://www.w3.org/2001/XMLSchema"">
142+
<xs:complexType name=""document"">
143+
<xs:sequence>
144+
<xs:element name=""someTime"" type=""xs:time"" />
145+
</xs:sequence>
146+
</xs:complexType>
147+
</xs:schema>";
148+
149+
var generatedType = ConvertXml(
150+
xsd, new()
151+
{
152+
NamespaceProvider = new()
153+
{
154+
GenerateNamespace = _ => "Test"
155+
},
156+
UseDateOnly = false,
157+
DateTimeWithTimeZone = true
158+
});
159+
160+
var code = string.Join(Environment.NewLine, generatedType);
161+
162+
Assert.Contains("public System.DateTimeOffset SomeTime", code);
163+
Assert.DoesNotContain("DataType=\"time\"", code);
164+
}
165+
[Fact]
166+
public void WhenUseDateOnlyIsFalse_AndDateTimeWithTimeZoneIsTrue_DateTimeOffsetIsGeneratedForDate()
167+
{
168+
var xsd = @$"<?xml version=""1.0"" encoding=""UTF-8""?>
169+
<xs:schema elementFormDefault=""qualified"" xmlns:xs=""http://www.w3.org/2001/XMLSchema"">
170+
<xs:complexType name=""document"">
171+
<xs:sequence>
172+
<xs:element name=""someDate"" type=""xs:date"" />
173+
</xs:sequence>
174+
</xs:complexType>
175+
</xs:schema>";
176+
177+
var generatedType = ConvertXml(
178+
xsd, new()
179+
{
180+
NamespaceProvider = new()
181+
{
182+
GenerateNamespace = _ => "Test"
183+
},
184+
UseDateOnly = false,
185+
DateTimeWithTimeZone = true
186+
});
187+
188+
var code = string.Join(Environment.NewLine, generatedType);
189+
190+
Assert.Contains("public System.DateTimeOffset SomeDate", code);
191+
Assert.DoesNotContain("DataType=\"date\"", code);
192+
}
193+
[Fact]
194+
public void WhenUseDateOnlyIsTrue_AndDateTimeWithTimeZoneIsTrue_DateOnlyAndTimeOnlyAreGenerated_WithDataTypeAttribute()
195+
{
196+
var xsd = @$"<?xml version=""1.0"" encoding=""UTF-8""?>
197+
<xs:schema elementFormDefault=""qualified"" xmlns:xs=""http://www.w3.org/2001/XMLSchema"">
198+
<xs:complexType name=""document"">
199+
<xs:sequence>
200+
<xs:element name=""someDate"" type=""xs:date"" />
201+
<xs:element name=""someTime"" type=""xs:time"" />
202+
</xs:sequence>
203+
</xs:complexType>
204+
</xs:schema>";
205+
206+
var generatedType = ConvertXml(
207+
xsd, new()
208+
{
209+
NamespaceProvider = new()
210+
{
211+
GenerateNamespace = _ => "Test"
212+
},
213+
UseDateOnly = true,
214+
DateTimeWithTimeZone = true
215+
});
216+
217+
var code = string.Join(Environment.NewLine, generatedType);
218+
219+
Assert.Contains("public System.DateOnly SomeDate", code);
220+
Assert.Contains("public System.TimeOnly SomeTime", code);
221+
Assert.Contains("DataType=\"date\"", code);
222+
Assert.Contains("DataType=\"time\"", code);
223+
}
224+
}

XmlSchemaClassGenerator.Tests/DateTimeTypeTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ public void WhenDateTimeOffsetIsNotUsed_DataTypePropertyIsPresent()
104104
}
105105

106106
[Fact]
107-
public void WhenDateTimeOffsetIsNotUsed_DataTypePropertyIsPresent2()
107+
public void WhenDateTimeOffsetIsUsed_DataTypePropertyIsNotPresent()
108108
{
109109
var xsd = @$"<?xml version=""1.0"" encoding=""UTF-8""?>
110110
<xs:schema elementFormDefault=""qualified"" xmlns:xs=""http://www.w3.org/2001/XMLSchema"">
@@ -125,7 +125,7 @@ public void WhenDateTimeOffsetIsNotUsed_DataTypePropertyIsPresent2()
125125
DateTimeWithTimeZone = true
126126
});
127127

128-
var expectedXmlSerializationAttribute = "[System.Xml.Serialization.XmlElementAttribute(\"someDate\", DataType=\"date\")]";
128+
var expectedXmlSerializationAttribute = "[System.Xml.Serialization.XmlElementAttribute(\"someDate\")]";
129129
var generatedProperty = generatedType.First();
130130

131131
Assert.Contains(expectedXmlSerializationAttribute, generatedProperty);

XmlSchemaClassGenerator/CodeUtilities.cs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,9 @@ public static string ToBackingField(this string propertyName, string privateFiel
7878
public static bool? IsDataTypeAttributeAllowed(this XmlSchemaDatatype type, GeneratorConfiguration configuration) => type.TypeCode switch
7979
{
8080
XmlTypeCode.AnyAtomicType => false,// union
81-
XmlTypeCode.DateTime or XmlTypeCode.Time => !configuration.DateTimeWithTimeZone,
82-
XmlTypeCode.Date or XmlTypeCode.Base64Binary or XmlTypeCode.HexBinary => true,
81+
XmlTypeCode.Date or XmlTypeCode.Time when configuration.UseDateOnly => true,
82+
XmlTypeCode.DateTime or XmlTypeCode.Date or XmlTypeCode.Time => !configuration.DateTimeWithTimeZone,
83+
XmlTypeCode.Base64Binary or XmlTypeCode.HexBinary => true,
8384
_ => false,
8485
};
8586

@@ -169,7 +170,9 @@ public static Type GetEffectiveType(this XmlSchemaDatatype type, GeneratorConfig
169170
XmlTypeCode.AnyAtomicType => configuration.MapUnionToWidestCommonType ? GetUnionType(configuration, schemaType, attribute) : typeof(string), // union
170171
XmlTypeCode.AnyUri or XmlTypeCode.GDay or XmlTypeCode.GMonth or XmlTypeCode.GMonthDay or XmlTypeCode.GYear or XmlTypeCode.GYearMonth => typeof(string),
171172
XmlTypeCode.Duration => configuration.NetCoreSpecificCode ? type.ValueType : typeof(string),
172-
XmlTypeCode.Time or XmlTypeCode.DateTime => configuration.DateTimeWithTimeZone ? typeof(DateTimeOffset) : typeof(DateTime),
173+
XmlTypeCode.Time => configuration.UseDateOnly ? typeof(TimeOnly) : (configuration.DateTimeWithTimeZone ? typeof(DateTimeOffset) : typeof(DateTime)),
174+
XmlTypeCode.Date => configuration.UseDateOnly ? typeof(DateOnly) : (configuration.DateTimeWithTimeZone ? typeof(DateTimeOffset) : typeof(DateTime)),
175+
XmlTypeCode.DateTime => configuration.DateTimeWithTimeZone ? typeof(DateTimeOffset) : typeof(DateTime),
173176
XmlTypeCode.Idref => typeof(string),
174177
XmlTypeCode.Integer or XmlTypeCode.NegativeInteger or XmlTypeCode.NonNegativeInteger or XmlTypeCode.NonPositiveInteger or XmlTypeCode.PositiveInteger => GetIntegerDerivedType(type, configuration, restrictions),
175178
XmlTypeCode.Decimal when restrictions.OfType<FractionDigitsRestrictionModel>().SingleOrDefault() is { IsSupported: true, Value: 0 } => GetIntegerDerivedType(type, configuration, restrictions),
@@ -436,6 +439,11 @@ public static CodeTypeReference CreateTypeReference(Type type, GeneratorConfigur
436439
}
437440
else
438441
{
442+
if (type == typeof(DateOnly))
443+
return new CodeTypeReference("System.DateOnly", conf.CodeTypeReferenceOptions);
444+
if (type == typeof(TimeOnly))
445+
return new CodeTypeReference("System.TimeOnly", conf.CodeTypeReferenceOptions);
446+
439447
var typeRef = new CodeTypeReference(type, conf.CodeTypeReferenceOptions);
440448

441449
foreach (var typeArgRef in typeRef.TypeArguments.OfType<CodeTypeReference>())
@@ -530,6 +538,8 @@ private static TypeInfo Make(string @namespace, [CallerMemberName] string name =
530538
public static TypeInfo AllowNull { get; } = Make(CodeAnalysis);
531539
public static TypeInfo MaybeNull { get; } = Make(CodeAnalysis);
532540
}
541+
internal struct DateOnly { }
542+
internal struct TimeOnly { }
533543
}
534544

535545
//Fixes a bug with VS2019 (https://developercommunity.visualstudio.com/content/problem/1244809/error-cs0518-predefined-type-systemruntimecompiler.html)

XmlSchemaClassGenerator/Generator.cs

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,10 @@ private Uri NormalizeUri(Uri baseUri, Uri resolvedUri)
3232
{
3333
case "none": return resolvedUri;
3434
case "same":
35-
{
36-
newScheme = baseUri.Scheme;
37-
break;
38-
}
35+
{
36+
newScheme = baseUri.Scheme;
37+
break;
38+
}
3939
}
4040

4141
var builder = new UriBuilder(resolvedUri) { Scheme = newScheme, Port = -1 };
@@ -53,7 +53,7 @@ public class Generator
5353
public string ForceUriScheme
5454
{
5555
get { return _configuration.ForceUriScheme; }
56-
set { _configuration.ForceUriScheme = value; }
56+
set { _configuration.ForceUriScheme = value; }
5757
}
5858

5959
public NamespaceProvider NamespaceProvider
@@ -228,6 +228,12 @@ public bool DateTimeWithTimeZone
228228
set { _configuration.DateTimeWithTimeZone = value; }
229229
}
230230

231+
public bool UseDateOnly
232+
{
233+
get { return _configuration.UseDateOnly; }
234+
set { _configuration.UseDateOnly = value; }
235+
}
236+
231237
public bool EntityFramework
232238
{
233239
get { return _configuration.EntityFramework; }
@@ -356,7 +362,7 @@ public bool NetCoreSpecificCode
356362
public bool UseArrayItemAttribute
357363
{
358364
get { return _configuration.UseArrayItemAttribute; }
359-
set { _configuration.UseArrayItemAttribute = value;}
365+
set { _configuration.UseArrayItemAttribute = value; }
360366
}
361367

362368
public bool GenerateCommandLineArgumentsComment

XmlSchemaClassGenerator/GeneratorConfiguration.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,10 @@ public NamingScheme NamingScheme
160160
/// </summary>
161161
public bool DateTimeWithTimeZone { get; set; } = false;
162162
/// <summary>
163+
/// Generate DateOnly and TimeOnly properties for xs:time and xs:date elements
164+
/// </summary>
165+
public bool UseDateOnly { get; set; } = false;
166+
/// <summary>
163167
/// Generate Entity Framework Code First compatible classes
164168
/// </summary>
165169
public bool EntityFramework { get; set; }

0 commit comments

Comments
 (0)