In my Startup.ConfigureServices method I have:
services.AddMvc(setupAction =>
{
setupAction.ReturnHttpNotAcceptable = true;
setupAction.OutputFormatters.Add(new XmlSerializerOutputFormatter());
setupAction.InputFormatters.Add(new XmlSerializerInputFormatter());
});
I've also tried:
services.AddMvc(setupAction =>
{
setupAction.ReturnHttpNotAcceptable = true;
setupAction.OutputFormatters.Add(new XmlDataContractSerializerOutputFormatter());
setupAction.InputFormatters.Add(new XmlDataContractSerializerInputFormatter());
});
My POST method's signature looks like:
[HttpPost("xunit")]
public IActionResult PostXunitResults([FromBody] XunitResult results)
XunitResult looks like:
[Serializable]
[XmlRoot("assemblies")]
public class XunitResult
{
[XmlIgnore]
public const string MimeType = "application/xml";
[XmlAttribute("timestamp")]
public string Timestamp { get; set; }
[XmlElement("assembly")]
public List<Assembly> Assemblies { get; set; }
}
[Serializable]
[XmlType("assembly")]
public class Assembly
{
...
With this setup I can run tests to check that my model deserializes the XML correctly:
[Fact]
public void WhenYouDeserializeXunitResultsIntoAnXunitResultsObject_ThenTheObjectIsPopulatedCorrectly()
{
var serializer = new XmlSerializer(typeof(XunitResult));
XunitResult results;
using (Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(_xunitResults)))
{
results = (XunitResult)serializer.Deserialize(stream);
}
results.Assemblies.Should().NotBeNull();
}
... which it does.
When I try to use XunitResult as a [FromBody] argument to the post method, the model is not rendered correctly in Swagger:
Swagger does not display XunitResult correctly in the example, it renders the root as 'XunitResult' instead of 'assemblies' as defined in the XMLRootAttribute on XunitResult.
Also, the same model does not render the Timestamp property in XunitResult as an attribute, it renders it as an element. This is just an example. The other XmlAttributes in the class are rendered as elements as well, and not as attributes like they should be.
The assemblies element is rendered as an assembly should be if it were inside of the assemblies element.
view screenshot for the above 2 items here
The XML model should look similar to this:
<?xml version="1.0" encoding="utf-8"?>
<assemblies timestamp="02/27/2018 14:32:47">
<assembly name="API.Test.dll" environment="64-bit .NET Standard [collection-per-class, parallel (8 threads)]" test-framework="xUnit.net 2.3.1.3858" run-date="2018-02-27" run-time="14:32:47" total="9" passed="5" failed="3" skipped="1" time="0.161" errors="0">
<errors />
<collection total="1" passed="0" failed="1" skipped="0" name="Test collection for API.Test.API.Results.TestResultPost2" time="0.000">
<test name="API.Test.API.Results.TestResultPost2.TestWeWontGetTo" type="API.Test.API.Results.TestResultPost2" method="TestWeWontGetTo" time="0" result="Fail">
<failure exception-type="System.Exception">
<message><![CDATA[System.Exception : exception in setup]]></message>
<stack-trace><![CDATA[ at API.Test.API.Results.TestResultPost2..ctor() in /Users//Documents///API.Test/Controller/TestResultsPost.cs:line 60]]></stack-trace>
</failure>
</test>
</collection>
...
And when I try to post XML according to the model in swagger, OR if I try to post valid XML, the results variable in the PostXunitResults method is null.
If I inspect the body of the call via Request.Body, then the first part of the XML that I posted is gone, only the last part is in the Body stream.
Any help to figure this out would be great!
Related
I have a basic .NET 6 web API project with Swagger and I have a dummy HttpPost endpoint that should accept both JSON and XML. As far as I see, model bindings for JSON are case insensitive but for XML it's not, and here comes my problem.
In Program.cs I have the following settings for XML serialization:
builder.Services.AddControllers(options =>
{
options.InputFormatters.Add(new XmlSerializerInputFormatter(options));
options.OutputFormatters.Add(new XmlSerializerOutputFormatter());
});
My endpoint:
[HttpPost]
public async Task<DummyDto> PostDummyData(DummyDto dummyDto)
{
return await Task.FromResult(dummyDto);
}
My DTO:
public class DummyDto
{
public int Id { get; set; }
public string Name { get; set; }
}
Default example in Swagger UI for application/xml and text/xml:
<?xml version="1.0" encoding="UTF-8"?>
<DummyDto>
<id>0</id>
<name>string</name>
</DummyDto>
As you can see my class name is correct, but my properties are lowercase by default and after a long search I still have no idea how to display my property attributes correctly, or how to disable case sensitivity for XML model binding.
By adding the [XmlElement] attribute to the properties I was able to make it work with lowercase, but I don't like this workaround. Is there any generic solution that I could use for this problem?
By default Web API serializes the fields of types that have a [Serializable] attribute (e.g. Version).
You can add this line of code in Program.cs to stop the Web API from doing this:
builder.Services.AddControllersWithViews().
AddJsonOptions(options =>
{
options.JsonSerializerOptions.PropertyNameCaseInsensitive = true;
options.JsonSerializerOptions.PropertyNamingPolicy = null;
});
Test Result:
I have implemented Swagger/Swashbuckle in my AspNetCore 2.1 app and it's working great. However some of my API models are based on complex WCF XML services and use a few System.Xml.Serialization annotations for adding namespaces and changing the names of properties.
When these models are viewed on the Swagger page they are missing the namespaces and ignore any attribute name changes. Therefore the swagger default requests won't deserialize when posted to my controller.
On the other hand the JSON requests work fine.
Consider these two classes;
public class test
{
[System.Xml.Serialization.XmlElementAttribute(Namespace = "http://www.example.com/ns/v1")]
public test_c ct1 { get; set; }
public string t2 { get; set; }
}
[System.Xml.Serialization.XmlTypeAttribute(AnonymousType = true, Namespace = "http://www.example.com/ns/v1")]
public class test_c
{
[System.Xml.Serialization.XmlElementAttribute("my-new-name")]
public string tc1 { get; set; }
public string tc2 { get; set; }
}
When serialized as XML we get something like;
<?xml version="1.0" encoding="UTF-8"?>
<test xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<ct1 xmlns="http://www.example.com/ns/v1">
<my-new-name>aaa</my-new-name>
<tc2>xxxxxx</tc2>
</ct1>
<t2>bbb</t2>
</test>
This is what is the xml that is expected as the request.
However, the swagger sample request shows as;
<?xml version="1.0" encoding="UTF-8"?>
<test>
<ct1>
<tc1>string</tc1>
<tc2>string</tc2>
</ct1>
<t2>string</t2>
</test>
Which will not deserialize when posted.
So now to the point. All I need/hope to do is modify the swagger request xml schema -- while not affecting the JSON request schema (I don't even know if they are -or can be- separate).
I thought this would be simple but I'm lost sea of swashbuckle options & setup.
I was hoping that I could simply assign the aspnet xml serializer to deserialize a provided request object. Or implement an ISchemaFilter or IOperationsFilter?
If someone could point me in the right direction I'd be forever grateful.
ok, well I'll answer this myself.
I did end up implementing ISchemaFilter. So to answer this question using the same models as in the question, I first created my own implementation of ISchemaFilter and just hardcoded in the checks for the required changes (in reality i'm going to have a big dictionary<> of class and property changes).
The "Schema" class allows us to add XML only meta to the model class or any of its properties.
public class MyRequestISchemaFilter : ISchemaFilter
{
public void Apply(Schema schema, SchemaFilterContext context)
{
if (schema.Type == "object"){
if (context.SystemType == typeof(test_c))
{
schema.Xml = new Xml()
{
Namespace = "http://www.example.com/ns/v1"
};
foreach (var prop in schema.Properties)
{
if (prop.Key == "tc1")
{
prop.Value.Xml = new Xml()
{
Name = "my-new-name"
};
}
}
}
}
}
}
Then we wire-up this filter in our AddSwaggerGen service configure call at startup.
services.AddSwaggerGen(c =>
{
c.SchemaFilter<MyRequestISchemaFilter>();
...
Here is the sample request XML that the filter will produce;
<?xml version="1.0" encoding="UTF-8"?>
<test>
<ct1 xmlns="http://www.example.com/ns/v1">
<my-new-name>string</my-new-name>
<tc2>string</tc2>
</ct1>
<t2>string</t2>
</test>
It's missing the root level XMLSchema namespaces but the Schema::Xml prop doesn't support multiple namespaces. In any case the XML deserializes fine as long as I don't use those namespaces.
I might be able to add them if I add properties with a namespace then set them as Attributes of the root element (which the Xml prop does handle). Haven't tried that though.
I want to serialize a list to xml (from a web-api method).
public class Result
{
public List<string> Users { get; set; }
}
So I get for example:
<result>
<user>Paul</user>
<user>David</user>
<user>Joan</user>
</result>
So far, I get:
<result>
<users>
<user>Paul</user>
<user>David</user>
<user>Joan</user>
</users>
</result>
How do I tell the serialization not to wrap the user list in a "users" tag?
Thanks.
You could either derive from XmlObjectSerializer and implement your own XML Serializer (see here FMI) or else manipulate your type so it works with the default formatter. Which isn't a great solution, but may work for a simple example, like so:
public class Result : List<User>
{
//Any user added to Result will be nested directly within Result in the XML
}
Further reading:
MSDN: XmlObjectSerializer Class:
"Extend the XmlObjectSerializer to create your own serializer to serialize and deserialize objects."
Insights on WCF: CustomXmlObjectSerializer: Real-world example with source code.
You need to replace default DataContractSerializer with XmlSerializer in Application_Start method.
For whole project:
var xml = GlobalConfiguration.Configuration.Formatters.XmlFormatter;
xml.UseXmlSerializer = true;
For specific type:
var xml = GlobalConfiguration.Configuration.Formatters.XmlFormatter;
xml.SetSerializer<Result>(new XmlSerializer(typeof(Result)));
After this you can use attributes to format your xml output:
public class Result
{
[XmlElement("user")]
public List<string> Users { get; set; }
}
I have a asp.net webapi v2 running katana and using XmlSerializer with a very simple custom Serialization implementation:
config.Formatters.XmlFormatter.UseXmlSerializer = true;
config.Formatters.XmlFormatter.SetSerializer<SalesOrder>(new XmlSerializer<SalesOrder>());
XmlSerializer is just a simple implementation of the to System.Runtime.Serialization.XmlObjectSerializer abstract class
When calling a Post like this:
public HttpResponseMessage Post([FromBody]SalesOrder value)
{
return value == null || value.SignZoneCustomerNumber.IsNullOrEmpty()
? Request.CreateResponse(HttpStatusCode.BadRequest)
: Request.CreateResponse(HttpStatusCode.Accepted, _service.CreateOrder(value, Request.GetCorrelationId()));
}
The value is null when the xml contains a declaration like this
<?xml version="1.0" encoding="ISO-8859-15"?>
Strip out the declaration and send the post it desearilizes fine.
Any idea why the declaration causes deseriazation to fail?
The Model is defined like this:
[XmlRoot("Order")]
[Serializable]
public class SalesOrder : BaseOrder<SalesOrderLineItem>
{
public string PoNumber { get; set; }
public DateTime? PoDate { get; set; }
...more properties...
}
Thanks
UPDATE
Turns out it was the encoding that caused the problem
this header works:
<?xml version="1.0" encoding="UTF-8"?>
other values like UTF-16 and ISO-8859-1 fail
any idea why?
I have xml file:
<?xml version="1.0" encoding="utf-8"?>
<LabelTypesCollection xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance="xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<LabelTypes>
<LabelType>
<Name>LabelTypeProduct</Name>
</LabelType>
<LabelType>
<Name>LabelTypeClient</Name>
</LabelType>
</LabelTypes>
</LabelTypesCollection>
And 2 c# classes:
[Serializable]
[XmlRoot("LabelTypesCollection")]
public class LabelTypesCollection
{
private static string _labelTypesCollectionPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), Path.Combine(Program.ProgramName, "LabelTypesCollection.xml"));
[XmlArray("LabelTypes", ElementName="LabelType")]
public List<LabelType> LabelTypes { get; set; }
public static LabelTypesCollection LoadAllLabelTypes()
{
FileInfo fi = new FileInfo(_labelTypesCollectionPath);
if (!fi.Exists)
{
Logger.WriteLog("Could not find size_types_collection.xml file.", new Exception("Could not find size_types_collection.xml file."));
return new LabelTypesCollection();
}
try
{
using (FileStream fs = fi.OpenRead())
{
XmlSerializer serializer = new XmlSerializer(typeof(LabelTypesCollection));
LabelTypesCollection labelTypesCollection = (LabelTypesCollection)serializer.Deserialize(fs);
return labelTypesCollection;
}
}
catch (Exception ex)
{
Logger.WriteLog("Error during loading LabelTypesCollection", ex);
return null;
}
}
}
[Serializable]
public class LabelType
{
[XmlElement("Name")]
public string Name { get; set; }
[XmlIgnore]
public string TranslatedName
{
get
{
string translated = Common.Resources.GetValue(Name);
return (translated == null) ? Name : translated;
}
}
}
And when I call:
LabelTypesCollection.LoadAllLabelTypes();
I get LabelTypeCollection object with empty LabelTypes list. There is no error or anything. Could anyone point me to the problem?
Change this
[XmlArray("LabelTypes", ElementName="LabelType")]
to this
[XmlArray]
The ElementName of an XmlArrayAttribute specifies the element name of the container, and is actually what you specify in the first parameter to the ctor! So the ctor you have says "this class serializes as a container named LabelTypes; no wait actually I want the container to be named LabelType". The named parameter is overwriting what the first unnamed parameter says.
And in fact, since you want the container element to be named LabelTypes, which is what the member is actually called, you don't need to specify it at all.
You may have been thinking of XmlArrayItemAttribute, which controls what the individual members of a serialized collection are named - but you don't need that here either.
My usual approach for working out xml serializer stuff is to build objects manually then look at the xml they serialize to. In this case, using the code you currently have produces xml like this:
<?xml version="1.0" encoding="utf-16"?>
<LabelTypesCollection xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<LabelType>
<LabelType>
<Name>one</Name>
</LabelType>
<LabelType>
<Name>two</Name>
</LabelType>
</LabelType>
</LabelTypesCollection>
which is what tipped me off to the incorrect LabelType specifier.
Note that you also don't need the XmlRoot on LabelTypesCollection, or the XmlElement on Name, since you are just specifying what the xml serializer will come up with anyway.
Here's a suggestion.
Write a small test program that creates an instance of LabelTypesCollection, and adds some LabelType objects into it.
Then use an XmlSerializer to write the object to a file, and look at the Xml you get, to ensure that your input Xml is in the correct schema.
Maybe there's something wrong with one of your Xml elements.
I really think you get an empty list because your code can't find the xml file. Also try instantiating your list. If you have the xml path correctly.
public List<LabelType> LabelTypes = new List<LabelType>();