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.
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:
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!
I have a wcf restful service on a IIS server.
I have made some API, which can be called sending both xml or json.
I've made my C# classes and then, I'm testing it. With JSON is perfect, but I have still some issues with XML request.
I want to send the xml with a post and this is the xml I send:
<?xml version="1.0" encoding="utf-8" ?>
<SetClientiXML
xmlns="http://tempuri.org/">
<dati>
<ArrayOfWrapClienti
xmlns="http://schemas.datacontract.org/2004/07/MultipayOnline"
xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<WrapClienti>
<CODRETE>0018</CODRETE>
<CODICE>20685</CODICE>
<NOME>A.T.E.R. Azienda Territoriale</NOME>
<INDIRIZZO>PIAZZA POZZA</INDIRIZZO>
<CITTA>Verona</CITTA>
<CAP>37123</CAP>
<PROV>VR</PROV>
<CODICEFISCALE>00223640236</CODICEFISCALE>
<PIVA>223640236</PIVA>
<EMAIL/>
<ESPOSIZ_CONTABILE>937,02</ESPOSIZ_CONTABILE>
<STATO>FALSE</STATO>
</WrapClienti>
</ArrayOfWrapClienti>
</dati>
<retista>3303903</retista>
<hashedString>oklkokokokok</hashedString>
</SetClientiXML>
the wcf read well "retista" and "hashedString", but "dati" is empty (0 elements), while I expect it has got the "wrapClienti" object I sent.
This is the prototype of my API:
[OperationContract]
[WebInvoke(UriTemplate = "SetClienti.xml", Method = "POST", BodyStyle=WebMessageBodyStyle.Wrapped, RequestFormat = WebMessageFormat.Xml)]
GenericResponse SetClientiXML(List<WrapClienti> dati, string retista, string hashedString);
So, the problem is that the List is empty.. why? How can I write the xml to make readable the list?
Ask me if I can give to you more details.
UPDATE: More weird!!
With this xml:
<?xml version="1.0" encoding="utf-8" ?><SetClientiXML xmlns="http://tempuri.org/">
<dati>
<WrapClienti xmlns="http://schemas.datacontract.org/2004/07/MultipayOnline" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<CODRETE>0018</CODRETE>
<CODICE>20685</CODICE>
<NOME>A.T.E.R. Azienda Territoriale</NOME>
<INDIRIZZO>PIAZZA POZZA</INDIRIZZO>
<CITTA>Verona</CITTA>
<CAP>37123</CAP>
<PROV>VR</PROV>
<CODICEFISCALE>00223640236</CODICEFISCALE>
<PIVA>223640236</PIVA>
<EMAIL/>
<ESPOSIZ_CONTABILE>937,02</ESPOSIZ_CONTABILE>
<STATO>FALSE</STATO>
</WrapClienti>
</dati>
<retista>3303903</retista>
<hashedString>oklkokokokok</hashedString>
</SetClientiXML>
the wcf read some attributes of the List, and other.. are nul!!!
I my WrapClienti I have a lof of attributes. Two of them are:
private string p_CAP { get; set; }
public string CAP
{
get
{
if (model == null)
return p_CAP.ToSafeString();
else
return this.model.CAP.ToSafeString();
}
set { p_CAP = value; }
}
private string p_PROV { get; set; }
public string PROV
{
get
{
if (model == null)
return p_PROV.ToSafeString();
else
return this.model.PROV.ToSafeString();
}
set { p_PROV = value; }
}
the problem is, with the xml above and with two breakpoint on the two set methods, only the set of PROV is called and, the one of CAP, not!!! Why? Now I'm really getting crazy... why this behavior??
Solution here.
This has to do with the ordering of the fields in your XML. It sounds very strange, but WCF DataContractSerializer is really fussy about the order in which the fields are encountered in the XML, but even worse, also in comparison to how they are defined in the code.
You see, the serializer wants the fields to be defined in alphabetical order, and if you serialize an instance of your class, you will find that the resulting XML fields are in alphabetical order. However, on deserialization, the serializer finds that the type you want to deserialize to has the fields defined in the "wrong" order. In this situation the behavior can seem random, but I think it has something to do with the fact that CAP should be the first field encountered, whereas PROV should be the last field, alphabetically.
So you have two options:
Reorder your XML and the fields in your class to be in alphabetical order, or
Decorate your class members with the DataMemeber property, and define the order of serialization.
You can do 2 like this:
[DataContract]
public class WrapClienti
{
[DataMember(Order=1)]
public string CAP { get; set; }
[DataMember(Order=2)]
public string PROV { get; set; }
...etc
}
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 problem which I have been bashing my head against for the better part of three hours. I am almost certain that I've missed something blindingly obvious...
I have a simple XML file:
<?xml version="1.0" encoding="utf-8"?>
<WeightStore xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Records>
<Record actual="150" date="2010-05-01T00:00:00" />
<Record actual="155" date="2010-05-02T00:00:00" />
</Records>
</WeightStore>
I have a simple class structure:
[Serializable]
public class Record
{
[XmlAttribute("actual")] public double weight { get; set; }
[XmlAttribute("date")] public DateTime date { get; set; }
[XmlIgnore] public double trend { get; set; }
}
[Serializable]
[XmlRoot("WeightStore")]
public class SimpleWeightStore
{
[XmlArrayAttribute("Records")]
private List<Record> records = new List<Record>();
public List<Record> Records { get { return records; } }
[OnDeserialized()]
public void OnDeserialized_Method(StreamingContext context)
{
// This code never gets called
Console.WriteLine("OnDeserialized");
}
}
I am using these in both calling code and in the class files:
using System.Xml.Serialization;
using System.Runtime.Serialization;
I have some calling code:
SimpleWeightStore weight_store_reload = new SimpleWeightStore();
TextReader reader = new StringReader(xml);
XmlSerializer deserializer = new XmlSerializer(weight_store.GetType());
weight_store_reload = (SimpleWeightStore)deserializer.Deserialize(reader);
The problem is that I am expecting OnDeserialized_Method to get called, and it isn't.
I suspect it might have something to do with the fact that it's XML deserialization rather than Runtime deserialization, and perhaps I am using the wrong attribute name, but I can't find out what it might be.
Any ideas, folks?
There's no equivalent of OnDeserialized for XML deserialization.
See this post for workarounds: How do you find out when you've been loaded via XML Serialization?
The only way you could do that in a graceful way is to manually implement IXmlSerializable, which is not fun. Simply; XmlSerializer doesn't support serialization callbacks.
Sometimes, though, you can switch to DataContractSerializer, which still offers xml capabilities but which does support serialization callbacks. Unfortunately the xml options are limited - it won't work for you xml structure, since that uses attributes (DataContractSerializer only supports elements).
You might also look at the comments on this answer, which discusses the points from this.