Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

XML namespace imports are not supported #836

Closed
Noah1989 opened this issue Feb 13, 2022 · 2 comments
Closed

XML namespace imports are not supported #836

Noah1989 opened this issue Feb 13, 2022 · 2 comments
Labels
VB -> C# Specific to VB -> C# conversion

Comments

@Noah1989
Copy link
Contributor

Example taken from the Documentation:

' Place Imports statements at the top of your program.  
Imports <xmlns="http://DefaultNamespace">
Imports <xmlns:ns="http://NewNamespace">

Module Module1

  Sub Main()
    ' Create element by using the default global XML namespace. 
    Dim inner = <innerElement/>

    ' Create element by using both the default global XML namespace
    ' and the namespace identified with the "ns" prefix.
    Dim outer = <ns:outer>
                  <ns:innerElement></ns:innerElement>
                  <siblingElement></siblingElement>
                  <%= inner %>
                </ns:outer>

    ' Display element to see its final form. 
    Console.WriteLine(outer)
  End Sub

End Module

Should produce an XElement that prints like this:

<ns:outer xmlns="http://DefaultNamespace"
          xmlns:ns="http://NewNamespace">
  <ns:innerElement></ns:innerElement>
  <siblingElement></siblingElement>
  <innerElement />
</ns:outer>

I tried reproducing that manually in C# and got the following code:

using System;
using System.Xml.Linq;

public partial class Program
{
    public static void Main()
    {
        var inner = new XElement("{http://DefaultNamespace}innerElement");
        var outer = new XElement("{http://NewNamespace}outer", 
                                 new XAttribute("xmlns", "http://DefaultNamespace"), 
                                 new XAttribute(XNamespace.Xmlns + "ns", "http://NewNamespace"), 
                                 new XElement("{http://NewNamespace}innerElement", ""), 
                                 new XElement("{http://DefaultNamespace}siblingElement", ""), 
                                 inner);
        Console.WriteLine(outer);
    }
}

The only weird thing about that are the two xmlns attributes. They are only present on the outermost XElement. But how do we know which one is the outermost? Indeed experimenting with VB.NET reveals, that whenever an XElement is created, the compiler adds code that "steals" the attributes from the inner elements. This can be verified by adding the following code to the example:

		Dim wtf = <foobar><%= outer %></foobar>			
		Console.WriteLine(outer)

This causes a mutation on outer after the fact, and it loses the xmlns="..." attribute, resulting in an equivalent, but more verbose representation, where the namespace is specified for each child element.

The problem is now, that replicating this behavior would result in very ugly C# code. My suggestion would be to generate the C# code without specifying any additional xmlns attributes:

using System;
using System.Xml.Linq;

public partial class Program
{
    public static void Main()
    {
        var inner = new XElement("{http://DefaultNamespace}innerElement");
        var outer = new XElement("{http://NewNamespace}outer", 
                                 new XElement("{http://NewNamespace}innerElement", ""), 
                                 new XElement("{http://DefaultNamespace}siblingElement", ""), 
                                 inner);
        Console.WriteLine(outer);
    }
}

Resulting in this XML:

<outer xmlns="http://NewNamespace">
  <innerElement></innerElement>
  <siblingElement xmlns="http://DefaultNamespace"></siblingElement>
  <innerElement xmlns="http://DefaultNamespace" />
</outer>

Which causes explicit xmlns attributes on each element (except for those equal to their parent). Ignoring which namespace has actually been imported as default, and dropping any aliases declared by imports.

Does anyone have a better idea? Have a look at (decompiled from IL) what VB actually does here.

@Noah1989
Copy link
Contributor Author

Well, upon further consideration, I think there is a way to replicate what VB does. First, generate a helper class from the namespace imports. This happens once per file, so we need to use something unique to avoid conflicts. Thankfully, a using directive can be used to give a shorter alias. Luckily, one can use using even before the declaration of the class itself.

Imports <xmlns="http://DefaultNamespace">
Imports <xmlns:ns="http://NewNamespace">

is therefore translated into this montrosity:

using XmlImports = XmlImports_3256c4d5_43af_4e4e_897f_71f64e2b775f;
internal static class XmlImports_3256c4d5_43af_4e4e_897f_71f64e2b775f
{
    internal static readonly XNamespace _ = "http://DefaultNamespace";
    internal static readonly XNamespace ns = "http://NewNamespace";
    
    private static XAttribute[] namespaceAttributes = {
              new XAttribute("xmlns", _.NamespaceName),
              new XAttribute(XNamespace.Xmlns + "ns", ns.NamespaceName)
    };
    
    internal static XElement Apply(XElement x) 
    {
        foreach (var d in x.Descendants()) {
            foreach (var n in namespaceAttributes) {
                var a = d.Attribute(n.Name);
                if(a != null && a.Value == n.Value) {
                    a.Remove();
                }
            }
        }
        x.Add(namespaceAttributes);
        return x;
    }    
}

Now the following VB code can be translated in a pretty straightforward way by adding a call to XmlImports.Apply on each XElementSyntax, where the parent is not also an XElementSyntax. Also references to the XNamespace from the generated hepler can be used to avoid runtime overhead. This would result in this conversion of the remaining file:

' XML imports were here

Public Module Module1
    Public Sub Main()
        ' Create element by using the default global XML namespace. 
        Dim inner = <innerElement/>

        ' Create element by using both the default global XML namespace
        ' and the namespace identified with the "ns" prefix.
        Dim outer = <ns:outer>
                      <ns:innerElement></ns:innerElement>
                      <siblingElement></siblingElement>
                      <%= inner %>
                    </ns:outer>
       
        Console.WriteLine(outer)        
        Console.WriteLine()
        
        ' As soon as we use the element in another one, the explicit xmlns attributes get stolen!
        Dim wtf = <foobar><%= outer %></foobar>        
        Console.WriteLine(outer)

    End Sub
End Module
// Xmlmports hepler goes here

public partial class Program
{    
    public static void Main()
    {
        //  Create element by using the default global XML namespace. 
        var inner = XmlImports.Apply(new XElement(XmlImports._ + "innerElement"));   

        // Create element by using both the default global XML namespace
        // and the namespace identified with the "ns" prefix.     
        var outer = XmlImports.Apply(new XElement(XmlImports.ns + "outer",
                                                  new XElement(XmlImports.ns + "innerElement", ""),
                                                  new XElement(XmlImports._ + "siblingElement", ""),
                                                  inner));
        Console.WriteLine(outer);        
        Console.WriteLine();
        
        // As soon as we use the element in another one, the explicit xmlns attributes get stolen!
        var wtf = XmlImports.Apply(new XElement(XmlImports._ + "foobar", outer));
        Console.WriteLine(outer);                           
    }
}

This way, both produce the same output:

<ns:outer xmlns="http://DefaultNamespace" xmlns:ns="http://NewNamespace">
  <ns:innerElement></ns:innerElement>
  <siblingElement></siblingElement>
  <innerElement />
</ns:outer>

<ns:outer xmlns:ns="http://NewNamespace">
  <ns:innerElement></ns:innerElement>
  <siblingElement xmlns="http://DefaultNamespace"></siblingElement>
  <innerElement xmlns="http://DefaultNamespace" />
</ns:outer>

Any objections to this appoach?

@GrahamTheCoder
Copy link
Member

GrahamTheCoder commented Feb 14, 2022

No objections to the general direction, it looks like you're onto something. I haven't looked closely yet, but perhaps you can get away with a single general helper class? Each call to apply would pass an array of the attributes stored in that class's field (ideally a short name, or you could add a private expression bodied method in the using class to make it shorter at the callsite)
private XmlElemnent Xml(string str) => XmlHelper. Apply(str, _ns)

@GrahamTheCoder GrahamTheCoder added the VB -> C# Specific to VB -> C# conversion label Feb 15, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
VB -> C# Specific to VB -> C# conversion
Projects
None yet
Development

No branches or pull requests

2 participants