Skip to content

Commit d0e54e1

Browse files
Update Custom Formmatter (#18795)
* Update Custom FOrmmatter * Update Custom FOrmmatter * Update Custom FOrmmatter * Update Custom FOrmmatter * Update Custom FOrmmatter * clean up * clean up * clean up * clean up * clean up * clean up * clean up * Apply suggestions from code review Awesome suggestions. Much appreciated. Co-authored-by: Kirk Larkin <6025110+serpent5@users.noreply.github.com> * final edit * Update aspnetcore/web-api/advanced/custom-formatters.md Co-authored-by: Kirk Larkin <6025110+serpent5@users.noreply.github.com> Co-authored-by: Kirk Larkin <6025110+serpent5@users.noreply.github.com>
1 parent 71a0045 commit d0e54e1

File tree

11 files changed

+461
-42
lines changed

11 files changed

+461
-42
lines changed

aspnetcore/web-api/advanced/custom-formatters.md

Lines changed: 64 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -9,112 +9,134 @@ uid: web-api/advanced/custom-formatters
99
---
1010
# Custom formatters in ASP.NET Core Web API
1111

12-
By [Tom Dykstra](https://github.com/tdykstra)
12+
By [Kirk Larkin](https://twitter.com/serpent5) and [Tom Dykstra](https://github.com/tdykstra).
1313

1414
ASP.NET Core MVC supports data exchange in Web APIs using input and output formatters. Input formatters are used by [Model Binding](xref:mvc/models/model-binding). Output formatters are used to [format responses](xref:web-api/advanced/formatting).
1515

1616
The framework provides built-in input and output formatters for JSON and XML. It provides a built-in output formatter for plain text, but doesn't provide an input formatter for plain text.
1717

18-
This article shows how to add support for additional formats by creating custom formatters. For an example of a custom input formatter for plain text, see [TextPlainInputFormatter](https://github.com/aspnet/Entropy/blob/master/samples/Mvc.Formatters/TextPlainInputFormatter.cs) on GitHub.
18+
This article shows how to add support for additional formats by creating custom formatters. For an example of a custom plain text input formatter, see [TextPlainInputFormatter](https://github.com/aspnet/Entropy/blob/master/samples/Mvc.Formatters/TextPlainInputFormatter.cs) on GitHub.
1919

2020
[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/master/aspnetcore/web-api/advanced/custom-formatters/sample) ([how to download](xref:index#how-to-download-a-sample))
2121

2222
## When to use custom formatters
2323

24-
Use a custom formatter when you want the [content negotiation](xref:web-api/advanced/formatting#content-negotiation) process to support a content type that isn't supported by the built-in formatters.
25-
26-
For example, if some of the clients for your web API can handle the [Protobuf](https://github.com/google/protobuf) format, you might want to use Protobuf with those clients because it's more efficient. Or you might want your web API to send contact names and addresses in [vCard](https://wikipedia.org/wiki/VCard) format, a commonly used format for exchanging contact data. The sample app provided with this article implements a simple vCard formatter.
24+
Use a custom formatter to add support for a content type that isn't handled by the bult-in formatters.
2725

2826
## Overview of how to use a custom formatter
2927

30-
Here are the steps to create and use a custom formatter:
31-
32-
* Create an output formatter class if you want to serialize data to send to the client.
33-
* Create an input formatter class if you want to deserialize data received from the client.
34-
* Add instances of your formatters to the `InputFormatters` and `OutputFormatters` collections in [MvcOptions](/dotnet/api/microsoft.aspnetcore.mvc.mvcoptions).
28+
To create a custom formatter:
3529

36-
The following sections provide guidance and code examples for each of these steps.
30+
* For serializing data sent to the client, create an output formatter class.
31+
* For deserialzing data received from the client, create an input formatter class.
32+
* Add instances of formatter classes to the `InputFormatters` and `OutputFormatters` collections in [MvcOptions](/dotnet/api/microsoft.aspnetcore.mvc.mvcoptions).
3733

3834
## How to create a custom formatter class
3935

4036
To create a formatter:
4137

42-
* Derive the class from the appropriate base class.
38+
* Derive the class from the appropriate base class. The sample app derives from <xref:Microsoft.AspNetCore.Mvc.Formatters.TextOutputFormatter> and <xref:Microsoft.AspNetCore.Mvc.Formatters.TextInputFormatter>.
4339
* Specify valid media types and encodings in the constructor.
44-
* Override `CanReadType`/`CanWriteType` methods
45-
* Override `ReadRequestBodyAsync`/`WriteResponseBodyAsync` methods
40+
* Override the <xref:Microsoft.AspNetCore.Mvc.Formatters.InputFormatter.CanReadType%2A> and <xref:Microsoft.AspNetCore.Mvc.Formatters.OutputFormatter.CanWriteType%2A> methods.
41+
* Override the <xref:Microsoft.AspNetCore.Mvc.Formatters.InputFormatter.ReadRequestBodyAsync%2A> and `WriteResponseBodyAsync` methods.
42+
43+
The following code shows the `VcardOutputFormatter` class from the [sample](https://github.com/dotnet/AspNetCore.Docs/tree/master/aspnetcore/web-api/advanced/custom-formatters/3.1sample):
44+
45+
[!code-csharp[](custom-formatters/3.1sample/Formatters/VcardOutputFormatter.cs?name=snippet)]
4646

4747
### Derive from the appropriate base class
4848

4949
For text media types (for example, vCard), derive from the [TextInputFormatter](/dotnet/api/microsoft.aspnetcore.mvc.formatters.textinputformatter) or [TextOutputFormatter](/dotnet/api/microsoft.aspnetcore.mvc.formatters.textoutputformatter) base class.
5050

51-
[!code-csharp[](custom-formatters/sample/Formatters/VcardOutputFormatter.cs?name=classdef)]
52-
53-
For an input formatter example, see the [sample app](https://github.com/dotnet/AspNetCore.Docs/tree/master/aspnetcore/web-api/advanced/custom-formatters/sample).
51+
[!code-csharp[](custom-formatters/3.1sample/Formatters/VcardOutputFormatter.cs?name=classdef)]
5452

5553
For binary types, derive from the [InputFormatter](/dotnet/api/microsoft.aspnetcore.mvc.formatters.inputformatter) or [OutputFormatter](/dotnet/api/microsoft.aspnetcore.mvc.formatters.outputformatter) base class.
5654

5755
### Specify valid media types and encodings
5856

5957
In the constructor, specify valid media types and encodings by adding to the `SupportedMediaTypes` and `SupportedEncodings` collections.
6058

61-
[!code-csharp[](custom-formatters/sample/Formatters/VcardOutputFormatter.cs?name=ctor&highlight=3,5-6)]
62-
63-
For an input formatter example, see the [sample app](https://github.com/dotnet/AspNetCore.Docs/tree/master/aspnetcore/web-api/advanced/custom-formatters/sample).
59+
[!code-csharp[](custom-formatters/3.1sample/Formatters/VcardOutputFormatter.cs?name=ctor)]
6460

65-
> [!NOTE]
66-
> You can't do constructor dependency injection in a formatter class. For example, you can't get a logger by adding a logger parameter to the constructor. To access services, you have to use the context object that gets passed in to your methods. A code example [below](#read-write) shows how to do this.
61+
A formatter class can **not** use constructor injection for its dependencies. For example, `ILogger<VcardOutputFormatter>` cannot be added as a parameter to the constructor. To access services, use the context object that gets passed in to the methods. A code example in this article and the [sample](https://github.com/dotnet/AspNetCore.Docs/tree/master/aspnetcore/web-api/advanced/custom-formatters/3.1sample) show how to do this.
6762

68-
### Override CanReadType/CanWriteType
63+
### Override CanReadType and CanWriteType
6964

70-
Specify the type you can deserialize into or serialize from by overriding the `CanReadType` or `CanWriteType` methods. For example, you might only be able to create vCard text from a `Contact` type and vice versa.
65+
Specify the type to deserialize into or serialize from by overriding the `CanReadType` or `CanWriteType` methods. For example, creating vCard text from a `Contact` type and vice versa.
7166

72-
[!code-csharp[](custom-formatters/sample/Formatters/VcardOutputFormatter.cs?name=canwritetype)]
73-
74-
For an input formatter example, see the [sample app](https://github.com/dotnet/AspNetCore.Docs/tree/master/aspnetcore/web-api/advanced/custom-formatters/sample).
67+
[!code-csharp[](custom-formatters/3.1sample/Formatters/VcardOutputFormatter.cs?name=canwritetype)]
7568

7669
#### The CanWriteResult method
7770

78-
In some scenarios you have to override `CanWriteResult` instead of `CanWriteType`. Use `CanWriteResult` if the following conditions are true:
71+
In some scenarios, `CanWriteResult` must be overridden rather than `CanWriteType`. Use `CanWriteResult` if the following conditions are true:
7972

80-
* Your action method returns a model class.
73+
* The action method returns a model class.
8174
* There are derived classes which might be returned at runtime.
82-
* You need to know at runtime which derived class was returned by the action.
75+
* The derived class returned by the action must be known at runtime.
8376

84-
For example, suppose your action method signature returns a `Person` type, but it may return a `Student` or `Instructor` type that derives from `Person`. If you want your formatter to handle only `Student` objects, check the type of [Object](/dotnet/api/microsoft.aspnetcore.mvc.formatters.outputformattercanwritecontext.object#Microsoft_AspNetCore_Mvc_Formatters_OutputFormatterCanWriteContext_Object) in the context object provided to the `CanWriteResult` method. Note that it's not necessary to use `CanWriteResult` when the action method returns `IActionResult`; in that case, the `CanWriteType` method receives the runtime type.
77+
For example, suppose the action method:
8578

86-
<a id="read-write"></a>
79+
* Signature returns a `Person` type.
80+
* Can return a `Student` or `Instructor` type that derives from `Person`.
81+
82+
For the formatter to handle only `Student` objects, check the type of [Object](/dotnet/api/microsoft.aspnetcore.mvc.formatters.outputformattercanwritecontext.object#Microsoft_AspNetCore_Mvc_Formatters_OutputFormatterCanWriteContext_Object) in the context object provided to the `CanWriteResult` method. When the action method returns `IActionResult`:
8783

88-
### Override ReadRequestBodyAsync/WriteResponseBodyAsync
84+
* It's not necessary to use `CanWriteResult`.
85+
* The `CanWriteType` method receives the runtime type.
8986

90-
You do the actual work of deserializing or serializing in `ReadRequestBodyAsync` or `WriteResponseBodyAsync`. The highlighted lines in the following example show how to get services from the dependency injection container (you can't get them from constructor parameters).
87+
<a id="read-write"></a>
88+
89+
### Override ReadRequestBodyAsync and WriteResponseBodyAsync
9190

92-
[!code-csharp[](custom-formatters/sample/Formatters/VcardOutputFormatter.cs?name=writeresponse&highlight=3-4)]
91+
Deserialization or serialization is performed in `ReadRequestBodyAsync` or `WriteResponseBodyAsync`. The following example shows how to get services from the dependency injection container. Services can't be obtained from constructor parameters.
9392

94-
For an input formatter example, see the [sample app](https://github.com/dotnet/AspNetCore.Docs/tree/master/aspnetcore/web-api/advanced/custom-formatters/sample).
93+
[!code-csharp[](custom-formatters/3.1sample/Formatters/VcardOutputFormatter.cs?name=writeresponse)]
9594

9695
## How to configure MVC to use a custom formatter
9796

9897
To use a custom formatter, add an instance of the formatter class to the `InputFormatters` or `OutputFormatters` collection.
9998

99+
::: moniker range=">= aspnetcore-3.0"
100+
101+
[!code-csharp[](custom-formatters/3.1sample/Startup.cs?name=mvcoptions)]
102+
103+
::: moniker-end
104+
105+
::: moniker range="< aspnetcore-3.0"
106+
100107
[!code-csharp[](custom-formatters/sample/Startup.cs?name=mvcoptions&highlight=3-4)]
101108

109+
::: moniker-end
110+
102111
Formatters are evaluated in the order you insert them. The first one takes precedence.
103112

104-
## Next steps
113+
## The completed `VcardInputFormatter` class
114+
115+
The following code shows the `VcardInputFormatter` class from the [sample](https://github.com/dotnet/AspNetCore.Docs/tree/master/aspnetcore/web-api/advanced/custom-formatters/3.1sample):
116+
117+
[!code-csharp[](custom-formatters/3.1sample/Formatters/VcardInputFormatter.cs?name=snippet)]
105118

106-
* [Sample app for this doc](https://github.com/dotnet/AspNetCore.Docs/tree/master/aspnetcore/web-api/advanced/custom-formatters/sample), which implements simple vCard input and output formatters. The apps reads and writes vCards that look like the following example:
119+
## Test the app
120+
121+
[Run the sample app for this article](https://github.com/dotnet/AspNetCore.Docs/tree/master/aspnetcore/web-api/advanced/custom-formatters/sample), which implements basic vCard input and output formatters. The app reads and writes vCards similar to the following:
107122

108123
```
109124
BEGIN:VCARD
110125
VERSION:2.1
111126
N:Davolio;Nancy
112127
FN:Nancy Davolio
113-
no-loc: [Blazor, "Identity", "Let's Encrypt", Razor, SignalR]
114-
uid:20293482-9240-4d68-b475-325df4a83728
115128
END:VCARD
116129
```
117130

118-
To see vCard output, run the application and send a Get request with Accept header "text/vcard" to `http://localhost:63313/api/contacts/` (when running from Visual Studio) or `http://localhost:5000/api/contacts/` (when running from the command line).
131+
To see vCard output, run the app and send a Get request with Accept header `text/vcard` to `https://localhost:5001/api/contacts`.
132+
133+
To add a vCard to the in-memory collection of contacts:
134+
135+
* Send a `Post` request to `/api/contacts` with a tool like Postman.
136+
* Set the `Content-Type` header to `text/vcard`.
137+
* Set `vCard` text in the body, formatted like the preceding example.
138+
139+
## Additional resources
119140

120-
To add a vCard to the in-memory collection of contacts, send a Post request to the same URL, with Content-Type header "text/vcard" and with vCard text in the body, formatted like the example above.
141+
* <xref:web-api/advanced/formatting>
142+
* <xref:grpc/dotnet-grpc>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
2+
3+
<PropertyGroup>
4+
<TargetFramework>netcoreapp3.1</TargetFramework>
5+
</PropertyGroup>
6+
7+
8+
</Project>
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Threading.Tasks;
5+
using Microsoft.AspNetCore.Mvc;
6+
using CustomFormatterDemo.Models;
7+
using System.Collections.Concurrent;
8+
9+
10+
namespace CustomFormatterDemo.Controllers
11+
{
12+
// [ApiController]
13+
[Route("api/[controller]")]
14+
public class ContactsController : Controller
15+
{
16+
private static ConcurrentDictionary<string, Contact> _contacts =
17+
new ConcurrentDictionary<string, Contact>();
18+
19+
public ContactsController()
20+
{
21+
if (_contacts.Count == 0)
22+
Add(new Contact() { FirstName = "Nancy", LastName = "Davolio" });
23+
}
24+
25+
public void Add(Contact contact)
26+
{
27+
contact.ID = Guid.NewGuid().ToString();
28+
_contacts[contact.ID] = contact;
29+
}
30+
31+
32+
// GET api/contacts
33+
[HttpGet]
34+
public IEnumerable<Contact> Get()
35+
{
36+
return _contacts.Values;
37+
}
38+
39+
// GET api/contacts/{guid}
40+
[HttpGet("{id}", Name="Get")]
41+
public IActionResult Get(string id)
42+
{
43+
Contact contact;
44+
_contacts.TryGetValue(id, out contact);
45+
if (contact == null)
46+
{
47+
return NotFound();
48+
}
49+
return Ok(contact);
50+
}
51+
52+
// POST api/contacts
53+
[HttpPost]
54+
public IActionResult Post([FromBody]Contact contact)
55+
{
56+
if (ModelState.IsValid)
57+
{
58+
Add(contact);
59+
return CreatedAtRoute("Get", new { id = contact.ID }, contact);
60+
}
61+
return BadRequest();
62+
}
63+
64+
// This is not a correct PUT so removing
65+
// PUT api/contacts/{guid}
66+
//[HttpPut("{id}")]
67+
//public IActionResult Put(string id, [FromBody]Contact contact)
68+
//{
69+
// if (ModelState.IsValid && id == contact.ID)
70+
// {
71+
// var contactToUpdate = Contacts.Get(id);
72+
// if (contactToUpdate != null)
73+
// {
74+
// Contacts.Update(contact);
75+
// return new NoContentResult();
76+
// }
77+
// return NotFound();
78+
// }
79+
// return BadRequest();
80+
//}
81+
82+
// DELETE api/contacts/{guid}
83+
[HttpDelete("{id}")]
84+
public IActionResult Delete(string id)
85+
{
86+
Contact contact;
87+
_contacts.TryGetValue(id, out contact);
88+
if (contact == null)
89+
{
90+
return NotFound();
91+
}
92+
93+
_contacts.TryRemove(id, out contact);
94+
return NoContent();
95+
}
96+
}
97+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
using Microsoft.AspNetCore.Mvc.Formatters;
2+
using Microsoft.Net.Http.Headers;
3+
using System;
4+
using System.IO;
5+
using System.Text;
6+
using System.Threading.Tasks;
7+
using CustomFormatterDemo.Models;
8+
using Microsoft.Extensions.Logging;
9+
10+
namespace CustomFormatterDemo.Formatters
11+
{
12+
#region snippet
13+
#region classdef
14+
public class VcardInputFormatter : TextInputFormatter
15+
#endregion
16+
{
17+
#region ctor
18+
public VcardInputFormatter()
19+
{
20+
SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/vcard"));
21+
22+
SupportedEncodings.Add(Encoding.UTF8);
23+
SupportedEncodings.Add(Encoding.Unicode);
24+
}
25+
#endregion
26+
27+
#region canreadtype
28+
protected override bool CanReadType(Type type)
29+
{
30+
if (type == typeof(Contact))
31+
{
32+
return base.CanReadType(type);
33+
}
34+
return false;
35+
}
36+
#endregion
37+
38+
#region readrequest
39+
public override async Task<InputFormatterResult> ReadRequestBodyAsync(
40+
InputFormatterContext context, Encoding effectiveEncoding)
41+
{
42+
IServiceProvider serviceProvider = context.HttpContext.RequestServices;
43+
var logger = serviceProvider.GetService(typeof(ILogger<VcardInputFormatter>))
44+
as ILogger;
45+
46+
if (context == null)
47+
{
48+
var nameOfContext = nameof(context);
49+
logger.LogError(nameOfContext);
50+
throw new ArgumentNullException(nameOfContext);
51+
}
52+
53+
if (effectiveEncoding == null)
54+
{
55+
var nameofEffectiveEncoding = nameof(effectiveEncoding);
56+
logger.LogError(nameofEffectiveEncoding);
57+
throw new ArgumentNullException(nameofEffectiveEncoding);
58+
}
59+
60+
var request = context.HttpContext.Request;
61+
62+
using (var reader = new StreamReader(request.Body, effectiveEncoding))
63+
{
64+
string nameLine=null;
65+
try
66+
{
67+
await ReadLineAsync("BEGIN:VCARD", reader, context, logger);
68+
await ReadLineAsync("VERSION:", reader, context, logger);
69+
70+
nameLine = await ReadLineAsync("N:", reader, context, logger);
71+
var split = nameLine.Split(";".ToCharArray());
72+
var contact = new Contact() { LastName = split[0].Substring(2),
73+
FirstName = split[1] };
74+
75+
await ReadLineAsync("FN:", reader, context, logger);
76+
await ReadLineAsync("END:VCARD", reader, context, logger);
77+
logger.LogInformation("nameLine = {nameLine}", nameLine);
78+
79+
return await InputFormatterResult.SuccessAsync(contact);
80+
}
81+
catch
82+
{
83+
logger.LogError("Read failed: nameLine = {nameLine}", nameLine);
84+
return await InputFormatterResult.FailureAsync();
85+
}
86+
}
87+
}
88+
89+
private async Task<string> ReadLineAsync(string expectedText, StreamReader reader,
90+
InputFormatterContext context,
91+
ILogger logger)
92+
{
93+
var line = await reader.ReadLineAsync();
94+
if (!line.StartsWith(expectedText))
95+
{
96+
var errorMessage = $"Looked for '{expectedText}' and got '{line}'";
97+
context.ModelState.TryAddModelError(context.ModelName, errorMessage);
98+
logger.LogError(errorMessage);
99+
throw new Exception(errorMessage);
100+
}
101+
return line;
102+
}
103+
#endregion
104+
}
105+
#endregion
106+
}

0 commit comments

Comments
 (0)