LotSizingDataModel.Core 2.0.1
Core domain model, shared abstractions and XML-serializable entities.
Loading...
Searching...
No Matches
SupplyChainXmlSerializer.cs
Go to the documentation of this file.
1using System;
2using System.IO;
3using System.Text;
4using System.Xml;
5using System.Xml.Serialization;
7
9
10/// <summary>
11/// Serializes and deserializes complete supply-chain models
12/// using the XML representation defined by the domain classes.
13/// </summary>
14public sealed class SupplyChainXmlSerializer
15{
16 // UTF-8 without byte-order mark for clean XML files.
17 private static readonly Encoding XmlEncoding =
18 new UTF8Encoding(
19 encoderShouldEmitUTF8Identifier: false);
20
21 private static readonly XmlSerializer Serializer =
22 new(typeof(SupplyChain));
23
24 private readonly SupplyChainValidator _validator;
25
26 /// <summary>
27 /// Initializes the serializer with a default
28 /// supply-chain validator.
29 /// </summary>
31 : this(new SupplyChainValidator())
32 {
33 }
34
35 /// <summary>
36 /// Initializes the serializer with the specified validator.
37 /// </summary>
38 /// <param name="validator">
39 /// Validator used before serialization and after deserialization.
40 /// </param>
42 SupplyChainValidator validator)
43 {
44 _validator = validator ??
45 throw new ArgumentNullException(nameof(validator));
46 }
47
48 /// <summary>
49 /// Saves a supply chain to an XML file.
50 ///
51 /// The model is validated before serialization by default.
52 /// The file is first written to a temporary location and then
53 /// moved to the destination to reduce the risk of leaving a
54 /// partially written XML file.
55 /// </summary>
56 /// <param name="filePath">
57 /// Destination XML file path.
58 /// </param>
59 /// <param name="supplyChain">
60 /// Supply chain to serialize.
61 /// </param>
62 /// <param name="validateBeforeSave">
63 /// Indicates whether the model must be validated before writing.
64 /// </param>
65 public void Save(
66 string filePath,
67 SupplyChain supplyChain,
68 bool validateBeforeSave = true)
69 {
70 ArgumentException.ThrowIfNullOrWhiteSpace(filePath);
71 ArgumentNullException.ThrowIfNull(supplyChain);
72
73 string fullPath =
74 Path.GetFullPath(filePath);
75
76 string? directoryPath =
77 Path.GetDirectoryName(fullPath);
78
79 if (string.IsNullOrWhiteSpace(directoryPath))
80 {
81 throw new InvalidOperationException(
82 "The destination directory cannot be determined.");
83 }
84
85 Directory.CreateDirectory(directoryPath);
86
87 // Generate a unique temporary file name in the destination directory.
88 string temporaryFilePath =
89 Path.Combine(
90 directoryPath,
91 "." +
92 Path.GetFileName(fullPath) +
93 "." +
94 Guid.NewGuid().ToString("N") +
95 ".tmp");
96
97 try
98 {
99 using (var stream = new FileStream(
100 temporaryFilePath,
101 FileMode.CreateNew,
102 FileAccess.Write,
103 FileShare.None))
104 {
105 Serialize(
106 stream,
107 supplyChain,
108 validateBeforeSave);
109 }
110
111 // Replace the destination file atomically.
112 File.Move(
113 temporaryFilePath,
114 fullPath,
115 overwrite: true);
116 }
117 finally
118 {
119 /*
120 * The temporary file remains only if serialization
121 * or replacement of the destination file failed.
122 */
123 if (File.Exists(temporaryFilePath))
124 {
125 File.Delete(temporaryFilePath);
126 }
127 }
128 }
129
130 /// <summary>
131 /// Loads a supply chain from an XML file.
132 /// </summary>
133 /// <param name="filePath">
134 /// Source XML file path.
135 /// </param>
136 /// <param name="validateAfterLoad">
137 /// Indicates whether the deserialized model must be validated.
138 /// </param>
139 /// <param name="synchronizePlanningHorizon">
140 /// Indicates whether all time series must be resized to the
141 /// global planning horizon before validation.
142 ///
143 /// Leave this option false to detect inconsistent series
144 /// in the source XML instead of silently correcting them.
145 /// </param>
146 /// <returns>
147 /// Deserialized supply-chain model.
148 /// </returns>
150 string filePath,
151 bool validateAfterLoad = true,
152 bool synchronizePlanningHorizon = false)
153 {
154 ArgumentException.ThrowIfNullOrWhiteSpace(filePath);
155
156 string fullPath =
157 Path.GetFullPath(filePath);
158
159 if (!File.Exists(fullPath))
160 {
161 throw new FileNotFoundException(
162 "The supply-chain XML file does not exist.",
163 fullPath);
164 }
165
166 using var stream = new FileStream(
167 fullPath,
168 FileMode.Open,
169 FileAccess.Read,
170 FileShare.Read);
171
172 return Deserialize(
173 stream,
174 validateAfterLoad,
175 synchronizePlanningHorizon);
176 }
177
178 /// <summary>
179 /// Serializes a supply chain to an output stream.
180 ///
181 /// The stream remains open after this method returns.
182 /// </summary>
183 /// <param name="output">
184 /// Writable output stream.
185 /// </param>
186 /// <param name="supplyChain">
187 /// Supply chain to serialize.
188 /// </param>
189 /// <param name="validateBeforeSerialization">
190 /// Indicates whether the model must be validated first.
191 /// </param>
192 public void Serialize(
193 Stream output,
194 SupplyChain supplyChain,
195 bool validateBeforeSerialization = true)
196 {
197 ArgumentNullException.ThrowIfNull(output);
198 ArgumentNullException.ThrowIfNull(supplyChain);
199
200 if (!output.CanWrite)
201 {
202 throw new ArgumentException(
203 "The output stream must be writable.",
204 nameof(output));
205 }
206
207 if (validateBeforeSerialization)
208 {
209 _validator.ThrowIfInvalid(supplyChain);
210 }
211
212 // Prepare namespace settings to omit default xsi/xsd declarations.
213 var namespaces =
214 new XmlSerializerNamespaces();
215
216 /*
217 * Prevents the unnecessary xsi and xsd namespace
218 * declarations from being written to the root element.
219 */
220 namespaces.Add(
221 string.Empty,
222 string.Empty);
223
224 var settings = new XmlWriterSettings
225 {
226 Encoding = XmlEncoding,
227 Indent = true,
228 IndentChars = " ",
229 NewLineChars = Environment.NewLine,
230 NewLineHandling = NewLineHandling.Replace,
231 OmitXmlDeclaration = false,
232 CloseOutput = false // Keep the stream open after writing.
233 };
234
235 using XmlWriter writer =
236 XmlWriter.Create(
237 output,
238 settings);
239
240 Serializer.Serialize(
241 writer,
242 supplyChain,
243 namespaces);
244 }
245
246 /// <summary>
247 /// Deserializes a supply chain from an input stream.
248 ///
249 /// The stream remains open after this method returns.
250 /// </summary>
251 /// <param name="input">
252 /// Readable input stream.
253 /// </param>
254 /// <param name="validateAfterDeserialization">
255 /// Indicates whether the model must be validated after loading.
256 /// </param>
257 /// <param name="synchronizePlanningHorizon">
258 /// Indicates whether all time series must be resized to the
259 /// global planning horizon before validation.
260 /// </param>
261 /// <returns>
262 /// Deserialized supply-chain model.
263 /// </returns>
265 Stream input,
266 bool validateAfterDeserialization = true,
267 bool synchronizePlanningHorizon = false)
268 {
269 ArgumentNullException.ThrowIfNull(input);
270
271 if (!input.CanRead)
272 {
273 throw new ArgumentException(
274 "The input stream must be readable.",
275 nameof(input));
276 }
277
278 var settings = new XmlReaderSettings
279 {
280 /*
281 * DTD processing and external entity resolution are
282 * disabled to prevent XML external-entity attacks.
283 */
284 DtdProcessing = DtdProcessing.Prohibit,
285 XmlResolver = null,
286 IgnoreComments = true,
287 IgnoreProcessingInstructions = true,
288 CloseInput = false // Keep the stream open after reading.
289 };
290
291 using XmlReader reader =
292 XmlReader.Create(
293 input,
294 settings);
295
296 var document = TransportXmlMigration.ReadAndMigrate(reader);
297 using var migratedReader = document.CreateReader();
298 object? result =
299 Serializer.Deserialize(migratedReader);
300
301 if (result is not SupplyChain supplyChain)
302 {
303 throw new InvalidOperationException(
304 "The XML document does not contain a valid " +
305 "supply-chain root object.");
306 }
307
308 /*
309 * In strict mode, synchronization is not performed.
310 * This allows the validator to report malformed or
311 * inconsistent planning horizons from the XML file.
312 */
313 if (synchronizePlanningHorizon)
314 {
315 supplyChain.SynchronizePlanningHorizon();
316 }
317
318 if (validateAfterDeserialization)
319 {
320 _validator.ThrowIfInvalid(supplyChain);
321 }
322
323 return supplyChain;
324 }
325
326 /// <summary>
327 /// Serializes a supply chain to an XML string.
328 /// </summary>
329 /// <param name="supplyChain">
330 /// Supply chain to serialize.
331 /// </param>
332 /// <param name="validateBeforeSerialization">
333 /// Indicates whether the model must be validated first.
334 /// </param>
335 /// <returns>
336 /// UTF-8 XML representation of the supply chain.
337 /// </returns>
338 public string SerializeToString(
339 SupplyChain supplyChain,
340 bool validateBeforeSerialization = true)
341 {
342 ArgumentNullException.ThrowIfNull(supplyChain);
343
344 using var stream = new MemoryStream();
345
346 Serialize(
347 stream,
348 supplyChain,
349 validateBeforeSerialization);
350
351 return XmlEncoding.GetString(
352 stream.ToArray());
353 }
354
355 /// <summary>
356 /// Deserializes a supply chain from an XML string.
357 /// </summary>
358 /// <param name="xml">
359 /// XML representation of the supply chain.
360 /// </param>
361 /// <param name="validateAfterDeserialization">
362 /// Indicates whether the resulting model must be validated.
363 /// </param>
364 /// <param name="synchronizePlanningHorizon">
365 /// Indicates whether all time series must be resized to the
366 /// global planning horizon before validation.
367 /// </param>
368 /// <returns>
369 /// Deserialized supply-chain model.
370 /// </returns>
372 string xml,
373 bool validateAfterDeserialization = true,
374 bool synchronizePlanningHorizon = false)
375 {
376 ArgumentException.ThrowIfNullOrWhiteSpace(xml);
377
378 byte[] xmlBytes =
379 XmlEncoding.GetBytes(xml);
380
381 using var stream =
382 new MemoryStream(
383 xmlBytes,
384 writable: false);
385
386 return Deserialize(
387 stream,
388 validateAfterDeserialization,
389 synchronizePlanningHorizon);
390 }
391}
void Save(string filePath, SupplyChain supplyChain, bool validateBeforeSave=true)
Saves a supply chain to an XML file.
SupplyChain DeserializeFromString(string xml, bool validateAfterDeserialization=true, bool synchronizePlanningHorizon=false)
Deserializes a supply chain from an XML string.
SupplyChain Load(string filePath, bool validateAfterLoad=true, bool synchronizePlanningHorizon=false)
Loads a supply chain from an XML file.
SupplyChainXmlSerializer()
Initializes the serializer with a default supply-chain validator.
string SerializeToString(SupplyChain supplyChain, bool validateBeforeSerialization=true)
Serializes a supply chain to an XML string.
SupplyChain Deserialize(Stream input, bool validateAfterDeserialization=true, bool synchronizePlanningHorizon=false)
Deserializes a supply chain from an input stream.
SupplyChainXmlSerializer(SupplyChainValidator validator)
Initializes the serializer with the specified validator.
void Serialize(Stream output, SupplyChain supplyChain, bool validateBeforeSerialization=true)
Serializes a supply chain to an output stream.
Lossless, in-memory migration of legacy nested transport lanes. Input files are never written.
static XDocument ReadAndMigrate(XmlReader reader)
Loads XML through the caller's secure reader and returns a migrated copy.
Represents the complete supply-chain data model.
Validates the structural, referential and decision-model consistency of a complete supply chain.