尝试在XML序列化期间动态控制元素名称时出错

时间:2015-08-20 14:26:54

标签: c# xml xml-serialization

我一直在网上进行研究,包括Stack Overflow,但要么我错过了某些内容,要么我看到的例子不适用于我的情况。

当我尝试在XML序列化期间动态设置根和列表项元素名称时,我收到此错误。

XmlRoot and XmlType attributes may not be specified for the type
System.Collections.Generic.List`1[
  [XmlSerializationFailureExample.Controllers.MyClass, XmlSerializationFailureExample,
  Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]]

我在Microsoft's site上看到了一个相当古老的帖子,其中指出该消息应该是:Only XmlRoot and XmlType attributes may be specified...。果然,如果删除除XmlRoot和XmlType之外的所有覆盖,则会清除错误,但渲染的XML不符合我的需要。

我正在使用XmlSerializer Overrides构造函数,因为我必须动态设置Root和第一个子元素名称。相同的类需要在不同的情况下生成不同的XML元素名称。虽然这个示例只有两个字段,但序列化的实际类有大约100个字段。

那么,在直接序列化List<MyClass>对象时,如何控制根元素和直接子元素的名称?

目标是让XML看起来像这样:

<ArrayOfPerson>
  <Person>
    <Name>John Doe</Name>
    <Age>57</Age>
  </Person>
  <Person>
    <Name>Doe, Jane</Name>
    <Age/>
  </Person>
</ArrayOfPerson>

通过更改覆盖值,我应该能够从同一个类生成这样的XML:

<ArrayOfEmployee>
  <Employee>
    <Name>John Doe</Name>
    <Age>57</Age>
  </Employee>
  <Employee>
    <Name>Doe, Jane</Name>
    <Age/>
  </Employee>
</ArrayOfEmployee>

这是一些演示我的问题的简化代码。我在本例中使用了Visual Studio 2013的基本MVC.Net应用程序。

// GET api/SerializationTest
public ActionResult SerializationTest()
{
    var list = new List<MyClass> {
        new MyClass {Name = "John Doe", Age = 57},
        new MyClass {Name = "Doe, Jane"}
    };

    XmlAttributes xmlPerson = new XmlAttributes {
        XmlRoot = new XmlRootAttribute { ElementName = "Person" }
    };

    XmlAttributes xmlPersonList = new XmlAttributes {
        XmlRoot = new XmlRootAttribute { ElementName = "ArrayOfPerson" },
        XmlArrayItems = {
            new XmlArrayItemAttribute("Person",typeof(MyClass))
        },
    };

    XmlAttributeOverrides overrides = new XmlAttributeOverrides();
    overrides.Add(typeof(MyClass), xmlPerson);
    overrides.Add(typeof(List<MyClass>), xmlPersonList);

    return new XmlResult(
        list,
        "TestFile.xml",
        overrides
    );
}

正在序列化的示例类。实际的班级有大约100个属性。

public class MyClass
{
    public string Name { get; set; }
    public int? Age { get; set; }
}

更新1

如果我将List<MyClass>封装在另一个类中并使用如下属性进行注释,我可以获得我想要的XML。但是我该如何动态地这样做,因为指定的元素名称必须在运行时变化?

[XmlRoot(ElementName = "ArrayOfPerson")]
public class MyCollection
{

    [XmlElement(ElementName = "Person")]
    public List<MyClass> Items { get; set; }

}

更新结束1

XmlResult类型派生自内置ActionResult并包含实际的序列化逻辑。该类旨在返回文件而不是HTML页面。

public class XmlResult : ActionResult
{

    private string FileName { get; set; }
    private object ObjectToSerialize { get; set; }
    private XmlAttributeOverrides Overrides { get; set; }

    public XmlResult(object objectToSerialize, string fileName, XmlAttributeOverrides overrides)
    {
        ObjectToSerialize = objectToSerialize;
        FileName = fileName;
        Overrides = overrides;
    }

    public override void ExecuteResult(ControllerContext context)
    {
        HttpContext.Current.Response.Clear();
        HttpContext.Current.Response.AddHeader("content-control", "no-store, no-cache");
        HttpContext.Current.Response.AddHeader("content-disposition", "attachment;filename=" + FileName);
        HttpContext.Current.Response.ContentType = "text/xml";
        try
        {
            if (ObjectToSerialize != null)
            {
                var xs = new XmlSerializer(ObjectToSerialize.GetType(), Overrides);
                xs.Serialize(HttpContext.Current.Response.Output, ObjectToSerialize);
            }
        }
        catch (Exception ex)
        {
            HttpContext.Current.Response.Write("<error>" + ex + "</error>");
        }
        HttpContext.Current.Response.Flush();
        HttpContext.Current.Response.SuppressContent = true;
        HttpContext.Current.ApplicationInstance.CompleteRequest();
    }

}

1 个答案:

答案 0 :(得分:0)

好吧,我无法让事情像我想的那样动态,但这个解决方案似乎有效。

首先,我使我的类成为一个抽象基类:

public abstract class MyBaseClass
{

    public string Name { get; set; }

    public int? Age { get; set; }

}

然后我为我想要的每个XML元素名称创建了派生类:

public class Person : MyBaseClass { }

public class Employee : MyBaseClass { }

然后,确保我在控制器操作方法中使用了正确的派生类是一件简单的事情。

public ActionResult SerializePersonCollectionByXml()
{
    var list = new List<Person> {
        new Person {Name = "John Doe", Age = 57},
        new Person {Name = "Doe, Jane"}
    };

    return new XmlResult(
        list,
        "PersonCollectionByXml.xml"
    );
}

// GET api/SerializationTest
public ActionResult SerializeEmployeeCollectionByXml()
{
    var list = new List<Employee> {
        new Employee {Name = "John Doe", Age = 57},
        new Employee {Name = "Doe, Jane"}
    };

    return new XmlResult(
        list,
        "EmployeeCollectionByXml.xml"
    );
}

当然,实际应用中的事情有点复杂...... 上面的技术允许我将所有属性和方法保留在基类中,只使用派生类来获取所需的XML元素名称。

真正的应用程序是在运行时根据action方法的输入参数选择PersonEmployee类。此外,逻辑被隐藏在另一组类中,其中包含一个镜像类的继承结构。由于.Net 4.6还不支持与泛型类型(仅接口和委托)的协方差,因此在通过XmlResult类返回之前,我必须经历一些更多的回转以从内部逻辑中获取正确的返回值。

示例:

内部服务类:

public abstract class ServiceBase<TRecord> : IDisposable where TRecord : MyBaseClass
{

    public abstract List<TRecord> Search(SearchParams p);

}

public class ServicePerson : ServiceBase<Person>
{

    public override List<Person> Search(SearchParams p)
    {
        var result = base.Search(p);
        // for example only, just used a simple cast; more complex operation may be required.
        return result.Select(r => (Person)r).ToList();
    }

}

public class ServiceEmployee : ServiceBase<Employee>
{

    public override List<Employee> Search(SearchParams p)
    {
        var result = base.Search(p);
        // for example only, just used a simple cast; more complex operation may be required.
        return result.Select(r => (Employee)r).ToList();
    }

}

Controller的操作方法:

public ActionResult Search(Guid apiKey, SearchParams p)
{
    try
    {
        if (p.UsePerson)
        {
            using (var service = new ServicePerson())
            {
                List<Person> result = service.Search(p);
                return XmlResult(result, "PersonList.xml");
            }
        }
        using (var service = new ServiceEmployee())
        {
            List<Employee> result = service.Search(p);
            return XmlResult(result, "EmployeeList.xml");
        }
    }
    catch (Exception ex)
    {
        // log error
    }
}