다음을 통해 공유


Using JSON.NET with ASP.NET Web API

Json.Net is a popular framework for working with JSON. In particular, it has a bunch of features that are not supported by the DataContractJsonSerializer such as being much more flexible in what kind of types it can serialize and exactly how they should be serialized. The ASP.NET Web API supports an open-ended set of formatters that can read and write data to and from any media type you want to support. For example, if you want to support the vCard format which has the media type text/vcard (previously it was text/directory) media type then you can write a formatter for vCard and register it for the media type (or types) in question.

Note: JSON.NET is now an integral part of ASP.NET Web API so you can just us it out of the box. 

This sample shows how to hook in Json.Net as the default formatter replacing the built in DataContractJsonSerializer formatter (in the beta bits DataContractJsonSerializer is the default formatter). There are already a bunch of Json.Net formatters provided by the community that may well be more full-featured but this should allow you to get started.

Building the Formatter

The first thing we do is building the formatter. The key part of the formatter is to provide support for reading and writing content of a given media type. The sample formatter derives from the base MediaTypeFormatter class; we are working on a buffered media type formatter that will help working with lots of small reads and writes but for this sample the goal is to keep things simple. The formatter looks like this:  

    1: public class JsonNetFormatter : MediaTypeFormatter
    2: {
    3:     private JsonSerializerSettings _jsonSerializerSettings;
    4:  
    5:     public JsonNetFormatter(JsonSerializerSettings jsonSerializerSettings)
    6:     {
    7:         _jsonSerializerSettings = jsonSerializerSettings ?? new JsonSerializerSettings();
    8:  
    9:         // Fill out the mediatype and encoding we support
   10:         SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/json"));
   11:         Encoding = new UTF8Encoding(false, true);
   12:     }
   13:  
   14:     protected override bool CanReadType(Type type)
   15:     {
   16:         if (type == typeof(IKeyValueModel))
   17:         {
   18:             return false;
   19:         }
   20:  
   21:         return true;
   22:     }
   23:  
   24:     protected override bool CanWriteType(Type type)
   25:     {
   26:         return true;
   27:     }
   28:  
   29:     protected override Task<object> OnReadFromStreamAsync(Type type, Stream stream, HttpContentHeaders contentHeaders, FormatterContext formatterContext)
   30:     {
   31:         // Create a serializer
   32:         JsonSerializer serializer = JsonSerializer.Create(_jsonSerializerSettings);
   33:  
   34:         // Create task reading the content
   35:         return Task.Factory.StartNew(() =>
   36:         {
   37:             using (StreamReader streamReader = new StreamReader(stream, Encoding))
   38:             {
   39:                 using (JsonTextReader jsonTextReader = new JsonTextReader(streamReader))
   40:                 {
   41:                     return serializer.Deserialize(jsonTextReader, type);
   42:                 }
   43:             }
   44:         });
   45:     }
   46:  
   47:     protected override Task OnWriteToStreamAsync(Type type, object value, Stream stream, HttpContentHeaders contentHeaders, FormatterContext formatterContext, TransportContext transportContext)
   48:     {
   49:         // Create a serializer
   50:         JsonSerializer serializer = JsonSerializer.Create(_jsonSerializerSettings);
   51:  
   52:         // Create task writing the serialized content
   53:         return Task.Factory.StartNew(() =>
   54:         {
   55:             using (JsonTextWriter jsonTextWriter = new JsonTextWriter(new StreamWriter(stream, Encoding)) { CloseOutput = false })
   56:             {
   57:                 serializer.Serialize(jsonTextWriter, value);
   58:                 jsonTextWriter.Flush();
   59:             }
   60:         });
   61:     }
   62: }

Building a Sample ApiController

Next we need a controller to try things out. For illustrative purposes we create a type that would not serialize well with DataContractJsonSerializer but other than that this is a completely vanilla controller that knows nothing about serialization:

    1: public class HomeController : ApiController
    2: {
    3:     public HomeInfo Get()
    4:     {
    5:         return new HomeInfo();
    6:     }
    7: }
    8:  
    9: public class HomeInfo
   10: {
   11:     private readonly DateTime _created = DateTime.UtcNow;
   12:     private readonly Dictionary<int, string> _colorMap = new Dictionary<int, string>
   13:     {
   14:         { 1, "blue"},
   15:         { 2, "red" },
   16:         { 3, "green" },
   17:         { 4, "black" },
   18:         { 5, "white" },
   19:     };
   20:  
   21:     public DateTime Created { get { return _created; } }
   22:  
   23:     public IDictionary<int, string> ColorMap { get { return _colorMap; } }
   24: }

Hosting the Controller

Now that we have the controller we can host it in either ASP or as selfhost. In this case we selfhost the controller in a simple console application but it would work exactly the same if hosted in ASP.

The first part is to configure the selfhost server and injecting the JsonNetFormatter as the first formatter in the configuration so that it becomes the default formatter. We also configure Json.Net to serialize DateTime types using ISO 8601 format instead of the more esoteric "/Date(1240718400000)/” format. The part of the console app that configures and starts the server looks like this:

    1: // Set up server configuration
    2: HttpSelfHostConfiguration config = new HttpSelfHostConfiguration("https://localhost:8080");
    3: config.Routes.MapHttpRoute("Default", "{controller}", new { controller = "Home" });
    4:  
    5: // Create Json.Net formatter serializing DateTime using the ISO 8601 format
    6: JsonSerializerSettings serializerSettings = new JsonSerializerSettings();
    7: serializerSettings.Converters.Add(new IsoDateTimeConverter());
    8: config.Formatters[0] = new JsonNetFormatter(serializerSettings);
    9:  
   10: // Create server
   11: server = new HttpSelfHostServer(config);
   12:  
   13: // Start listening
   14: server.OpenAsync().Wait();

Note: In order to successfully start the selfhost server you have to run as admin (or configure http.sys with the appropriate URI prefix using netsh).

Trying it Out

Once the controller is running, we can access it using any HTTP client. In this case we use HttpClient to access it and print out the result to the console. If we put both the server configuration and the client in the same Main then we get something like this:

    1: class Program
    2: {
    3:     static void Main(string[] args)
    4:     {
    5:         HttpSelfHostServer server = null;
    6:         try
    7:         {
    8:             // Set up server configuration
    9:             HttpSelfHostConfiguration config = new HttpSelfHostConfiguration("https://localhost:8080");
   10:             config.Routes.MapHttpRoute("Default", "{controller}", new { controller = "Home" });
   11:  
   12:             // Create Json.Net formatter serializing DateTime using the ISO 8601 format
   13:             JsonSerializerSettings serializerSettings = new JsonSerializerSettings();
   14:             serializerSettings.Converters.Add(new IsoDateTimeConverter());
   15:             config.Formatters[0] = new JsonNetFormatter(serializerSettings);
   16:  
   17:             // Create server
   18:             server = new HttpSelfHostServer(config);
   19:  
   20:             // Start listening
   21:             server.OpenAsync().Wait();
   22:  
   23:             // Create HttpClient, do an HTTP GET on the controller, and show the output
   24:             HttpClient client = new HttpClient();
   25:             client.GetAsync("https://localhost:8080").ContinueWith(
   26:                 (requestTask) =>
   27:                 {
   28:                     // Get HTTP response from completed task.
   29:                     HttpResponseMessage response = requestTask.Result;
   30:  
   31:                     // Check that response was successful or throw exception
   32:                     response.EnsureSuccessStatusCode();
   33:  
   34:                     // Read response asynchronously as string and write out
   35:                     response.Content.ReadAsStringAsync().ContinueWith(
   36:                         (readTask) =>
   37:                         {
   38:                             Console.WriteLine(readTask.Result);
   39:                         });
   40:                 });
   41:  
   42:             Console.WriteLine("Hit ENTER to exit...");
   43:             Console.ReadLine();
   44:  
   45:         }
   46:         finally
   47:         {
   48:             if (server != null)
   49:             {
   50:                 // Stop listening
   51:                 server.CloseAsync().Wait();
   52:             }
   53:         }
   54:     }
   55: }

The resulting output written to the console is

{"Created":"2012-02-18T00:54:06.8447642Z","ColorMap":{"1":"blue","2":"red","3":"green","4":"black","5":"white"}}

Note the ISO date and the nice serialization of the dictionary!

Henrik

del.icio.us Tags: asp.net,webapi,mvc,rest,httpclient

Comments

  • Anonymous
    February 17, 2012
    Hey Henrik, I was wondering why there isn't any interfaces for the HttpClient? I'd like to inject it as a dependency in my classes and an interface would make possible for me to mock it using something like Moq. Thanks!

  • Anonymous
    February 19, 2012
    We don't have interfaces but you can actually just new up the HttpRequestMessage and HttpResponseMessage etc. types directly and use them in your tests without a lot of mocking.

  • Anonymous
    February 19, 2012
    Henrik, I am having trouble on the client side.  Can you create some examples of a client sending  CRUD calls using HttpClient? Thanks.

  • Anonymous
    February 19, 2012
    Terrence, Sure, what in particular are you looking for?

  • Anonymous
    February 19, 2012
    The comment has been removed

  • Anonymous
    February 20, 2012
    Well I am working on a data app for the windows store.  I need an example of submitting a poco to add, update and delete using the httpclient.   I know it may seem obvious, but I am a simpleton and nothing  is obvious to me with these new technologies.  Thanks or any help you can provide.

  • Anonymous
    February 20, 2012
    Hey Henrik, thanks for answering. I got some Glenn Block on twitter on how to deal with it. I've blogged about it so maybe it will help the next person that comes along. gbogea.com/.../web-api-testing-with-httpclient

  • Anonymous
    February 20, 2012
    Are there any examples out there posting files to the new Web Api with Form Data?  For example in MVC 3 you can have a model with properties and one property with the HttpPostedFileWrapper.  I haven't been able to get this to work with the ApiController

  • Anonymous
    February 21, 2012
    I would like to use my traditional services using ASP.NET MVC4 Web API (i.e. i would like to replace WCF Web API), can you please provide blog post on this. Thanks a million in advance.

  • Anonymous
    February 21, 2012
    This code doesn't work for me. It sometimes throws a HttpException and terminates IIS Express: OnWriteToStreamAsync => "The remote host closed the connection. The error code is 0x800704CD." A first chance exception of type 'System.Web.HttpException' occurred in System.Web.dll. But it seems to me I've found the solution in your JsonMediaTypeFormatter class: return TaskHelpers.RunSynchronously(...);

  • Anonymous
    February 21, 2012
    Unfortunately, I was wrong. This is some other problems. Here is the last three stack trace entries:   at System.Web.Hosting.IIS7WorkerRequest.RaiseCommunicationError(Int32 result, Boolean throwOnDisconnect)   at System.Web.Hosting.IIS7WorkerRequest.ExplicitFlush()   at System.Web.HttpResponse.Flush(Boolean finalFlush)

  • Anonymous
    February 21, 2012
    Could you provide some hints as to how I could develop a MediaTypeFormatter that passed control to the standard ASP.Net MVC rendering engine so that I could easily support text/html along side application/xml, application/json?

  • Anonymous
    February 23, 2012
    Unfortunately there was a bug in JsonNetFormatter.OnWriteToStreamAsync in the formatter when used on the client side causing the stream to be closed too early. The blog has been updated to contain a working version of OnWriteToStreamAsync. Sorry about that!

  • Anonymous
    February 24, 2012
    I have tried it but it is some other problem. JsonTextWriter's Flush/Serialize method also throws a HttpException. Moreover, it sometimes stops IIS Express: "A Task's exception(s) were not observed either by Waiting on the Task or accessing its Exception property. As a result, the unobserved exception was rethrown by the finalizer thread." (IIS7WorkerRequest - RaiseCommunicationError). It is worth to mention that this code (without async support) works fine with WCF Web API preview versions.

  • Anonymous
    February 24, 2012
    Hi, skimming through your article I can'f find where the Configuration is to be acquired in an ASP hosted solution. Guessing GlobalConfiguration.Configuratioin and then add the custom formatter there? Have not tried it, hence guessing. //Daniel

  • Anonymous
    February 24, 2012
    Configuration: yup that is correct!

  • Anonymous
    February 26, 2012
    Hi Henrik.  Thanks for calling out the Date issues.  This is one of the main reasons to use a different serializer like Json.Net or even ServiceStack.Text.  The ISO 8601 format solves lots of issues, and DataContractJsonSerializer is buggy even in its own implementation  (see connect.microsoft.com/.../723368) I appreciate the detail of your post! Cheers, Matt

  • Anonymous
    February 26, 2012
    It's not clear how you would integrate the JsonNetFormatter with a stand-alone HttpClient; how do you register alternative MediaTypeFormatter implementations with HttpClient; hopefully not via a HttpSelfHostConfiguration since that is just for hosting.

  • Anonymous
    March 01, 2012
    James, there are overloads for passing in the formatters on the client side so that's how you can enable your own formatter(s).

  • Anonymous
    March 07, 2012
    I was trying to add your JsonNetFormatter class in a non-self hosted environment. I created an ASP.NET Web API Project. I added JsonNetFormatter in Global.asax.cs as such:       static void Configure(HttpConfiguration config)       {                      // Create Json.Net formatter serializing DateTime using the ISO 8601 format            JsonSerializerSettings serializerSettings = new JsonSerializerSettings();            serializerSettings.Converters.Add(new IsoDateTimeConverter());            config.Formatters.Add(new JsonNetFormatter(serializerSettings));        }        protected void Application_Start()        {                        AreaRegistration.RegisterAllAreas();            RegisterGlobalFilters(GlobalFilters.Filters);            RegisterRoutes(RouteTable.Routes);            BundleTable.Bundles.RegisterTemplateBundles();            Configure(GlobalConfiguration.Configuration);        } I found that JsonNetFormatter OnWriteToStreamAsync() was never getting called. I learned the problem was that there already was a formatter for application/xml that was being used instead. So I had to remove that formatter and JsonNetFormatter was then being used. To remove the formatter I added the following at the beginning of the Configure method: var formatter = config.Formatters            .Where(f =>            {                return f.SupportedMediaTypes.Any(v => v.MediaType.Equals("application/json",                StringComparison.CurrentCultureIgnoreCase));            })            .FirstOrDefault();            if (formatter != null)            {                config.Formatters.Remove(formatter);            } Just thought I would add this comment in case anyone else is trying the same.

  • Anonymous
    March 07, 2012
    Unexpected token parsing date. Expected String, got Date. apicontroller        public Contact PostContact(Contact value)        {            _contacts.Add(value);            return value;        } model. public class Contact    {        public Contact()        {            LastModified = DateTime.Now;        }        public int Id { get; set; }        public string Name { get; set; }        public string Phone { get; set; }        public string Email { get; set; }        public DateTime LastModified { get; set; }    }

  • Anonymous
    March 10, 2012
    Hi as Aaron Williams noted JSON.NET dll is no longer needed in order to output JSON as default instead of xml.

  1. Create MVC4 Web Api Project.
  2. Modify Global.asax. Add this method. This will remove the xml formatter static void Configure(HttpConfiguration config)        {            var formatter = config.Formatters.FirstOrDefault(f => f.SupportedMediaTypes.Any(v => v.MediaType.Equals("application/xml", StringComparison.CurrentCultureIgnoreCase)));            if (formatter != null)            {                config.Formatters.Remove(formatter);            }        }
  3. Call the above method from  Application_Start() Configure(GlobalConfiguration.Configuration); That's all. The API will give you json. I hope this will help others too
  • Anonymous
    March 13, 2012
    Using formatters[0] instead of formatters.Add to override default JSON formatter. JSON.NET is needed for better DateTime format.

  • Anonymous
    March 14, 2012
    Using JSON.NET works well with DateTime Output correct ISO8601 Input Dates now works well - 2012-03-15T09:21:59.2630521Z end up as a Datetime.Kind=Utc instead of local great. But my RouteParameter Id don't work now id is always  null api/client/100 Did I miss something ?

  • Anonymous
    March 18, 2012
    would this be easy to do while debugging in visual studio?

  • Anonymous
    March 18, 2012
    Please see this comment on a Stackoverflow post: stackoverflow.com/.../9681171. While JSON.NET solves the date issues, it appears that it's incompatible with upshot.js / DbDataContext, and so POSTing new records breaks. Any suggested workarounds?

  • Anonymous
    March 26, 2012
    Hi Henrik, I used the same custom formatter. When I called API a file is getting downloaded which has the serialised output. Thanks

  • Anonymous
    April 03, 2012
    Per the Web API roadmap - aspnetwebstack.codeplex.com/wikipage It looks like JSON.NET will be the defacto serialization method when Web API goes into release. Looking forward to this.

  • Anonymous
    April 29, 2012
    Hi Henrik, I just translate your code to MVC4 WebAPI, it is great and it works. I was worried because I have to work with entities, and I couldn't fit IsReference=True to False, and this code solve my problem. Thank you very much.

  • Anonymous
    June 01, 2012
    great post. Love to see what changes are required with ASP.NET MVC 4 RC

  • Anonymous
    July 10, 2012
    Seems like that in RC version your code have some troubles. Some classes disappeared from System.Net.Http.Formatting . Can you suggest me how to fix?

  • Anonymous
    July 13, 2012
    any updates on this to use with the RC version?

  • Anonymous
    October 28, 2012
    Awesome article very much help full, I have got date serialized as 2012-02-18T00:54:06.8447642Z. Is there any way to get the date in specified format like 2012/02/08 etc. thanks in advance.

  • Anonymous
    November 27, 2012
    anyone knows where do they keep IKeyValueModel in the release version?

  • Anonymous
    February 25, 2015
    There is no IKeyValueModel and FormatterContext and etc. Seems to be copypaste from some of your project.