自定义 XML 格式化程序仅添加必需的命名空间

Custom XML Formatter add only required namespaces

由于客户端应用程序使用带有硬编码前缀的 xpath(不要让我开始),我不得不向 XML 添加命名空间前缀,这些前缀由我的一些 api 端点生成。

我已经成功创建了一个自定义 XmlSerializerOutputFormatter,它覆盖了 Serialize 方法,如下所示:

    public class CustomXmlOutputFormatter : XmlSerializerOutputFormatter
    {
        protected override void Serialize(XmlSerializer xmlSerializer, XmlWriter xmlWriter, object value)
        {
            var namespaces = new XmlQualifiedName[]
            {
                new XmlQualifiedName("xsi", "http://www.w3.org/2001/XMLSchema-instance"),
                new XmlQualifiedName("t1", "my.namespace.one"),
                new XmlQualifiedName("t2", "my.namespace.two"),
                new XmlQualifiedName("t3", "my.namespace.three"),

            };

            var nsManager = new XmlSerializerNamespaces(namespaces);                        
            xmlSerializer.Serialize(xmlWriter, value, nsManager);
        }
    }

只要 XML 是使用正确的命名空间前缀生成的,这就有效。

但是,我现在在生成的每个 XML 中都得到了我的所有命名空间前缀,即使它们没有被使用。 例如,下面的 xml 仅使用 "t1" 命名空间中的项,但由于我在命名空间管理器中声明了所有三个项,所以我在 xml 中获取了所有三个项:

<t1:MyRootNode xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
            xmlns:t1="my.namespace.one" 
            xmlns:t2="my.namespace.two" 
            xmlns:t3="my.namespace.three">
    <t1:Hello>World</t1:Hello>
</t1:MyRootNode>

这显着增加了 XML 的大小,因为我们有超过 200 个不同的命名空间,总共 可以 使用 - 但每个端点实际上只使用最多两个或三个不同的名称空间

我正在寻找的是一种仅包含被序列化对象(或其任何子对象)实际使用的命名空间的方法。

由于我所有的对象都用相关 XML 序列化属性(例如 [XmlElement])进行了注释,并且它们在需要时使用了命名空间参数,我想我可以写一些可怕的反射来解决问题rabbithole 并找到任何正在使用的命名空间。

但是,我希望有一个我看不到的更优雅(并且可能更明显)的解决方案:)

有什么想法吗?

以下是我最终使用反射实现此功能的方式:

    protected override void Serialize(XmlSerializer xmlSerializer, XmlWriter xmlWriter, object value)
    {
        //this is a List<XmlQualifiedName>() that contains all the namespaces and prefixes we could be using - its dynamically built during the constructor from a json file for ease of use.
        var allNamespaces = AllPossibleNamespaces.ToList(); //create a temp copy we can modify

        //remove surplus namespaces
        var usedNamespaces = GetUsedNamespaces(value.GetType());
        allNamespaces.RemoveAll(n => !usedNamespaces.Contains(n.Namespace));

        //now add in the default ones we always want
        allNamespaces.AddRange(requiredNamespaces);

        //finally, serialize the object using the namespaces and prefixes
        var nsManager = new XmlSerializerNamespaces(allNamespaces.ToArray());
        xmlSerializer.Serialize(xmlWriter, value,nsManager);            
    }

    private List<string> GetUsedNamespaces(Type baseType)
    {
        var usedNamespaces = new List<string>();

        var attribs = baseType.GetXmlAttributes();
        baseType.GetProperties().ToList().ForEach(p => attribs.AddRange(p.GetXmlAttributes()));

        foreach (var a in attribs)
        {
            var nsProp = a.GetType().GetProperty("Namespace",typeof(string));
            if (nsProp!=null)
            {
                var ns = (string)nsProp.GetValue(a);
                if (!string.IsNullOrWhiteSpace(ns) && !usedNamespaces.Contains(ns))
                    usedNamespaces.Add(ns);
            }
        }

        foreach(var child in baseType.GetProperties())
        {
            var propType = child.PropertyType;
            var type = propType;
            if (propType.IsClass)
            {
                //dont check inside strings :)
                if (propType != typeof(string))
                {
                    foreach (var iface in propType.GetInterfaces())
                    {
                        if (iface.IsGenericType && iface.GetGenericTypeDefinition() == typeof(IList<>))
                        {
                            type = propType.GetGenericArguments()[0];
                        }
                    }
                    GetUsedNamespaces(type).ForEach(n => { 
                        if (!usedNamespaces.Contains(n)) 
                            usedNamespaces.Add(n);
                    });                        
                }
            }                
        }

        return usedNamespaces;
    }   

如果出现不需要链接反射的更好解决方案,我会给出可接受的答案,因为我宁愿不必这样做:)