Измените свойства на camelCase при сериализации в XML в С#

У меня есть несколько классов DTO, которые сериализуются (де) из XML и в XML. Я хочу использовать соглашение C# PascalCase для свойств, но я хочу, чтобы они отображались как camelCase в XML.

Пример:

[XmlElement("config")]
public ConfigType Config { get; set; }

Здесь свойство имеет значение Config, но отображается как config в XML.

Я считаю, что использование [XmlAttribute] для каждого свойства расточительно, учитывая, что они всегда совпадают с именем свойства, только первая буква не заглавная. Кроме того, если я изменю имя свойства в будущем, я должен не забыть изменить [XmlAttribute], иначе они станут несинхронизированными.

Возможно ли иметь общеклассовый атрибут, который говорит: «используйте верблюжий регистр для свойств, даже если они имеют регистр паскаля», или, что еще лучше, настройку для XmlSerializer?


person sashoalm    schedule 14.06.2017    source источник
comment
Помогает ли weblogs.asp.net/cazzu/129106?   -  person mjwills    schedule 14.06.2017


Ответы (2)


Вы можете создать собственный XmlWriter, обернув тот, который предоставляется фреймворком, примерно так:

public class MyXmlWriter : XmlWriter
{
    private bool disposedValue;
    private XmlWriter writer; // The XmlWriter that will actually write the xml
    public override WriteState WriteState => writer.WriteState;

    public MyXmlWriter(XmlWriter writer)
    {
        this.writer = writer;
    }

    public override void WriteStartElement(string prefix, string localName, string ns)
    {
        localName = char.ToLower(localName[0]) + localName.Substring(1); // Assuming that your properties are in PascalCase we just need to lower-case the first letter.
        writer.WriteStartElement(prefix, localName, ns);
    }

    public override void WriteStartAttribute(string prefix, string localName, string ns)
    {
        // If you want to do the same with attributes you can do the same here
        writer.WriteStartAttribute(prefix, localName, ns); 
    }

    protected override void Dispose(bool disposing)
    {
        if (!disposedValue)
        {
            if (disposing)
            {
                writer.Dispose();
                base.Dispose(disposing);
            }
            disposedValue = true;
        }
    }

    // Wrapping every other methods...
    public override void Flush()
    {
        writer.Flush();
    }

    public override string LookupPrefix(string ns)
    {
        return writer.LookupPrefix(ns);
    }

    public override void WriteBase64(byte[] buffer, int index, int count)
    {
        writer.WriteBase64(buffer, index, count);
    }

    public override void WriteCData(string text)
    {
        writer.WriteCData(text);
    }

    public override void WriteCharEntity(char ch)
    {
        writer.WriteCharEntity(ch);
    }

    public override void WriteChars(char[] buffer, int index, int count)
    {
        writer.WriteChars(buffer, index, count);
    }

    public override void WriteComment(string text)
    {
        writer.WriteComment(text);
    }

    public override void WriteDocType(string name, string pubid, string sysid, string subset)
    {
        writer.WriteDocType(name, pubid, sysid, subset);
    }

    public override void WriteEndAttribute()
    {
        writer.WriteEndAttribute();
    }

    public override void WriteEndDocument()
    {
        writer.WriteEndDocument();
    }

    public override void WriteEndElement()
    {
        writer.WriteEndElement();
    }

    public override void WriteEntityRef(string name)
    {
        writer.WriteEntityRef(name);
    }

    public override void WriteFullEndElement()
    {
        writer.WriteFullEndElement();
    }

    public override void WriteProcessingInstruction(string name, string text)
    {
        writer.WriteProcessingInstruction(name, text);
    }

    public override void WriteRaw(char[] buffer, int index, int count)
    {
        writer.WriteRaw(buffer, index, count);
    }

    public override void WriteRaw(string data)
    {
        writer.WriteRaw(data);
    }

    public override void WriteStartDocument()
    {
        writer.WriteStartDocument();
    }

    public override void WriteStartDocument(bool standalone)
    {
        writer.WriteStartDocument(standalone);
    }

    public override void WriteString(string text)
    {
        writer.WriteString(text);
    }

    public override void WriteSurrogateCharEntity(char lowChar, char highChar)
    {
        writer.WriteSurrogateCharEntity(lowChar, highChar);
    }

    public override void WriteWhitespace(string ws)
    {
        writer.WriteWhitespace(ws);
    }
}

А затем используйте его как:

public class CustomClassOne
{
    public string MyCustomName { get; set; }
    public CustomClassTwo MyOtherProperty { get; set; }
    public CustomClassTwo[] MyArray { get; set; }
}
public class CustomClassTwo
{
    public string MyOtherCustomName { get; set; }
}
.
.
.
static void Main(string[] args)
{
    var myObj = new CustomClassOne()
    {
        MyCustomName = "MYNAME",
        MyOtherProperty = new CustomClassTwo()
        {
            MyOtherCustomName = "MyOtherName"
        },
        MyArray = new CustomClassTwo[]
        {
            new CustomClassTwo(){MyOtherCustomName = "Elem1"},
            new CustomClassTwo(){MyOtherCustomName = "Elem2"}
        }
    };
    var sb = new StringBuilder();
    var serializer = new XmlSerializer(typeof(CustomClassOne));
    var settings = new XmlWriterSettings()
    {
        Indent = true // Indent it so we can see it better
    };
    using (var sw = new StringWriter(sb))
    using (var xw = new MyXmlWriter(XmlWriter.Create(sw, settings)))
    {
        serializer.Serialize(xw, myObj);
    }
    Console.WriteLine(sb.ToString());
}

И вывод будет:

<?xml version="1.0" encoding="utf-16"?>
<customClassOne xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <myCustomName>MYNAME</myCustomName>
  <myOtherProperty>
    <myOtherCustomName>MyOtherName</myOtherCustomName>
  </myOtherProperty>
  <myArray>
    <customClassTwo>
      <myOtherCustomName>Elem1</myOtherCustomName>
    </customClassTwo>
    <customClassTwo>
      <myOtherCustomName>Elem2</myOtherCustomName>
    </customClassTwo>
  </myArray>
</customClassOne>

Чтобы иметь возможность десериализовать, мы можем создать оболочку для XmlReader, подобную той, что была сделана для XmlWriter:

public class MyXmlReader : XmlReader
{
    private bool disposedValue;
    private XmlReader reader;
    // The property names will be added in the XmlNameTable, so we wrap it with a simple class that lower-cases the first letter like we did previously
    private XmlNameTableWrapper nameTable;

    private class XmlNameTableWrapper : XmlNameTable
    {
        private XmlNameTable wrapped;
        // Some names that are added by default to this collection. We can skip the lower casing logic on them.
        private string[] defaultNames = new string[]
            {
                "http://www.w3.org/2001/XMLSchema","http://www.w3.org/2000/10/XMLSchema","http://www.w3.org/1999/XMLSchema","http://microsoft.com/wsdl/types/","http://www.w3.org/2001/XMLSchema-instance","http://www.w3.org/2000/10/XMLSchema-instance","http://www.w3.org/1999/XMLSchema-instance","http://schemas.xmlsoap.org/soap/encoding/","http://www.w3.org/2003/05/soap-encoding","schema","http://schemas.xmlsoap.org/wsdl/","arrayType","null","nil","type","arrayType","itemType","arraySize","Array","anyType"
            };

        public XmlNameTableWrapper(XmlNameTable wrapped)
        {
            this.wrapped = wrapped;
        }

        public override string Add(char[] array, int offset, int length)
        {
            if (array != null && array.Length > 0 && !defaultNames.Any(n => n == new string(array)))
            {
                array[0] = char.ToLower(array[0]);
            }
            return wrapped.Add(array, offset, length);
        }

        public override string Add(string array)
        {
            if (array != null && !defaultNames.Any(n => n == array))
            {
                if (array.Length < 2)
                {
                    array = array.ToLower();
                }
                else
                    array = char.ToLower(array[0]) + array.Substring(1);
            }
            return wrapped.Add(array);
        }

        public override string Get(char[] array, int offset, int length)
        {
            if (array != null && array.Length > 0 && !defaultNames.Any(n => n == new string(array)))
            {
                array[0] = char.ToLower(array[0]);
            }
            return wrapped.Get(array, offset, length);
        }

        public override string Get(string array)
        {
            if (array != null && !defaultNames.Any(n => n == array))
            {
                if (array.Length < 2)
                {
                    array = array.ToLower();
                }
                array = char.ToLower(array[0]) + array.Substring(1);
            }
            return wrapped.Get(array);
        }
    }

    public MyXmlReader(XmlReader reader)
    {
        this.reader = reader;
        nameTable = new XmlNameTableWrapper(reader.NameTable);
    }

    protected override void Dispose(bool disposing)
    {
        if (!disposedValue)
        {
            if (disposing)
            {
                reader.Dispose();
                base.Dispose(disposing);
            }
            disposedValue = true;
        }
    }

    // Instead of returning reader.NameTable we return the wrapper that will care to populate it
    public override XmlNameTable NameTable => nameTable;

    // Everything else does not need additional logic...
    public override XmlNodeType NodeType => reader.NodeType;

    public override string LocalName => reader.LocalName;

    public override string NamespaceURI => reader.NamespaceURI;

    public override string Prefix => reader.Prefix;

    public override string Value => reader.Value;

    public override int Depth => reader.Depth;

    public override string BaseURI => reader.BaseURI;

    public override bool IsEmptyElement => reader.IsEmptyElement;

    public override int AttributeCount => reader.AttributeCount;

    public override bool EOF => reader.EOF;

    public override ReadState ReadState => reader.ReadState;

    public override string GetAttribute(string name)
    {
        return reader.GetAttribute(name);
    }

    public override string GetAttribute(string name, string namespaceURI)
    {
        return reader.GetAttribute(name, namespaceURI);
    }

    public override string GetAttribute(int i)
    {
        return reader.GetAttribute(i);
    }

    public override string LookupNamespace(string prefix)
    {
        return reader.LookupNamespace(prefix);
    }

    public override bool MoveToAttribute(string name)
    {
        return reader.MoveToAttribute(name);
    }

    public override bool MoveToAttribute(string name, string ns)
    {
        return reader.MoveToAttribute(name, ns);
    }

    public override bool MoveToElement()
    {
        return reader.MoveToElement();
    }

    public override bool MoveToFirstAttribute()
    {
        return reader.MoveToFirstAttribute();
    }

    public override bool MoveToNextAttribute()
    {
        return reader.MoveToNextAttribute();
    }

    public override bool Read()
    {
        return reader.Read();
    }

    public override bool ReadAttributeValue()
    {
        return reader.ReadAttributeValue();
    }

    public override void ResolveEntity()
    {
        reader.ResolveEntity();
    }
}

Затем используйте его как:

var serializer = new XmlSerializer(typeof(CustomClassOne));
using (var sr = new StringReader(theXmlGeneratedBefore))
using (var xr = new MyXmlReader(XmlReader.Create(sr)))
{
    var o = serializer.Deserialize(xr);
}
person Matteo Umili    schedule 03.08.2020
comment
Несколько замечаний: 1. Если вы собираетесь использовать это, обратите внимание на defaultNames. Без них десериализация даст вам System.InvalidOperationException : There is an error in the XML document. System.FormatException : Input string was not in a correct format 2. Используйте HashSet для defaultNames для более быстрого поиска 3. Вы можете пропустить реализацию методов, которые принимают char[] array — XmlReader их не вызывает. Это позволит вам изменить длину токена (поскольку это строка), например, при преобразовании camelCase в snake_case. P.S. как вы получили список значений по умолчанию? - person Rast; 04.09.2020
comment
@Rast Спасибо за ваш вклад! Честно говоря, я не очень хорошо помню, как я их получил, но я думаю, что поставил точку останова в методе Add и принял к сведению полученные им аргументы. - person Matteo Umili; 04.09.2020

Это, скорее всего, не лучшее решение (конечно, с точки зрения производительности), но, на мой взгляд, оно довольно лаконичное и аккуратное.

Пример объекта данных с PascalCasing

public class ReportSummary
{
    public string Name { get; set; }
    public bool IsSuccess { get; set; }
}

Использование Newtonsoft.Json

var summary = new ReportSummary { Name = "My Awesome Report", IsSuccess = true };

var resolver = new DefaultContractResolver { NamingStrategy = new CamelCaseNamingStrategy() };
var settings = new JsonSerializerSettings { ContractResolver = resolver };

var json = JsonConvert.SerializeObject(summary, settings);
XNode xml = JsonConvert.DeserializeXNode(json, ToCamelCasing(nameof(ReportSummary)));
Console.WriteLine(xml.ToString());

Помощник ToCamelCasing для узла верхнего уровня

static string ToCamelCasing(string input) 
  => char.ToLowerInvariant(input[0]) + input.Substring(1);

Вывод с camelCasing

<reportSummary>
  <name>My Awesome Report</name>
  <isSuccess>true</isSuccess>
</reportSummary>
person Peter Csala    schedule 31.07.2020
comment
@Zoidbergseasharp Пожалуйста, проверьте это предложение. - person Peter Csala; 31.07.2020
comment
Выглядит очень хорошо. Одна небольшая вещь, которая, возможно, была бы немного лучше, — это использовать вместо этого JsonNamingPolicy.CamelCase.ConvertName. Но как насчет свойств, помеченных XmlAttributeAttribute? Разве они теперь не атрибуты xml? - person Zoidbergseasharp; 03.08.2020
comment
@Zoidbergseasharp Насколько я понимаю, ОП искал альтернативный способ для CamelCase имен свойств без необходимости везде указывать [XmlElement]/[XmlAttribute]. Поэтому мое предлагаемое решение полностью игнорирует эти атрибуты. - person Peter Csala; 03.08.2020