XML Does not work for me in WebApi -Something to do with my Datacontracts - asp.net-web-api

I am making a web api project and I noticed that one of my method dies when I try to use xml. I usually just use json when testing as this is my preferred method and it works fine.
public HttpResponseMessage GetStoreNames(StoreDm vm)
{
if (ModelState.IsValid)
{
var result = storeService.AutoCompleteStore(vm.Latitude, vm.Longitude, vm.Name);
return Request.CreateResponse <ResponseResult<AutoCompleteSearchDto>>(result.Status.Code, result);
}
var responseResult = new ResponseResultWrapper();
responseResult.Status.Code = HttpStatusCode.BadRequest;
responseResult.Status.Message = GenericErrors.InvalidRequest;
responseResult.ModelStateToResponseResult(ModelState);
return Request.CreateResponse<ResponseResult>(responseResult.Status.Code, responseResult);
}
[DataContract(Name = "MyRoot")]
public class ResponseResultWrapper : ResponseResult
{
public void ModelStateToResponseResult(ModelStateDictionary modelState)
{
foreach (var kvp in modelState)
{
foreach (var error in kvp.Value.Errors)
{
string key = kvp.Key;
string msg = error.ErrorMessage;
if (String.IsNullOrEmpty(msg))
{
msg = error.Exception.Message;
}
base.AddError(key, msg);
}
}
}
}
[DataContract(Name = "MyRoot")]
public class ResponseResult
{
public ResponseResult()
{
Errors = new Dictionary<string, string>();
Status = new ResponseBase();
}
public void AddError(string key, string errorMessage)
{
if (!Errors.ContainsKey(key))
{
Errors.Add(key, errorMessage);
}
}
public bool IsValid()
{
if (Errors.Count > 0)
{
return false;
}
return true;
}
[DataMember]
public Dictionary<string, string> Errors { get; private set; }
[DataMember]
public ResponseBase Status { get; set; }
}
[DataContract(Name = "MyRoot")]
public class ResponseResult<T> : ResponseResult
{
[DataMember]
public T Response { get; set; }
}
I get this error
HTTP/1.1 500 Internal Server Error
Cache-Control: private
Content-Type: application/xml; charset=utf-8
Server: Microsoft-IIS/8.0
X-AspNet-Version: 4.0.30319
X-SourceFiles: =?UTF-8?B?QzpcVXNlcnNcV2luZG93czdcRGVza3RvcFxQcmljZUNoZWNrXHRydW5rXFByaWNlQ2hlY2tcUHJpY2VDaGVjay5BcGlcYXBpXHN0b3JlXEdldFN0b3JlTmFtZXM=?=
X-Powered-By: ASP.NET
Date: Thu, 23 May 2013 16:44:14 GMT
Content-Length: 2195
<Error><Message>An error has occurred.</Message><ExceptionMessage>The 'ObjectContent`1' type failed to serialize the response body for content type 'application/xml; charset=utf-8'.</ExceptionMessage><ExceptionType>System.InvalidOperationException</ExceptionType><StackTrace /><InnerException><Message>An error has occurred.</Message><ExceptionMessage>Type 'ResponseResultWrapper' with data contract name 'http://schemas.datacontract.org/2004/07/Api.Models' is not expected. Consider using a DataContractResolver or add any types not known statically to the list of known types - for example, by using the KnownTypeAttribute attribute or by adding them to the list of known types passed to DataContractSerializer.</ExceptionMessage><ExceptionType>System.Runtime.Serialization.SerializationException</ExceptionType><StackTrace> at System.Runtime.Serialization.XmlObjectSerializerWriteContext.SerializeAndVerifyType(DataContract dataContract, XmlWriterDelegator xmlWriter, Object obj, Boolean verifyKnownType, RuntimeTypeHandle declaredTypeHandle, Type declaredType)
at System.Runtime.Serialization.XmlObjectSerializerWriteContext.SerializeWithXsiTypeAtTopLevel(DataContract dataContract, XmlWriterDelegator xmlWriter, Object obj, RuntimeTypeHandle originalDeclaredTypeHandle, Type graphType)
at System.Runtime.Serialization.DataContractSerializer.InternalWriteObjectContent(XmlWriterDelegator writer, Object graph, DataContractResolver dataContractResolver)
at System.Runtime.Serialization.DataContractSerializer.InternalWriteObject(XmlWriterDelegator writer, Object graph, DataContractResolver dataContractResolver)
at System.Runtime.Serialization.XmlObjectSerializer.WriteObjectHandleExceptions(XmlWriterDelegator writer, Object graph, DataContractResolver dataContractResolver)
at System.Runtime.Serialization.DataContractSerializer.WriteObject(XmlWriter writer, Object graph)
at System.Net.Http.Formatting.XmlMediaTypeFormatter.<>c__DisplayClass7.<WriteToStreamAsync>b__6()
at System.Threading.Tasks.TaskHelpers.RunSynchronously(Action action, CancellationToken token)</StackTrace></InnerException></Error>
Edit
[DataContract]
public class StoreDm
{
[DataMember(IsRequired = true)]
[MinLength(3)]
public string Name { get; set; }
[DataMember(IsRequired = true)]
public double Latitude { get; set; }
[DataMember(IsRequired = true)]
public double Longitude { get; set; }
}
I get this error after trying "Youssef Moussaoui" first part of his answer.
There was an error deserializing the object of type StoreDm. The data at the root level is invalid. Line 1, position 1.
I can't really do his second part as "ResponseResultWrapper" and "ResponeResult" are in 2 different libraries.

Try changing:
return Request.CreateResponse<ResponseResult>(responseResult.Status.Code, responseResult);
to
return Request.CreateResponse<ResponseResultWrapper>(responseResult.Status.Code, responseResult);
The problem is that DataContractSerializer needs known types to support inheritance. You can learn more about that here:
http://blogs.msdn.com/b/youssefm/archive/2009/04/21/understanding-known-types.aspx
Changing the generic parameter on CreateResponse is the simplest solution, but if you want to serialize derived instances of ResponseResult, you'd want to add known type attributes to ResponseResult like this:
[DataContract(Name = "MyRoot")]
[KnownType(typeof(ResponseResultWrapper))]
public class ResponseResult
{
}
.

Related

Cannot deserialize the current JSON object into type ObservableCollection because the type requires a JSON array

I'm trying to display a search result API in a ListView.
This is the model which I want to display in the ListView:
public class Video
{
[Key]
public int VideoId { get; set; }
public string Exercice { get; set; }
public string Titre { get; set; }
public int Sexe{ get; set; }
public int Categorie { get; set; }
public int Level { get; set; }
public string FilePath { get; set; }
public DateTime DateUpload { get; set; } = DateTime.Now;
[ForeignKey("Machine")]
public int Machine_Qr { get; set; }
}
This is the get method:
public async Task<ObservableCollection<Video>> search(string qr)
{
string url = Base_url + "machines/" +qr;
try
{
HttpClient client = new HttpClient();
HttpResponseMessage responseMessage = await client.GetAsync(url);
var result = await responseMessage.Content.ReadAsStringAsync();
var json = JsonConvert.DeserializeObject<ObservableCollection<Video>>(result);
return json;
}
catch(Exception ex)
{
Console.WriteLine(ex.Message);
}
return null;
}
the result json value:
[
{
"videoId": 7,
"exercice": "string",
"titre": "string",
"sexe": 0,
"categorie": 0,
"level": 0,
"filePath": "string",
"dateUpload": "2022-07-07T13:13:39.725",
"machine_Qr": 7895,
"machine": null
}
]
This is the View.cs
public partial class SearchResult : INotifyPropertyChanged
{
IMachineService _rest = DependencyService.Get<IMachineService>();
public SearchResult(string value)
{
InitializeComponent();
qr.Text = value;
GetVideos();
}
public async void GetVideos()
{
string value = qr.Text;
var result = await _rest.search(value);
if (result != null)
{
Videos = result;
}
}
public ObservableCollection<Video> videos;
public ObservableCollection<Video> Videos
{
get { return videos; }
set
{
videos = value;
}
}
but I got an exception while debugging this line in the service
HttpResponseMessage responseMessage = await client.GetAsync(url);
and this is the detailed exception:
Cannot deserialize the current JSON object (e.g. {"name":"value"}) into type 'System.Collections.ObjectModel.ObservableCollection`1[App5.Models.Video]' because the type requires a JSON array (e.g. [1,2,3]) to deserialize correctly.
To fix this error either change the JSON to a JSON array (e.g. [1,2,3]) or change the deserialized type so that it is a normal .NET type (e.g. not a primitive type like integer, not a collection type like an array or List) that can be deserialized from a JSON object. JsonObjectAttribute can also be added to the type to force it to deserialize from a JSON object.
Path 'type', line 1, position 8.
Do this as a test:
public ObservableCollection<Video> search(string qr)
{
try
{
var result = "[ { \"videoId\": 7, \"exercice\": \"string\", \"titre\": \"string\","
+ " \"sexe\": 0, \"categorie\": 0, \"level\": 0, \"filePath\": \"string\","
+ " \"dateUpload\": \"2022-07-07T13:13:39.725\", \"machine_Qr\": 7895, \"machine\": null } ]";
var json = JsonConvert.DeserializeObject<ObservableCollection<Video>>(result);
return json;
}
catch(Exception ex)
{
Console.WriteLine(ex.Message);
}
return null;
}
Put breakpoint on both return statements. To make sure this really is the code that is giving the error. Which breakpoint does it hit now? Does it print the same exception message?

.NET5 Web Api how to allow Guid in any format

I have a Dto that looks like this:
public class CreateObjectDto
{
public Guid SomeGuid { get; set; }
}
problem I'm having is that default Guid converter does not allow values outside "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx" format, so users are required to enter Guid with dashes which is not desirable. I would like an option to parse any regular Guid whether it has dashes or not.
Error if I call api with different Guid format is:
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"traceId": "00-1714eba1650b1548afd8581204d38a0c-ffc921fac3022540-00",
"errors": {
"$.attachmentList[0].documentId": [
"The JSON value could not be converted to System.Guid. Path: $.attachmentList[0].documentId | LineNumber: 0 | BytePositionInLine: 292."
]
}
}
EDIT with solution
(modified accepted solution a bit):
CustomGuidConverter:
public class CustomGuidConverter : JsonConverter<Guid>
{
public override Guid Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (!Guid.TryParse(reader.GetString(), out var parsedGuid))
{
throw new Exception($"Unable to parse {reader.GetString()} to GUID");
}
return parsedGuid;
}
public override void Write(Utf8JsonWriter writer, Guid value, JsonSerializerOptions options)
=> writer.WriteStringValue(value.ToString("D"));
}
startup.cs:
// add this line to apply conversion globally and not only for one property
services.AddMvc().AddJsonOptions(opts =>
{
opts.JsonSerializerOptions.Converters.Add(new CustomGuidConverter());
});
You have to use a custom JsonConverter https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-converters-how-to?pivots=dotnet-6-0 for the given type (Guid)
public class CustomGuidJsonConverter : JsonConverter<Guid>
{
private Regex _uwCharsRegex = new Regex("[ \t-]");
private Regex _validityRegex = new Regex("[a-f0-9]{32}");
public override Guid Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
string value = _uwCharsRegex.Replace(reader.GetString(), "").ToLower();
// Check validity
if (!_validityRegex.IsMatch(value))
{
return Guid.Empty; // or throw exception
}
return new Guid(value);
}
public override void Write(Utf8JsonWriter writer, Guid guidValue, JsonSerializerOptions options)
=> writer.WriteStringValue(guidValue.ToString());
}
Then you can use it by annoting the wanted Guid property.
public class CreateObjectDto
{
[JsonConverter(typeof(CustomGuidJsonConverter))]
public Guid SomeGuid { get; set; }
}

Access custom attributes of .NET class inside custom json converter

In my project, I have written a custom json converter to trim the white-spaces present in the string property.
Here is an example of the typical class we will use,
public class Candidate
{
public string CandidateName { get; set; }
}
Here is my custom json converter
public class StringSanitizingConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return objectType == typeof(string);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue , JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.String)
if (reader.Value != null)
{
string sanitizedString = (reader.Value as string).Trim();
if (StringSanitizeOptions.HasFlag(StringSanitizeOptions.ToLowerCase))
sanitizedString = sanitizedString.ToLowerInvariant();
return sanitizedString;
}
return reader.Value;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var text = (string)value;
if (text == null)
writer.WriteNull();
else
writer.WriteValue(text.Trim());
}
}
With my custom converter I am now able to format the string by trimming any white-spaces present sent to the action methods using my 'Candidate' as one of its parameter.
public void Post(ComplexType complexTypeParameter){
}
Everything worked well so far. I later wanted to enhance this json converter to format the string properties based on the attributes set to the string property in the Candidate class. for example, assume I have written my candidate class like this,
public class Candidate
{
[StringSanitizingOptions(Option.ToLowerCase)]
public string CandidateName { get; set; }
}
And if I wanted to format the string properties of a class based on the custom attribute configuration inside the json converter , I am not able to access this custom attribute and its configuration inside the ReadJson method of the custom converter.
Here is what I have tried so far but with no luck,
Not present in the CustomAttributes property of the objectType
parameter sent to the ReadJson() method.
Was trying to see if I could extract the parent class of the property inside the ReadJson() method, so that I could apply reflection on the class to extract the custom attributes given to any of its property,but I could not extract that too.
The stack of containing object(s) is not made available to JsonConverter.ReadJson(), thus you cannot do what you want inside ReadJson().
Instead, what you can do is to create a custom contract resolver that applies an appropriately configured instance of StringSanitizingConverter based on the properties of the object for which a contract is being generated.
First, let's say your data model, attribute, and JsonConverter look like the following (where I had to modify a few things to make your code compile and include some additional test cases):
public class Candidate
{
[StringSanitizingOptions(Option.ToLowerCase)]
public string CandidateName { get; set; }
[StringSanitizingOptions(Option.DoNotTrim)]
public string StringLiteral { get; set; }
public string DefaultString { get; set; }
public List<string> DefaultStrings { get; set; }
}
[System.AttributeUsage(System.AttributeTargets.Property | System.AttributeTargets.Field | System.AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)]
public class StringSanitizingOptionsAttribute : System.Attribute
{
public Option StringSanitizeOptions { get; set; }
public StringSanitizingOptionsAttribute(Option stringSanitizeOptions)
{
this.StringSanitizeOptions = stringSanitizeOptions;
}
}
[Flags]
public enum Option
{
Default = 0,
ToLowerCase = (1<<0),
DoNotTrim = (1<<1),
}
public static class StringSanitizeOptionsExtensions
{
public static bool HasFlag(this Option options, Option flag)
{
return (options & flag) == flag;
}
}
public class StringSanitizingConverter : JsonConverter
{
readonly Option options;
public StringSanitizingConverter() : this(Option.Default) { }
public StringSanitizingConverter(Option options)
{
this.options = options;
}
public override bool CanConvert(Type objectType)
{
return objectType == typeof(string);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.String)
if (reader.Value != null)
{
var sanitizedString = (reader.Value as string);
if (!options.HasFlag(Option.DoNotTrim))
sanitizedString = sanitizedString.Trim();
if (options.HasFlag(Option.ToLowerCase))
sanitizedString = sanitizedString.ToLowerInvariant();
return sanitizedString;
}
return reader.Value;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
// WriteJson is never called with null
var text = (string)value;
if (!options.HasFlag(Option.DoNotTrim))
text = text.Trim();
writer.WriteValue(text);
}
}
Next, grab ConfigurableContractResolver from How to add metadata to describe which properties are dates in JSON.Net, and define the extension method JsonContractExtensions.AddStringConverters():
public static class JsonContractExtensions
{
public static JsonContract AddStringConverters(this JsonContract contract)
{
if (contract is JsonPrimitiveContract)
{
if (contract.UnderlyingType == typeof(string))
contract.Converter = new StringSanitizingConverter();
}
else if (contract is JsonObjectContract)
{
var objectContract = (JsonObjectContract)contract;
foreach (var property in objectContract.Properties)
{
if (property.PropertyType == typeof(string))
{
var attr = property.AttributeProvider.GetAttributes(typeof(StringSanitizingOptionsAttribute), true)
.Cast<StringSanitizingOptionsAttribute>()
.SingleOrDefault();
if (attr != null)
{
property.Converter = property.MemberConverter = new StringSanitizingConverter(attr.StringSanitizeOptions);
}
}
}
}
return contract;
}
}
public class ConfigurableContractResolver : DefaultContractResolver
{
// This contract resolver taken from the answer to
// https://stackoverflow.com/questions/46047308/how-to-add-metadata-to-describe-which-properties-are-dates-in-json-net
// https://stackoverflow.com/a/46083201/3744182
readonly object contractCreatedPadlock = new object();
event EventHandler<ContractCreatedEventArgs> contractCreated;
int contractCount = 0;
void OnContractCreated(JsonContract contract, Type objectType)
{
EventHandler<ContractCreatedEventArgs> created;
lock (contractCreatedPadlock)
{
contractCount++;
created = contractCreated;
}
if (created != null)
{
created(this, new ContractCreatedEventArgs(contract, objectType));
}
}
public event EventHandler<ContractCreatedEventArgs> ContractCreated
{
add
{
lock (contractCreatedPadlock)
{
if (contractCount > 0)
{
throw new InvalidOperationException("ContractCreated events cannot be added after the first contract is generated.");
}
contractCreated += value;
}
}
remove
{
lock (contractCreatedPadlock)
{
if (contractCount > 0)
{
throw new InvalidOperationException("ContractCreated events cannot be removed after the first contract is generated.");
}
contractCreated -= value;
}
}
}
protected override JsonContract CreateContract(Type objectType)
{
var contract = base.CreateContract(objectType);
OnContractCreated(contract, objectType);
return contract;
}
}
public class ContractCreatedEventArgs : EventArgs
{
public JsonContract Contract { get; private set; }
public Type ObjectType { get; private set; }
public ContractCreatedEventArgs(JsonContract contract, Type objectType)
{
this.Contract = contract;
this.ObjectType = objectType;
}
}
public static class ConfigurableContractResolverExtensions
{
public static ConfigurableContractResolver Configure(this ConfigurableContractResolver resolver, EventHandler<ContractCreatedEventArgs> handler)
{
if (resolver == null || handler == null)
throw new ArgumentNullException();
resolver.ContractCreated += handler;
return resolver;
}
}
Then, finally you can deserialize and serialize Candidate as follows:
var settings = new JsonSerializerSettings
{
ContractResolver = new ConfigurableContractResolver
{
}.Configure((s, e) => { e.Contract.AddStringConverters(); }),
};
var candidate = JsonConvert.DeserializeObject<Candidate>(json, settings);
var json2 = JsonConvert.SerializeObject(candidate, Formatting.Indented, settings);
Notes:
I don't know why the stack of containing object(s) is not available in ReadJson(). Possibilities include:
Simplicity.
A JSON object is "an unordered set of name/value pairs", so trying to access the containing .Net object while reading a property value isn't guaranteed to work, since the information required might not have been read in yet (and the parent might not even have been constructed).
Because a default instance of StringSanitizingConverter is applied to the contract generated for string itself, it is not necessary to add the converter to JsonSerializer.SettingsConverters. This in turn may lead to a small performance enhancement as CanConvert will no longer get called.
JsonProperty.MemberConverter was recently marked obsolete in Json.NET 11.0.1 but must be set to the same value as JsonProperty.Converter in previous versions of Json.NET. If you are using 11.0.1 or a more recent version you should be able to remove the setting.
You may want to cache the contract resolver for best performance.
To modify JsonSerializerSettings in asp.net-web-api, see JsonSerializerSettings and Asp.Net Core, Web API: Configure JSON serializer settings on action or controller level, How to set custom JsonSerializerSettings for Json.NET in MVC 4 Web API? or ASP.NET Core API JSON serializersettings per request, depending on your requirements and the version of the framework in use.
Sample working .Net fiddle here.

Custom error response in Web Api

On an Web API controller I have the following:
if (!ModelState.IsValid)
return BadRequest();
BadRequest: https://msdn.microsoft.com/en-us/library/system.web.http.apicontroller_methods%28v=vs.118%29.aspx
I would like to create a custom error message to pass errors as json.
Something like:
if (!ModelState.IsValid)
return ModelHasErrors(errors);
How can I create a custom error response?
The simplest way is
return Request.CreateErrorResponse(HttpStatusCode.NotFound, ModelState);
I am used to use special object, that represents error response. I can add my specific (translated) message, status code, etc.
public class ErrorModel
{
public ErrorModel(HttpStatusCode statusCode, string message)
{
StatusCode = (int)statusCode;
Message = message;
ValidationErrors = new Dictionary<string, ModelErrorCollection>();
}
public ErrorModel(HttpStatusCode statusCode)
{
StatusCode = (int)statusCode;
ValidationErrors = new Dictionary<string, ModelErrorCollection>();
}
public string Message { get; set; }
public int StatusCode { get; set; }
public Dictionary<string, ModelErrorCollection> ValidationErrors { get; set; }
public Exception Exception { get; set; }
}
Then I have extension for CreateCustomResponse
public static class ApiExtensions
{
public static HttpResponseMessage CreateCustomResponse(this HttpRequestMessage request, HttpStatusCode statusCode, string errorMessage)
{
var errorMessageModel = new ErrorModel(statusCode, errorMessage);
return request.CreateResponse(statusCode, errorMessageModel);
}
public static HttpResponseMessage CreateCustomResponse(this HttpRequestMessage request, HttpStatusCode statusCode, Exception exception, string errorMessage = "")
{
if (string.IsNullOrEmpty(errorMessage) && exception != null)
{
errorMessage = exception.Message;
}
var errorMessageModel = new ErrorModel(statusCode, errorMessage)
{
Exception = exception
};
return request.CreateResponse(statusCode, errorMessageModel);
}
public static HttpResponseMessage CreateCustomResponse(this HttpRequestMessage request,
HttpStatusCode statusCode, ModelStateDictionary modelState, string errorMessage = "")
{
if (string.IsNullOrEmpty(errorMessage))
{
errorMessage = ApiValidationMessages.GeneralModelIsNotValid;
}
var errorMessageModel = new ErrorModel(statusCode, errorMessage);
foreach (var error in modelState.Where(x => x.Value.Errors != null && x.Value.Errors.Any()))
{
errorMessageModel.ValidationErrors.Add(error.Key.Replace("model.", ""), error.Value.Errors);
}
return request.CreateResponse(statusCode, errorMessageModel);
}
}
And finally in my controllers I just call:
return Request.CreateCustomResponse(HttpStatusCode.NotFound, ApiHttpResultMessages.NotFound);
You can find inspiration in my CodePlex project Web API Design: https://webapidesign.codeplex.com/
You can return directly whichever object you want, and it will be serialized as JSON. It can even be an anonymous class object created with new { }
On The client side you have to check if you've received a regurlar response, or the error object, which can be easyly donde by checking the existence of some property of your custom error object.
You could return a HttpResponseMessage using an object containing your error messages (in this example errors) as content:
return new HttpResponseMessage(HttpStatusCode.BadRequest)
{
Content = new ObjectContent(typeof(ErrorClass), errors, new JsonMediaTypeFormatter())
};
More information about how to return action results can be found here.
You can obviously reuse the above code by creating a method and call it like in your example: ModelHasErrors(errors).
However, if you often find yourself returning the same response, a nice option would be to create an custom exception filter which would return the same response whenever ModelState.IsValid is false.

ASP.NET WebApi with Protocol Buffers - Error handling

Context:
What i have now:
3-tiers app
Client-Server communication
Server: ASP.NET WebApi v1
Client: HttpClient
Serialization - JSON.NET
However,
JSON.NET is slow
JSON.NET is even slower on the first call (i take it this is because of serializer assembly generation on the fly). This is too slow for me - according the requirements i need to optimize the first call as much as possible.
I am considering using protobuf-net instead of JSON.NET. On a simple PoC app it has shown more than twice as fast result, even the first call, especially when i'd pre-generated the assembly for protocol buffers serializer.
So i've implemented MediaTypeFormatter using protobuf-net and everything works well except one thing - serializing errors.
This is how the exceptions are passed to the client:
public class ExceptionShielderAttribute : ExceptionFilterAttribute
{
public override void OnException(HttpActionExecutedContext context)
{
context.Response = context.Request.CreateErrorResponse(HttpStatusCode.InternalServerError, context.Exception);
}
}
Internally, CreateErrorResponse method creates an instance of HttpError (which inherits from Dictionary[string, object]) and writes it to the content.
By default, protobuf-net knows nothing about HttpError so I've tried to add HttpError to protobuf runtime model as follows
typeModel.Add(typeof (HttpError), true);
but it did not help, when i call
typeModel.Compile("ModelSerializer", "ModelSerializer.dll")
it throws InvalidOperationException: No serializer defined for type: System.Object.
Likely due to type of Dictionary[string, object] which is not supported by protobuf-net.
Questions:
Is there anything I can do to serialize errors properly or should i avoid using out-of-the-box errorhanding and implement my own error handling on the server that uses well-know types that protobuf is aware of?
Is protobuf a good choice for my problem at all?
Here is a snippet that enable serialization of System.Web.Http.HttpError using protobuf-net
namespace WS
using System;
using System.Runtime.Serialization;
using System.Web.Http;
using WebApiContrib.Formatting;
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
config.Formatters.Add(new ProtoBufFormatter());
ProtoBufFormatter.Model.Add(typeof(HttpErrorProto), true);
var model = ProtoBufFormatter.Model.Add(typeof(HttpError), false);
model.IgnoreListHandling = true;
model.SetSurrogate(typeof(HttpErrorProto));
}
}
[DataContract]
public class HttpErrorProto
{
[DataMember(Order = 1)]
public String ExceptionMessage { get; set; }
[DataMember(Order = 2)]
public String ExceptionType { get; set; }
[DataMember(Order = 3)]
public String InnerException { get; set; }
[DataMember(Order = 4)]
public String MessageDetail { get; set; }
[DataMember(Order = 5)]
public String Message { get; set; }
[DataMember(Order = 6)]
public String ModelState { get; set; }
[DataMember(Order = 7)]
public String StackTrace { get; set; }
public static implicit operator HttpErrorProto(HttpError error)
{
return error == null ? null : new HttpErrorProto
{
ExceptionMessage = error.ContainsKey("ExceptionMessage") ? error["ExceptionMessage"] as string : null,
ExceptionType = error.ContainsKey("ExceptionType") ? error["ExceptionType"] as string : null,
InnerException = error.ContainsKey("InnerException") ? error["InnerException"] as string : null,
MessageDetail = error.ContainsKey("MessageDetail") ? error["MessageDetail"] as string : null,
Message = error.Message,
ModelState = error.ContainsKey("ModelState") ? error["ModelState"] as string : null,
StackTrace = error.ContainsKey("StackTrace") ? error["StackTrace"] as string : null
};
}
public static implicit operator HttpError(HttpErrorProto error)
{
return error == null ? null : new HttpError
{
Message = error.Message
...
};
}
}
}
Your best bet would be to use a surrogate; something like:
typeModel.Add(typeof(HttpError), false)
.SetSurrogate(typeof(MyError));
Where MyError is a custom type of yours which:
has a conversion operator (implicit or explicit) to/from HttpError
is suitable for use from protobuf-net
I've managed work around the problem by adding a special case to my protobuf media type formatter:
public override Task WriteToStreamAsync(Type type, object value, Stream stream, HttpContent content, TransportContext transportContext)
{
var completionSource = new TaskCompletionSource<object>();
try
{
if (type == typeof(HttpError))
{
value = CreateDto((HttpError) value);
}
model.Value.Serialize(stream, value);
completionSource.SetResult((object)null);
}
catch (Exception ex)
{
completionSource.SetException(ex);
}
return (Task)completionSource.Task;
}
private HttpErrorDto CreateDto(HttpError error)
{
if (error == null) return null;
return new HttpErrorDto
{
Message = error.GetValueOrDefault<string>("Message"),
ExceptionMessage = error.GetValueOrDefault<string>("ExceptionMessage"),
StackTrace = error.GetValueOrDefault<string>("StackTrace"),
ExceptionType = error.GetValueOrDefault<string>("ExceptionType"),
InnerException = CreateDto(error.GetValueOrDefault<HttpError>("InnerException"))
};
}

Resources