Deserialize Boolean from Soap using Servicestack - soap

I am issuing a soap request from SSRS to servicestack and no matter what I try, I can't get Servicestack to recognize anything as a boolean value and deserialize it.
[DataContract]
[Route("/Stuff")]
public class GetStuff : IReturn<GetStuffResponse>
{
[DataMember]
[ApiMember(Name = "Is Enabled",
DataType = "bool",
IsRequired = false)]
public bool? IsEnabled { get; set; }
}
The incoming soap request looks like:
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<GetStuff xmlns="http://www.myCompany.com/types">
<IsEnabled>true</IsEnabled>
</GetTradesGroupedByClient>
</soap:Body>
</soap:Envelope>
I have tried to make the soap request send "0" and "1" for true/false, and true with a capital "T", but Servicestack always deserializes it as 'null'.
Anyone have any suggestions?
Edit 1
Update on further strangeness. I replaced the bool with int in the hopes that would be less of a hassle, however this also didn't deserialize. So I added some fields to the request to see if all deserialization would fail:
[DataContract]
[Route("/Stuff")]
public class GetStuff : IReturn<GetStuffResponse>
{
[DataMember]
[ApiMember(Name = "Summary Date",
DataType = "DateTime",
IsRequired = false)]
public DateTime? SummaryDate { get; set; }
[DataMember]
[ApiMember(Name = "Summary End Date",
DataType = "DateTime",
IsRequired = false)]
public DateTime? SummaryEndDate { get; set; }
[DataMember]
[ApiMember(Name = "Symbol",
DataType = "string",
IsRequired = false)]
public string Symbol { get; set; }
[DataMember]
[ApiMember(Name = "Email",
DataType = "string",
IsRequired = false)]
public string Email { get; set; }
[DataMember]
[ApiMember(Name = "Is Enabled",
DataType = "int",
IsRequired = false)]
public int? IsEnabled { get; set; }
}
Here is the soap being sent to serviceStack:
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<GetStuff xmlns="http://www.myCompany.com/types">
<SummaryDate>2018-04-26</SummaryDate>
<SummaryEndDate>2018-04-26</SummaryEndDate>
<Symbol>TOU</Symbol>
<Email>Guy.Smiley#myCompany.net</Email>
<IsEnabled>1</IsEnabled>
</GetStuff>
</soap:Body>
</soap:Envelope>
Now here is where it gets weird, the two dates and the 'symbol' field deserialize correctly. The 'Email' field and the 'IsEnabled' fields fail and are null. Is there some way to trace the Deserializer in Serivcestack?

ServiceStack doesn't use it's own XML Serializer, it uses .NET's WCF Message class and .NET's Xml DataContract Serializer to serialize/deserialize SOAP requests.
WCF's SOAP is very strict/fragile, so you'll need to capture exactly what's sent to know what's the right SOAP/XML to send. Easiest way to do that is to use the Soap12ServiceClient to send your Request DTO and a packet sniffer like Fiddler to capture the raw HTTP Request body that's sent, e.g:
var client = new Soap12ServiceClient(baseUrl);
var request = new GetStuff {
SummaryDate = DateTime.Parse("2018-04-26"),
SummaryEndDate = DateTime.Parse("2018-04-26"),
Symbol = "TOU",
Email = "Guy.Smiley#myCompany.net",
IsEnabled = true,
};
var response = client.Send(request);
Which sends the SOAP Request:
<?xml version="1.0" encoding="UTF-8"?>
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://www.w3.org/2005/08/addressing">
<s:Header>
<a:Action s:mustUnderstand="1">GetStuff</a:Action>
<a:MessageID>urn:uuid:5cb1b87c-82ae-422b-9b6a-3044eeb90fe7</a:MessageID>
<a:ReplyTo>
<a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>
</a:ReplyTo>
<a:To s:mustUnderstand="1">http://test.servicestack.net/Soap12</a:To>
</s:Header>
<s:Body>
<GetStuff xmlns="http://schemas.servicestack.net/types" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<Email>Guy.Smiley#myCompany.net</Email>
<IsEnabled>true</IsEnabled>
<SummaryDate>2018-04-26T00:00:00</SummaryDate>
<SummaryEndDate>2018-04-26T00:00:00</SummaryEndDate>
<Symbol>TOU</Symbol>
</GetStuff>
</s:Body>
</s:Envelope>
Notice the order of properties is in Alphabetical order which is important in SOAP/XML.

Related

"JsonException: The JSON value could not be converted to NodaTime.Instant" NodaTime issue with ASP.NET Core 3.1 Razor Page web application

In ASP.NET Core 3.1 Razor Page pure front end web application I received the below error.
Installed the following packages:
<PackageReference Include="System.Text.Json" Version="4.7.2" />
<PackageReference Include="EnumExtensions.System.Text.Json" Version="1.0.0" />
<PackageReference Include="NodaTime" Version="3.0.1" />
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.0.0" />
Also set this in Startup:
services.AddRazorPages()
.AddJsonOptions(options =>
{
// options.JsonSerializerOptions.PropertyNamingPolicy = null;
// options.JsonSerializerOptions.PropertyNameCaseInsensitive = true;
// options.JsonSerializerOptions.DictionaryKeyPolicy = null;
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverterWithAttributeSupport(null, true, true, true, true));
//options.JsonSerializerOptions.IgnoreNullValues = true;
options.JsonSerializerOptions.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb);
options.JsonSerializerOptions.Converters.Add(NodaConverters.IntervalConverter);
options.JsonSerializerOptions.Converters.Add(NodaConverters.InstantConverter);
})
.AddRazorPagesOptions(options =>
{
options.Conventions.AddPageRoute("/Login", "");
});
JsonException: The JSON value could not be converted to NodaTime.Instant. Path: $.data[0].created_at | LineNumber: 0 | BytePositionInLine: 261.
System.Text.Json.ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(Type propertyType)
System.Text.Json.JsonPropertyInfoNotNullable<TClass, TDeclaredProperty, TRuntimeProperty, TConverter>.OnRead(ref ReadStack state, ref Utf8JsonReader reader)
System.Text.Json.JsonPropertyInfo.Read(JsonTokenType tokenType, ref ReadStack state, ref Utf8JsonReader reader)
System.Text.Json.JsonSerializer.ReadCore(JsonSerializerOptions options, ref Utf8JsonReader reader, ref ReadStack readStack)
System.Text.Json.JsonSerializer.ReadCore(Type returnType, JsonSerializerOptions options, ref Utf8JsonReader reader)
System.Text.Json.JsonSerializer.Deserialize(string json, Type returnType, JsonSerializerOptions options)
System.Text.Json.JsonSerializer.Deserialize(string json, JsonSerializerOptions options)
Here's the snippet of data it's trying to deserialize. If I switch from Instant to DateTimeOffset, it works "instantly" (pun intended :D)
{
"data": [
{
"created_at": "2020-08-09T22:10:26.274672Z",
"updated_at": "2020-08-13T02:22:02.640871Z",
}
],
"page": 1,
"size": 20,
"count": 1,
"total": 1,
"success": true,
"message": null
}
Note: this json data is a result of serialization of an object that does include CreatedAt & UpdatedAt properties of the type (NodaTime)Instant. I confirm it works nicely with an asp.net core 3.1 mvc api application.
Not sure why it's not working. (Perhaps, John Skeet can shed some light?)
After doing some research & ultimately remembering/realizing that JsonSerializerOptions cannot be set at global in the current version of System.Text.Json I was finally able to get it working as expected by building options right where I need them. Here's the code snippet in case anyone gets stuck like me in the future.
var jsonString = "{\"data\":[{\"id\":\"f606942c-4740-46a7-be6f-66ceb38c530b\",\"created_at\":\"2020-08-09T22:10:26.274672Z\",\"updated_at\":\"2020-08-13T02:22:02.640871Z\"}],\"page\":1,\"size\":20,\"count\":1,\"total\":1,\"success\":true,\"message\":null }";
JsonSerializerOptions options = new JsonSerializerOptions();
options.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb);
options.Converters.Add(new JsonStringEnumConverterWithAttributeSupport(null, true, true, true, true));
var response = JsonSerializer.Deserialize<Response>(jsonString, options);
public enum SampleType
{
TYPE_0,
TYPE_1
}
public class Sample
{
[JsonProperty("id")]
public string Id { get; set; } = System.Guid.NewGuid().ToString();
[JsonProperty("created_at")]
public Instant CreatedAt { get; set; }
[JsonProperty("updated_at")]
public Instant UpdateAt { get; set; }
[JsonProperty("type")]
public SampleType Type { get; set; }
}
public class Response
{
[JsonProperty("data")]
public IEnumerable<Sample> Data { get; set; }
[JsonProperty("page")]
public int Page { get; set; }
[JsonProperty("size")]
public int Size { get; set; }
[JsonProperty("count")]
public int Count { get; set; }
[JsonProperty("total")]
public int Total { get; set; }
[JsonProperty("success")]
public bool Success { get; set; }
[JsonProperty("message")]
public string Message { get; set; }
}

Optional #Pathvariable in REST controller spring 4

I'm writing a Rest Service (HTTP Get endpoint), where in the below uri does the following
http://localhost:8080/customers/{customer_id}
fetch the details for the customer_id passed in the uri
if the customer_id is not passed (http://localhost:8080/customers), fetch all the customers details.
Code:
#RequestMapping(method = RequestMethod.GET, value = "customers/{customer_id}")
public List<Customer> getCustomers(
#PathVariable(name = "customer_id", required = false) final String customerId) {
LOGGER.debug("customer_id {} received for getCustomers request", customerId);
}
However, with the above code, for the second scenario control is flowing to getCustomers().
Note: I'm using Java8 and spring-web 4.3.10 version
Highly appreciate any help on this.
Optional #PathVariable is used only if you want to map both GET /customers/{customer_id} and GET customers into single java method.
You cannot send request which will be sent to GET /customers/{customer_id} if you don't send customer_id.
So in your case it will be:
#RequestMapping(method = RequestMethod.GET, value = {"/customers", "customers/{customer_id}"})
public List<Customer> getCustomers(#PathVariable(name = "customer_id", required = false) final String customerId) {
LOGGER.debug("customer_id {} received for getCustomers request", customerId);
}
public abstract boolean required
Whether the path variable is required.
Defaults to true, leading to an exception being thrown if the path variable is missing in the incoming request. Switch this to false if you prefer a null or Java 8 java.util.Optional in this case. e.g. on a ModelAttribute method which serves for different requests.
You can use null or Optional from java8
This may help someone that is trying to use multiple optional path variables.
If you have more than one variable, you can always accept multiple paths.
For instance:
#GetMapping(value = {"customers/{customerId}&{startDate}&{endDate}",
"customers/{customerId}&{startDate}&",
"customers/{customerId}&&{endDate}",
"customers/{customerId}&&"
})
public Customer getCustomerUsingFilter(#PathVariable String customerId, #PathVariable Optional<Date> startDate, #PathVariable Optional<Date> endDate)
Then you would call this URL using all the path separators (in this case &)
Like GET /customers/1&& or
GET /customers/1&&2018-10-31T12:00:00.000+0000 or
GET /customers/1&2018-10-31T12:00:00.000+0000& or
GET /customers/1&2018-10-31T12:00:00.000+0000&2018-10-31T12:00:00.000+0000
You should create two end-point here to handle the individual request :
#GetMapping("/customers")
public List<Customer> getCustomers() {
LOGGER.debug("Fetching all customer");
}
#GetMapping("/customers/{id}")
public List<Customer> getCustomers(#PathVariable("id") String id) {
LOGGER.debug("Fetching customer by Id {} ",id);
}
#GetMapping is equivalent to #RequestMapping(method = RequestMethod.GET) and #GetMapping("/customers/{id}") is equivalent to #RequestMapping(method = RequestMethod.GET, value = "customers/{id}")
Better approach would be like this :
#RestController
#RequestMapping("/customers")
public class CustomerController {
#GetMapping
public List<Customer> getAllCustomers() {
LOGGER.debug("Fetching all customer");
}
#GetMapping("/{id}")
public Customer getCustomerById(#PathVariable("id") String id) {
LOGGER.debug("Fetching customer by Id {} ",id);
}

Web API controller parameter is null only on GET request

So I have a very simple controller that accepts a Search object
public class ProfileController : ApiController
{
[AcceptVerbs("GET", "POST")]
public async Task<ProfileDTO> GetProfile(Search profile)
{
//My code
}
}
The Search object only contains very simple, primitive data types
public class Search
{
public string Uuid { get; set; }
public string Email { get; set; }
public string Name { get; set; }
public bool Censored { get; set; }
}
For testing purposes I am using Swagger to call my API with sample data. For some reason if I make the same call to api/profile with the same data the profile argument will only contain data during the POST call. On a GET it is always null.
I only added the AcceptVerbs("POST") to demonstrate this issue, but by RESTful design I want the endpoint to only accept GET verbs
As of right now I'm just sending some boilerplate sample data such as
{
"Uuid": "string",
"Email": "string",
"Name": "string",
"Censored": true
}
I know this is probably a very simple issue, but why is the parameter always null only on a GET request?

XMLArray with different types but same element name and i:type attribute

I am trying to serialize some data I have into this XML format but not able to achive the same.
The Desired XML output is below:
<?xml version="1.0" encoding="utf-8"?>
<Root xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<Datas>
<Data xmlns="" i:type="DataA">
<Name>A1</Name>
<ADesc>Description for A</ADesc>
</Data>
<Data xmlns="" i:type="DataB">
<Name>B1</Name>
<BDesc>Description for b</BDesc>
</Data>
</Datas>
</Root>
The Classes I created for serialization are as follows:
public class Data
{
[XmlElement("Name")]
public string Name { get; set; }
}
public class DataA : Data
{
[XmlElement("ADesc")]
public string ADesc { get; set; }
}
public class DataB : Data
{
[XmlElement("BDesc")]
public string BDesc { get; set; }
}
[XmlRoot("Root")]
public class Root
{
[XmlArray("Datas")]
[XmlArrayItem(Type = typeof(Data))]
[XmlArrayItem(Type = typeof(DataA))]
[XmlArrayItem(Type = typeof(DataB))]
public List<Data> Datas { get; set; }
}
I use the below method for serializing:
internal static string Serialize(Root obj)
{
var ns = new XmlSerializerNamespaces();
ns.Add("i", "http://www.w3.org/2001/XMLSchema-instance");
XmlSerializer xmlSerializer = new XmlSerializer(typeof(Root));
using (StringWriter textWriter = new StringWriter())
{
xmlSerializer.Serialize(textWriter, obj, ns);
return textWriter.ToString();
}
}
But the output I get is this (which is not correct):
<?xml version="1.0" encoding="utf-8"?>
<Root xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<Datas>
<DataA>
<Name>A1</Name>
<ADesc>Description for A</ADesc>
</DataA>
<DataB>
<Name>B1</Name>
<BDesc>Description for b</BDesc>
</DataB>
</Datas>
</Root>
In order to generate the {http://www.w3.org/2001/XMLSchema-instance}type attribute using XmlSerializer, you need to attach [XmlInclude(typeof(XXX))] for all subclasses XXX of Data to a declared type somewhere in your object graph, i.e. on the Root class or the Data class itself:
//[XmlInclude(typeof(DataA))] /* Could also go here if you prefer. */
//[XmlInclude(typeof(DataB))] /* Could also go here if you prefer. */
public class Data
{
[XmlElement("Name")]
public string Name { get; set; }
}
public class DataA : Data
{
[XmlElement("ADesc")]
public string ADesc { get; set; }
}
public class DataB : Data
{
[XmlElement("BDesc")]
public string BDesc { get; set; }
}
[XmlRoot("Root")]
[XmlInclude(typeof(DataA))]
[XmlInclude(typeof(DataB))]
public class Root
{
[XmlArray("Datas")]
public List<Data> Datas { get; set; }
}
For more information, see Declaring Serialization Types in Troubleshooting Common Problems with the XmlSerializer and also Xsi:type Attribute Binding Support.

ServiceStack deserialization of JSON content in multipart/form-data request

I'm creating a RESTful service using ServiceStack that should consume a POST with multipart/form-data content. The content is in JSON format, but when I send the POST, the object is not deserialized correctly (all properties are null/default values). If I try just sending that object as a regular POST (without the multipart/form-data), it deserializes just fine.
I poked around in the ServiceStack code to try to figure out what was going on, and this is my current understanding:
HttpListenerRequestWrapper::LoadMultiPart() is loading the multipart request and saving the (non-file) parts to "FormData", which maps the name of the part to its contents. However, it appears the content-type (which is correctly written to the HttpMultiPart::Element as the individual sections are being parsed) is lost because it isn't stored in anywhere.
Some time later in control-flow, EndpointHandlerBase::DeserializeHttpRequest() calls KeyValueDataContractDeserializer.Instance.Parse() with the FormData and the type to deserialize to.
If this is the first time that kind of object is being deserialized, a StringMapTypeDeserializer is created for that type and cached to typeStringMapSerializerMap. For each property of the type, we call JsvReader.GetParseFn() to get a ParseStringDelegate to parse that deserialize that property.
The created/cached StringMapTypeDeserializer is then used to deserialize the object, using all the "ParseFn's" set earlier... which all treat the content as JSV format.
I confirmed that JsvReader.ParseFnCache has a bunch of types in it, while JsonReader.ParseFnCache is empty. Also, if I change my request to remove all quotes (i.e. turn it from JSON into JSV format), it deserializes correctly. The one weird thing is that one of the properties of my object is a Dictionary, and that deserializes correctly, even when it's in JSON format; I'm assuming this is just a fortunate coincidence (?!?).
Am I correct in my understanding of what's going on here? Is this a known limitation in ServiceStack? Bug? Is there anyway to work around it other than putting my object in a file and manually calling JsonSerializer.DeserializeFromStream()?
Thanks!
jps
Also, just incase it's useful, here's the relevant request and data-objects:
POST /api/Task HTTP/1.1
Accept: application/json
Content-Type: multipart/form-data; boundary=Boundary_1_1161035867_1375890821794
MIME-Version: 1.0
Host: localhost:12345
Content-Length: 385
--Boundary_1_1161035867_1375890821794
Content-Type: application/json
Content-Disposition: form-data; name="MyMap"
{"myfile.dat":"ImportantFile"}
--Boundary_1_1161035867_1375890821794
Content-Disposition: form-data; name="MyThing"
Content-Type: application/json
{"Id":123,"Name":"myteststring"}
--Boundary_1_1161035867_1375890821794
Content-Type: application/octet-stream
Content-Disposition: form-data; filename="myfile.dat"
mydatagoeshere...
--Boundary_1_1161035867_1375890821794--
.
public class TestObj
{
public long Id { get; set; }
public string Name { get; set; }
}
[Route("/Task", "POST")]
public class TaskRequest : AuthenticatedRequest, IReturn<TaskResponse>
{
public TestObj MyThing { get; set; }
public Dictionary<string, string> MyMap { get; set; }
}
Have you tried setting the properties of your request object using the 'ApiMember' attribute? In particular the 'ParameterType' properties.
/// <summary>
/// Create and upload a new video.
/// </summary>
[Api("Create and upload a new video.")]
[Route("/videos", "POST", Summary = #"Create and upload a new video.",
Notes = "Video file / attachment must be uploaded using POST and 'multipart/form-data' encoding.")]
public class CreateVideo : OperationBase<IVideo>
{
/// <summary>
/// Gets or sets the video title.
/// </summary>
[ApiMember(Name = "Title",
AllowMultiple = false,
DataType = "string",
Description = "Video title. Required, between 8 and 128 characters.",
IsRequired = true,
ParameterType = "form")]
public string Title { get; set; }
/// <summary>
/// Gets or sets the video description.
/// </summary>
[ApiMember(Name = "Description",
AllowMultiple = false,
DataType = "string",
Description = "Video description.",
ParameterType = "form")]
public string Description { get; set; }
/// <summary>
/// Gets or sets the publish date.
/// </summary>
/// <remarks>
/// If blank, the video will be published immediately.
/// </remarks>
[ApiMember(Name = "PublishDate",
AllowMultiple = false,
DataType = "date",
Description = "Publish date. If blank, the video will be published immediately.",
ParameterType = "form")]
public DateTime? PublishDate { get; set; }
}