Introduction
ASP.NET Core (MVC / Web API) applications communicate to other applications by using built-in formats, such as JSON, XML, or plain text. By default, ASP.NET Core MVC supports built-in format for data exchange using JSON, XML, or plain text. We can also add the support for custom format by creating custom formatters.
In this article, we will learn how to create custom formatters in ASP.NET Core MVC. We can use custom formatters if we want the content negotiation process to support a content type which is not supported by built-in formatters.
Following are the step to create a custom formatter,
- Create output formatter (if we require serializing the data to send to the client)
- Create input formatter (if we require deserializing the data received from the client)
- Add the instance(s) of our formatter to the InputFormatters and OutputFormatters collections in MVCOptions.
Create Custom formatter class
To create custom formatter class, we need to derive class from appropriate base class. There are many built-in formatter classes available for input formatters, such as TextInputFormatter, JsonInputFormatter, and XmlDataContractSerializerInputFormatter etc. The InputFormatter is the base class of all input formatters and also, all the input formatters implement IInputFormatter interface.
Similarly, for the output formatter, there are many built-in formatter classes available for output formatter, such as TextOutputFormatter, JsonOutputFormatter, and XmlDataContractSerializerOutputFormatter etc. The OutputFormatter is the base class of all output formatters and also, all the output formatters implement IOutputFormatter interface.
To demonstrate the example, I have inherited the class from the TextInputFormatter or TextOutputFormatter base class.
- namespace CustomFormatter.Formatters
- {
- using Microsoft.AspNetCore.Mvc.Formatters;
- using System;
- using System.Text;
- using System.Threading.Tasks;
- public class CustomOutputFormatter : TextOutputFormatter
- {
- public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
- {
- throw new NotImplementedException();
- }
- }
- }
The next step is to specify valid media type and encoding within constructor of the formatter class. We can specify the media types and encodings by adding the SupportedMediaTypes and SupportedEncodings collections.
- public CustomOutputFormatter()
- {
- SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/CustomFormat"));
-
- SupportedEncodings.Add(Encoding.UTF8);
- SupportedEncodings.Add(Encoding.Unicode);
- }
Next step is to override CanReadType and CanWriteType method of the base class. In these methods, we can deserialize into specify the type (CanReadType) or serialize from the type (CanWriteType).
To demonstrate the example, I have created user defined types called "Employee". So, we might only be able to create custom formatter text from this type (Employee) and vice versa.
In some cases, we might need to use CanWriteResult instead of CanWriteType method. Followings are some scenarios in which we need to use CanWriteResult method,
- Our Action method returns the Model class
- The derived type classes might be returned at runtime
- If we need to know which derived class was returned by the action at runtime?
For example, our action method returns a "User" type but it might return either "Employee" or "Customer" type that derives from "User" type. If we want our formatter to only handle "Employee" objects, check the type of Object in contextObject provided by CanWriteResult method. It is not necessary to use CanWriteResult method when our action method returns IActionResult. In this case, CanWriteType method can receive the runtime type.
- protected override bool CanWriteType(Type type)
- {
- if (typeof(Employee).IsAssignableFrom(type)
- || typeof(IEnumerable<Employee>).IsAssignableFrom(type))
- {
- return base.CanWriteType(type);
- }
- return false;
- }
Next step is to override WriteResponseBodyAsync method. This method returns response in required format. To demonstrate the example, I have sent employee data in pipe format.
- public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
- {
- IServiceProvider serviceProvider = context.HttpContext.RequestServices;
- var response = context.HttpContext.Response;
-
- var buffer = new StringBuilder();
- if (context.Object is IEnumerable<Employee>)
- {
- foreach (var employee in context.Object as IEnumerable<Employee>)
- {
- FormatData(buffer, employee);
- }
- }
- else
- {
- var employee = context.Object as Employee;
- FormatData(buffer, employee);
- }
- return response.WriteAsync(buffer.ToString());
- }
-
- private static void FormatData(StringBuilder buffer, Employee employee)
- {
- buffer.Append("BEGIN|");
- buffer.AppendFormat("VERSION:1.0|");
- buffer.AppendFormat($"Data:{employee.Id}|{employee.EmployeeCode}|{employee.FirstName}|{employee.LastName}");
- buffer.Append("|END");
- }
The final step is to configure MVC to use a custom formatter. To use custom formatter, add the instance of the formatter class to OutputFormatters or InputFormatters collection MvsOption.
- public void ConfigureServices(IServiceCollection services)
- {
-
- services.AddMvc(
- option =>
- {
- option.OutputFormatters.Insert(0, new CustomOutputFormatter());
- option.InputFormatters.Add(new CustomInputFormatter());
- option.FormatterMappings.SetMediaTypeMappingForFormat("cu-for", MediaTypeHeaderValue.Parse("text/cu-for"));
- }
- );
- }
Here, I have added our custom formatter at 0 (zero) position of the OutputFormatters or InputFormatters collection, so our custom formatter has the highest priority.
Output
Similarly, we can also create input formatter that accepts the data in particular format and converts it in to required format. Following example code is input formatter that formats incoming data to Employee model.
- namespace CustomFormatter.Formatters
- {
- using Microsoft.AspNetCore.Mvc.Formatters;
- using Microsoft.Net.Http.Headers;
- using System;
- using System.IO;
- using System.Text;
- using System.Threading.Tasks;
- public class CustomInputFormatter : TextInputFormatter
- {
- public CustomInputFormatter()
- {
- SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/cu-for"));
-
- SupportedEncodings.Add(Encoding.UTF8);
- SupportedEncodings.Add(Encoding.Unicode);
- }
-
-
- protected override bool CanReadType(Type type)
- {
- if (type == typeof(Employee))
- {
- return base.CanReadType(type);
- }
- return false;
- }
- public override async Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context, Encoding encoding)
- {
- if (context == null)
- {
- throw new ArgumentNullException(nameof(context));
- }
-
- if (encoding == null)
- {
- throw new ArgumentNullException(nameof(encoding));
- }
-
- var request = context.HttpContext.Request;
-
- using (var reader = new StreamReader(request.Body, encoding))
- {
- try
- {
- var line = await reader.ReadLineAsync();
- if (!line.StartsWith("BEGIN|VERSION:1.0|"))
- {
- var errorMessage = $"Data must start with 'BEGIN|VERSION:1.0|'";
- context.ModelState.TryAddModelError(context.ModelName, errorMessage);
- throw new Exception(errorMessage);
- }
- if (!line.EndsWith("|END"))
- {
- var errorMessage = $"Data must end with '|END'";
- context.ModelState.TryAddModelError(context.ModelName, errorMessage);
- throw new Exception(errorMessage);
- }
-
- var split = line.Substring(line.IndexOf("Data:") + 5).Split(new char[] { '|' });
- var emp = new Employee()
- {
- Id = Convert.ToInt32(split[0]),
- EmployeeCode = split[1],
- FirstName = split[2],
- LastName = split[3]
- };
-
- return await InputFormatterResult.SuccessAsync(emp);
- }
- catch
- {
- return await InputFormatterResult.FailureAsync();
- }
- }
- }
- }
- }
Output
Summary
ASP.NET Core provides facility to create our own formatter. There are many built-in formatters supported by ASP.NET Core, such as JSON, XML, and plain text. Custom formatter is very useful when we interact with any third party system which accepts data in a specific format and sends the data in that specific format. Using custom formatter, we can add support to additional formats.
You can view or download the source code from the following
link.