@@ -313,5 +313,150 @@ public void Write_emits_no_extension_when_VendorExtensions_is_all_nulls()
313313 xml ,
314314 Is . EqualTo ( "<Configuration />" ) . Or . EqualTo ( "<Configuration></Configuration>" ) ) ;
315315 }
316+
317+ // ---------------- interface contract ----------------
318+
319+ /// <summary>The <see cref="IConfiguration.VendorExtensions"/> interface
320+ /// getter reflects the value set through the concrete
321+ /// <see cref="Configuration.VendorExtensions"/> setter — the
322+ /// polymorphic surface projects the concrete slot without a
323+ /// separate backing field.</summary>
324+ [ Test ]
325+ public void IConfiguration_VendorExtensions_getter_reflects_concrete_setter ( )
326+ {
327+ var payload = new [ ]
328+ {
329+ XElement . Parse ( "<v:V xmlns:v=\" urn:v\" >payload</v:V>" )
330+ } ;
331+ IConfiguration configuration = new Configuration
332+ {
333+ VendorExtensions = payload
334+ } ;
335+
336+ Assert . That ( configuration . VendorExtensions , Is . SameAs ( payload ) ) ;
337+ }
338+
339+ // ---------------- mixed content ----------------
340+
341+ /// <summary>A <c>Configuration</c> that carries a standard
342+ /// <c>Motion</c> child AND a vendor extension round-trips through
343+ /// write + read with both slots populated. Pins the branch of
344+ /// <see cref="XmlConfiguration.ToConfiguration"/> where standard
345+ /// children AND vendor-namespaced children coexist inside the
346+ /// same envelope.</summary>
347+ [ Test ]
348+ public void Round_trip_preserves_standard_child_and_vendor_extension_together ( )
349+ {
350+ var original = new Configuration
351+ {
352+ Motion = new Motion
353+ {
354+ Id = "m1" ,
355+ Type = MotionType . PRISMATIC ,
356+ Actuation = MotionActuationType . DIRECT ,
357+ Axis = new Axis { Value = "1 2 3" }
358+ } ,
359+ VendorExtensions = new [ ]
360+ {
361+ XElement . Parse (
362+ "<vendor:Custom xmlns:vendor=\" urn:vendor:mtconnect\" "
363+ + "id=\" custom-1\" >"
364+ + "<Payload>ping</Payload>"
365+ + "</vendor:Custom>" )
366+ }
367+ } ;
368+
369+ var xml = XmlRoundTripHelper . Write ( w =>
370+ XmlConfiguration . WriteXml ( w , original , outputComments : false ) ) ;
371+ var wire = XmlRoundTripHelper . Read < XmlConfiguration > ( xml ) ;
372+ var round = wire . ToConfiguration ( ) ;
373+
374+ Assert . That ( round . Motion , Is . Not . Null ) ;
375+ Assert . That ( round . Motion ! . Id , Is . EqualTo ( "m1" ) ) ;
376+
377+ Assert . That ( round . VendorExtensions , Is . Not . Null ) ;
378+ var extensions = round . VendorExtensions . ToList ( ) ;
379+ Assert . That ( extensions , Has . Count . EqualTo ( 1 ) ) ;
380+ Assert . That ( extensions [ 0 ] . Name . LocalName , Is . EqualTo ( "Custom" ) ) ;
381+ Assert . That ( extensions [ 0 ] . Attribute ( "id" ) ? . Value , Is . EqualTo ( "custom-1" ) ) ;
382+ Assert . That ( extensions [ 0 ] . Element ( "Payload" ) ? . Value , Is . EqualTo ( "ping" ) ) ;
383+ }
384+
385+ // ---------------- attribute + text preservation ----------------
386+
387+ /// <summary>Vendor extensions with attributes on both the root element
388+ /// AND nested descendants round-trip verbatim — the <c>WriteRaw</c>
389+ /// path on the write side and <c>XElement.Parse</c> with
390+ /// <see cref="LoadOptions.PreserveWhitespace"/> on the read side
391+ /// preserve the authored structure.</summary>
392+ [ Test ]
393+ public void Round_trip_preserves_nested_attributes_verbatim ( )
394+ {
395+ var original = new Configuration
396+ {
397+ VendorExtensions = new [ ]
398+ {
399+ XElement . Parse (
400+ "<vendor:Ext xmlns:vendor=\" urn:vendor\" rootAttr=\" 1\" >"
401+ + "<Child childAttr=\" A\" order=\" first\" >alpha</Child>"
402+ + "<Child childAttr=\" B\" order=\" second\" >beta</Child>"
403+ + "</vendor:Ext>" )
404+ }
405+ } ;
406+
407+ var xml = XmlRoundTripHelper . Write ( w =>
408+ XmlConfiguration . WriteXml ( w , original , outputComments : false ) ) ;
409+ var wire = XmlRoundTripHelper . Read < XmlConfiguration > ( xml ) ;
410+ var round = wire . ToConfiguration ( ) ;
411+
412+ var extensions = round . VendorExtensions . ToList ( ) ;
413+ Assert . That ( extensions , Has . Count . EqualTo ( 1 ) ) ;
414+ var ext = extensions [ 0 ] ;
415+
416+ Assert . That ( ext . Attribute ( "rootAttr" ) ? . Value , Is . EqualTo ( "1" ) ) ;
417+ var children = ext . Elements ( "Child" ) . ToList ( ) ;
418+ Assert . That ( children , Has . Count . EqualTo ( 2 ) ) ;
419+ Assert . That ( children [ 0 ] . Attribute ( "childAttr" ) ? . Value , Is . EqualTo ( "A" ) ) ;
420+ Assert . That ( children [ 0 ] . Attribute ( "order" ) ? . Value , Is . EqualTo ( "first" ) ) ;
421+ Assert . That ( children [ 0 ] . Value , Is . EqualTo ( "alpha" ) ) ;
422+ Assert . That ( children [ 1 ] . Attribute ( "childAttr" ) ? . Value , Is . EqualTo ( "B" ) ) ;
423+ Assert . That ( children [ 1 ] . Attribute ( "order" ) ? . Value , Is . EqualTo ( "second" ) ) ;
424+ Assert . That ( children [ 1 ] . Value , Is . EqualTo ( "beta" ) ) ;
425+ }
426+
427+ // ---------------- multiple vendors, distinct namespaces ----------------
428+
429+ /// <summary>Two vendor extensions from distinct vendor namespaces
430+ /// round-trip independently — each keeps its own namespace binding,
431+ /// and the deserialiser distinguishes them by fully-qualified
432+ /// <see cref="XName"/> rather than by local name alone.</summary>
433+ [ Test ]
434+ public void Round_trip_preserves_distinct_vendor_namespaces ( )
435+ {
436+ var original = new Configuration
437+ {
438+ VendorExtensions = new [ ]
439+ {
440+ XElement . Parse (
441+ "<v1:Ext xmlns:v1=\" urn:vendor-one\" >one</v1:Ext>" ) ,
442+ XElement . Parse (
443+ "<v2:Ext xmlns:v2=\" urn:vendor-two\" >two</v2:Ext>" )
444+ }
445+ } ;
446+
447+ var xml = XmlRoundTripHelper . Write ( w =>
448+ XmlConfiguration . WriteXml ( w , original , outputComments : false ) ) ;
449+ var wire = XmlRoundTripHelper . Read < XmlConfiguration > ( xml ) ;
450+ var round = wire . ToConfiguration ( ) ;
451+
452+ var extensions = round . VendorExtensions . ToList ( ) ;
453+ Assert . That ( extensions , Has . Count . EqualTo ( 2 ) ) ;
454+ Assert . That ( extensions [ 0 ] . Name . NamespaceName , Is . EqualTo ( "urn:vendor-one" ) ) ;
455+ Assert . That ( extensions [ 0 ] . Name . LocalName , Is . EqualTo ( "Ext" ) ) ;
456+ Assert . That ( extensions [ 0 ] . Value , Is . EqualTo ( "one" ) ) ;
457+ Assert . That ( extensions [ 1 ] . Name . NamespaceName , Is . EqualTo ( "urn:vendor-two" ) ) ;
458+ Assert . That ( extensions [ 1 ] . Name . LocalName , Is . EqualTo ( "Ext" ) ) ;
459+ Assert . That ( extensions [ 1 ] . Value , Is . EqualTo ( "two" ) ) ;
460+ }
316461 }
317462}
0 commit comments