⭐ If you would like to buy me a coffee, well thank you very much that is mega kind! : https://www.buymeacoffee.com/honeyvig
Hire a web Developer and Designer to upgrade and boost your online presence with cutting edge Technologies
Showing posts with label C#. Show all posts
Showing posts with label C#. Show all posts

Friday, September 16, 2022

Angular Lazy Loading

 

Create a New Module

So let’s start with the Angular CLI command for the module creation:

ng g module owner --routing=true --module app.module

This command does several things. It creates a new Owner module, it also creates a routing file for that module, and finally, updates the App module file:

CREATE src/app/owner/owner-routing.module.ts (248 bytes)
CREATE src/app/owner/owner.module.ts (276 bytes)
UPDATE src/app/app.module.ts (989 bytes)

Let’s inspect the owner.module.ts file:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { OwnerRoutingModule } from './owner-routing.module';
@NgModule({
declarations: [],
imports: [
CommonModule,
OwnerRoutingModule
]
})
export class OwnerModule { }

There are two small differences between this module file and the app module file. The first difference is that in the app module file we have an import statement for the BrowserModule, and in the owner module file, we have an import statement for the CommonModule. That’s because the BrowserModule is only related to the root module in the application.

The second difference is that we don’t have the providers array inside the owner module file. That’s because we should register all the services in the root module. That way components will inject the same instance of the service only once and you can keep the state in your service.

Of course, if we really want to register a service inside any child module, we could just add the providers array. But, by doing so we cannot keep the state inside our service because every time we create a new instance of that component a new instance of a service is created.

Finally, we can see that this module imports the OwnerRoutingModule from a separate file.

Owner Component and Angular Lazy Loading

Let’s start with the creation of the owner component files:

ng g component owner/owner-list --skip-tests

This command is going to create the required folder structure and it is going to import this component inside the owner.module.ts file as well.

What we want now is, when we click on the “Owner-Actions” menu, to show the content from this component’s HTML file. So first, just for the testing purposes, let’s modify the owner.component.html file by adding one paragraph (<p> tag):

<p>This is owner-list component page.</p>

After that, let’s modify the app-routing.module.ts file:

const routes: Routes = [
{ path: 'home', component: HomeComponent },
{ path: 'owner', loadChildren: () => import('./owner/owner.module').then(m => m.OwnerModule) },
{ path: '404', component: NotFoundComponent },
{ path: '', redirectTo: '/home', pathMatch: 'full' },
{ path: '**', redirectTo: '/404', pathMatch: 'full' }
];

With the modified part of the code, we are configuring the app-routing.module to load the owner module whenever someone searches for the http://localhost:4200/owner endpoint. As we can notice, we are using the loadChildren property which means, that the owner module with its components won’t be loaded until we explicitly ask for them. By doing this, we are configuring Angular lazy loading from the owner module content.

Now if we navigate to the Home page, we will get only resources from the root module, not from the owner module. And only by navigating to the owner-actions menu, we will load the owner module resources into the application. From the previous statement, we can see why is Angular lazy loading important for Angular applications.

Routing for the Owner Module

Now, to enable navigation to the OwnerList component, we have to modify the owner-routing.module.ts:

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { OwnerListComponent } from './owner-list/owner-list.component';
const routes: Routes = [
{ path:'list', component: OwnerListComponent }
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class OwnerRoutingModule { }

With this setup, we are exposing our OwnerListComponent on the http://localhost:4200/owner/list endpoint. Moreover, we are using the RouterModule.forChild function and not the forRoot function. This is the case because we should use the forRoot function only in the root module of the application.

Now we have to modify the menu.component.html file:

<div class="collapse navbar-collapse" id="collapseNav" [collapse]="!isCollapsed" [isAnimated]="true">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link" [routerLink]="['/owner/list']" routerLinkActive="active"
[routerLinkActiveOptions]="{exact: true}"> Owner Actions </a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Account Actions </a>
</li>
</ul>
</div>

After all of these modifications, we can run our app and click the Owner Actions link. As soon as we do that, our new component will show up, and the link will get an active class style:

Now we know how to set up the routing for the child module, and for the component inside that module as well.

Subscription and Data Display

Let’s continue on.

When we navigate to the Owner Actions menu, we want to show all of the owners to the user. So that means when the owner component loads, the app automatically gets all the owners from the server.

We already have our Owner interface created (from the previous post) and we will use it here.

That said, let’s modify the OwnerListComponent file:

import { Component, OnInit } from '@angular/core';
import { Owner } from './../../_interfaces/owner.model';
import { OwnerRepositoryService } from './../../shared/services/owner-repository.service';
@Component({
selector: 'app-owner-list',
templateUrl: './owner-list.component.html',
styleUrls: ['./owner-list.component.css']
})
export class OwnerListComponent implements OnInit {
owners: Owner[];
constructor(private repository: OwnerRepositoryService) { }
ngOnInit(): void {
this.getAllOwners();
}
private getAllOwners = () => {
const apiAddress: string = 'api/owner';
this.repository.getOwners(apiAddress)
.subscribe(own => {
this.owners = own;
})
}
}

We have the owners property with the and it is of the Owner array type. Next, we execute the subscribe function, which is going to populate that property with all the owners from the server. Using that owners property to create our HTML page is what we aim for.

To accomplish that, let’s modify the HTML component:

<div class="row">
<div class="offset-10 col-md-2 mt-2"> <a href="#">Create owner</a> </div>
</div> <br>
<div class="row">
<div class="col-md-12">
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>Owner name</th>
<th>Owner address</th>
<th>Date of birth</th>
<th>Details</th>
<th>Update</th>
<th>Delete</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let owner of owners">
<td>{{owner.name}}</td>
<td>{{owner.address}}</td>
<td>{{owner.dateOfBirth | date: 'dd/MM/yyyy'}}</td>
<td><button type="button" id="details" class="btn btn-primary">Details</button></td>
<td><button type="button" id="update" class="btn btn-success">Update</button></td>
<td><button type="button" id="delete" class="btn btn-danger">Delete</button></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>

We could split this html file into two components (the parent component: OwnerList and the child component Owner). But because we didn’t explain how to use child components, which we’ll do in the next posts, we are going to leave it like this for now.

We use some basic Bootstrap classes to create a table showing the owner’s data. Inside that table, we loop over all the owners with the *ngFor directive. Then by using interpolation {{}}, we show owner properties on the page. For the dateOfBirth property, we are using just the Date pipe | date: 'dd/MM/yyyy' to format it the way we want to see it on a screen.

In our application, we are going to use the date format as MM/dd/yyyy, but here we are going to use dd/MM/yyyy just to demonstrate the way to change the format with pipes without too much effort.

Now, we can start our server app, which you can find on this GitHub repo. Once we start it, we can run our Angular app and navigate to the Owner Actions link:

Conclusion

By reading this post we have learned how to create a new module and what imports to use. Additionally, we’ve learned the way to configure Angular lazy loading and how it can help our application.

Also, now we know how to execute HTTP request with a subscription and display result data on the page, and the way to reformat our date when displaying it.

Saturday, September 10, 2022

ASP.NET Core Web API – Post, Put, Delete

 

In the previous post, we have handled different GET requests with the help of a DTO object. In this post, we are going to create POST PUT DELETE requests and by doing so we are going to complete the server part (.NET Core part) of this series.

Let’s get into it.

Let’s get right into it.

Handling POST Request

Firstly, let’s modify the decoration attribute for the action method GetOwnerById in the Owner controller:

[HttpGet("{id}", Name = "OwnerById")]

With this modification, we are setting the name for the action. This name will come in handy in the action method for creating a new owner.

we use the model class just to fetch the data from the database, to return the result we need a DTO. It is the same for the create action. So, let’s create the OwnerForCreationDto class in the Entities/DataTransferObjects folder:

public class OwnerForCreationDto
{
[Required(ErrorMessage = "Name is required")]
[StringLength(60, ErrorMessage = "Name can't be longer than 60 characters")]
public string? Name { get; set; }
[Required(ErrorMessage = "Date of birth is required")]
public DateTime DateOfBirth { get; set; }
[Required(ErrorMessage = "Address is required")]
[StringLength(100, ErrorMessage = "Address cannot be loner then 100 characters")]
public string? Address { get; set; }
}

As you can see, we don’t have the Id and Accounts properties.

We are going to continue with the interface modification:

public interface IOwnerRepository : IRepositoryBase<Owner>
{
IEnumerable<Owner> GetAllOwners();
Owner GetOwnerById(Guid ownerId);
Owner GetOwnerWithDetails(Guid ownerId);
void CreateOwner(Owner owner);
}

After the interface modification, we are going to implement that interface:

public void CreateOwner(Owner owner)
{
Create(owner);
}

Before we modify the OwnerController, we have to create an additional mapping rule:

CreateMap<OwnerForCreationDto, Owner>();

Lastly, let’s modify the controller:

[HttpPost]
public IActionResult CreateOwner([FromBody]OwnerForCreationDto owner)
{
try
{
if (owner is null)
{
_logger.LogError("Owner object sent from client is null.");
return BadRequest("Owner object is null");
}
if (!ModelState.IsValid)
{
_logger.LogError("Invalid owner object sent from client.");
return BadRequest("Invalid model object");
}
var ownerEntity = _mapper.Map<Owner>(owner);
_repository.Owner.CreateOwner(ownerEntity);
_repository.Save();
var createdOwner = _mapper.Map<OwnerDto>(ownerEntity);
return CreatedAtRoute("OwnerById", new { id = createdOwner.Id }, createdOwner);
}
catch (Exception ex)
{
_logger.LogError($"Something went wrong inside CreateOwner action: {ex.Message}");
return StatusCode(500, "Internal server error");
}
}

Right now is a good time to test this code by sending the POST request by using Postman.

Code Explanation

Let’s talk a little bit about this code. The interface and the repository parts are pretty clear so we won’t talk about that. But the code in the controller contains several things worth mentioning.

The CreateOwner method has its own [HttpPost] decoration attribute, which restricts it to the POST requests. Furthermore, notice the owner parameter which comes from the client. We are not collecting it from Uri but from the request body. Thus the usage of the [FromBody] attribute. Also, the owner object is a complex type and because of that, we have to use [FromBody].

If we wanted to, we could explicitly mark the action to take this parameter from the Uri by decorating it with the [FromUri] attribute, though I wouldn’t recommend that at all due to the security reasons and complexity of the request.

Since the owner parameter comes from the client, it could happen that the client doesn’t send that parameter at all. As a result, we have to validate it against the reference type’s default value, which is null.

Further down the code, you can notice this type of validation: if(!ModelState.IsValid). If you look at the owner model properties: Name, Address, and DateOfBirth, you will notice that all of them are decorated with Validation Attributes. If for some reason validation fails, the ModelState.IsValid will return false as a result, signaling that something is wrong with the creation DTO object. Otherwise, it will return true which means that values in all the properties are valid.

We have two map actions as well. The first one is from the OwnerForCreationDto type to the Owner type because we accept the OwnerForCreationDto object from the client and we have to use the Owner object for the create action. The second map action is from the Owner type to the OwnerDto type, which is a type we return as a result.

The last thing to mention is this part of the code:

CreatedAtRoute("OwnerById", new { id = owner.Id}, owner);

CreatedAtRoute will return a status code 201, which stands for Created as explained in our post: The HTTP Reference. Also, it will populate the body of the response with the new owner object as well as the Location attribute within the response header with the address to retrieve that owner. We need to provide the name of the action, where we can retrieve the created entity:

If we copy this address and paste it in Postman, once we send the GET request, we are going to get a newly created owner object.

Handling PUT Request

Excellent.

Let’s continue with the PUT request, to update the owner entity.

First, we are going to add an additional DTO class:

public class OwnerForUpdateDto
{
[Required(ErrorMessage = "Name is required")]
[StringLength(60, ErrorMessage = "Name can't be longer than 60 characters")]
public string Name { get; set; }
[Required(ErrorMessage = "Date of birth is required")]
public DateTime DateOfBirth { get; set; }
[Required(ErrorMessage = "Address is required")]
[StringLength(100, ErrorMessage = "Address cannot be loner then 100 characters")]
public string Address { get; set; }
}

We did the same thing as with the OwnerForCreationDto class. Even though this class looks the same as the OwnerForCreationDto, they are not the same. First of all, they have a semantical difference, this one is for update action and the previous one is for creation. Additionally, the validation rules that apply for the creation of DTO don’t have to be the same for the update DTO. Therefore, it is always a good practice to separate those.

One more thing, if you want to remove the code duplication from the OwnerForCreationDto and OwnerForUpdateDto, you can create an additional abstract class, extract properties to it and then just force these classes to inherit from the abstract class. Due to the sake of simplicity, we won’t do that now.

After that, we have to create a new map rule:

CreateMap<OwnerForUpdateDto, Owner>();

Then, let’s change the interface:

public interface IOwnerRepository : IRepositoryBase<Owner>
{
IEnumerable<Owner> GetAllOwners();
Owner GetOwnerById(Guid ownerId);
Owner GetOwnerWithDetails(Guid ownerId);
void CreateOwner(Owner owner);
void UpdateOwner(Owner owner);
}

Of course, we have to modify the OwnerRepository.cs:

public void UpdateOwner(Owner owner)
{
Update(owner);
}

Finally, alter the OwnerController:

[HttpPut("{id}")]
public IActionResult UpdateOwner(Guid id, [FromBody]OwnerForUpdateDto owner)
{
try
{
if (owner is null)
{
_logger.LogError("Owner object sent from client is null.");
return BadRequest("Owner object is null");
}
if (!ModelState.IsValid)
{
_logger.LogError("Invalid owner object sent from client.");
return BadRequest("Invalid model object");
}
var ownerEntity = _repository.Owner.GetOwnerById(id);
if (ownerEntity is null)
{
_logger.LogError($"Owner with id: {id}, hasn't been found in db.");
return NotFound();
}
_mapper.Map(owner, ownerEntity);
_repository.Owner.UpdateOwner(ownerEntity);
_repository.Save();
return NoContent();
}
catch (Exception ex)
{
_logger.LogError($"Something went wrong inside UpdateOwner action: {ex.Message}");
return StatusCode(500, "Internal server error");
}
}

As you may have noticed, the action method is decorated with the [HttpPut] attribute. Furthermore, it receives two parameters: id of the entity we want to update and the entity with the updated fields, taken from the request body. The rest of the code is pretty simple. After the validation, we are pulling the owner from the database and executing the update of that owner. Finally, we return NoContent which stands for the status code 204:

You can read more about Update actions in ASP.NET Core with EF Core to get a better picture of how things are done behind the scene. It could be very useful to upgrade quality of the update actions.

Handling DELETE Request

For the Delete request, we should just follow these steps:

Interface:

public interface IOwnerRepository : IRepositoryBase<Owner>
{
IEnumerable<Owner> GetAllOwners();
Owner GetOwnerById(Guid ownerId);
Owner GetOwnerWithDetails(Guid ownerId);
void CreateOwner(Owner owner);
void UpdateOwner(Owner owner);
void DeleteOwner(Owner owner);
}

OwnerRepository:

public void DeleteOwner(Owner owner)
{
Delete(owner);
}

OwnerController:

[HttpDelete("{id}")]
public IActionResult DeleteOwner(Guid id)
{
try
{
var owner = _repository.Owner.GetOwnerById(id);
if(owner == null)
{
_logger.LogError($"Owner with id: {id}, hasn't been found in db.");
return NotFound();
}
_repository.Owner.DeleteOwner(owner);
_repository.Save();
return NoContent();
}
catch (Exception ex)
{
_logger.LogError($"Something went wrong inside DeleteOwner action: {ex.Message}");
return StatusCode(500, "Internal server error");
}
}

Let’s handle one more thing. If you try to delete the owner that has accounts, you are going to get 500 internal errors because we didn’t allow cascade delete in our database configuration. What we want is to return a BadRequest. So, to do that let’s make a couple of modifications.

Modify the IAccountRepository interface:

using Entities.Models;
namespace Contracts
{
public interface IAccountRepository
{
IEnumerable<Account> AccountsByOwner(Guid ownerId);
}
}

Then modify the AccountRepository file by adding one new method:

public IEnumerable<Account> AccountsByOwner(Guid ownerId)
{
return FindByCondition(a => a.OwnerId.Equals(ownerId)).ToList();
}

Finally, modify the DeleteOwner action in the OwnerController by adding one more validation before deleting the owner:

if(_repository.Account.AccountsByOwner(id).Any())
{
_logger.LogError($"Cannot delete owner with id: {id}. It has related accounts. Delete those accounts first");
return BadRequest("Cannot delete owner. It has related accounts. Delete those accounts first");
}

So, that is it. Send the Delete request from Postman and see the result. The owner object should be deleted from the database.

We have created these actions that use Repository Pattern logic synchronously but it could be done asynchronously as well. If you want to learn how to do that you can visit Implementing Async Repository in .NET Core. Although we strongly recommend finishing all the parts from this series for an easier understanding of the project’s business logic.

Also, in all these actions we are using try-catch blocks to handle our errors. You can do that in a more readable and maintainable way by introducing the Global Error Handling feature.

Conclusion

Now that you know all of this, try to repeat all the actions but for the Account entity. Because nothing beats the practice, doesn’t it? 😉

With all this code in place, we have a working web API that covers all the features for handling the CRUD operations.

By reading this post you’ve learned:

  • The way to handle the POST request
  • How to handle PUT request
  • How to write better and more reusable code
  • And the way to handle DELETE request

Thank you for reading and I hope you found something useful in it.

 

 

 

 

Saturday, August 27, 2022

Content Negotiation in Web API

Content negotiation is the process of selecting the best resource for a response when multiple resource representations are available. Content negotiation is an HTTP feature that has been around for a while, but for one reason or another, it is, maybe, a bit underused.

In short, content negotiation lets you choose or rather “negotiate” the content you want to get in response to the REST API request.

 

Let’s dive in.

How Does Content Negotiation Work?

Content negotiation happens when a client specifies the media type it wants as a response to the request Accept header. By default, ASP.NET Core Web API returns a JSON formatted result and it will ignore the browser Accept header.

ASP.NET Core supports the following media types by default:

  • application/json
  • text/json
  • text/plain

To try this out let’s create a default Web API project and simple model for a BlogPost:

public class BlogPost
{
public string Title { get; set; }
public string MetaDescription { get; set; }
public bool Published { get; set; }
}

And one class that lists all the blog posts called Blog:

public class Blog
{
public string Name { get; set; }
public string Description { get; set; }
public List<BlogPost> BlogPosts { get; set; }
public Blog()
{
BlogPosts = new List<BlogPost>();
}
}

We are going to create a controller called BlogController with only one method Get() that ads one blog post and one blog and returns them as a result:

[Route("api/blog")]
public class BlogController : Controller
{
public IActionResult Get()
{
var blogs = new List<Blog>();
var blogPosts = new List<BlogPost>();
blogPosts.Add(new BlogPost
{
Title = "Content Negotiation in Web API",
MetaDescription = "Content negotiation is the process of selecting the best resource for a response when multiple resource representations are available.",
Published = true
});
blogs.Add(new Blog()
{
Name = "Code Maze",
Description = "C#, .NET and Web Development Tutorials",
BlogPosts = blogPosts
});
return Ok(blogs);
}
}

Content negotiation is implemented by ObjectResult and the Ok() method inherits from OkObjectResult that inherits from ObjectResult. That means our controller method is able to return the content negotiated response.

Although the object creation logic is in the controller. You should not implement your controllers like this. This is just for the sake of simplicity. To learn more of the best practices you can read our article on 10 Things You Should Avoid in Your ASP.NET Core Controllers or our article on  Top REST API best practices article

We are returning the result with the Ok() helper method which returns the object and the status code 200 OK all the time.

Return the Default JSON Response

If we run our application right now, we’ll get a JSON response by default when run in a browser:

[
{
"name": "Code Maze",
"description": "C#, .NET and Web Development Tutorials",
"blogPosts": [
{
"title": "Content Negotiation in Web API",
"metaDescription": "Content negotiation is the process of selecting the best resource for a response when multiple resource representations are available.",
"published": true
}
]
}
]

For the sake of testing the responses properly we’re going to use Postman:

You can clearly see that the default result when calling GET on /api/blog returns our JSON result. Those of you with sharp eyes might have even noticed that we used the Accept header with application/xml to try forcing the server to return other media types like plain text and XML.

But that doesn’t work. Why?

Because we need to configure server formatters to format a response the way we want it.

Let’s see how to do that.

Return XML Response

A server does not explicitly specify where it formats a response to JSON. But we can override it by changing configuration options through the AddControllers() method options. By default, it can be found in the Program class and it looks like this:

builder.Services.AddControllers();

We can add the following options to enable the server to format the XML response when the client tries negotiating for it:

builder.Services.AddControllers(options =>
{
options.RespectBrowserAcceptHeader = true;
}).AddXmlSerializerFormatters();

First things first, we must tell a server to respect the Accept header. After that, we just add the AddXmlSerializerFormatters() method to support XML formatters.

Now that we have our server configured let’s test the content negotiation once more:

<ArrayOfBlog xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.datacontract.org/2004/07/ContentNegotiation.Models">
<Blog>
<BlogPosts>
<BlogPost>
<MetaDescription>Content negotiation is the process of selecting the best resource for a response when multiple resource representations are available.</MetaDescription>
<Published>true</Published>
<Title>Content Negotiation in Web API</Title>
</BlogPost>
</BlogPosts>
<Description>C#, .NET and Web Development Tutorials</Description>
<Name>Code Maze</Name>
</Blog>
</ArrayOfBlog>

Let’s see what happens now if we fire the same request through Postman:

There is our XML response, the response is no longer a default one. By changing the Accept header we can get differently formatted responses.

But what if despite all this flexibility a client requests a media type that a server doesn’t know how to format?

Restricting Media Types in Content Negotiation

Currently, the response will default to JSON if the media type is not recognized.

But we can restrict this behavior by adding one line to the configuration:

builder.Services.AddControllers(options =>
{
options.RespectBrowserAcceptHeader = true;
options.ReturnHttpNotAcceptable = true;
}).AddXmlSerializerFormatters();

We’ve added the ReturnHttpNotAcceptable = true option, which tells the server that if the client tries to negotiate for the media type the server doesn’t support, it should return the 406 Not Acceptable status code.

This will make your application more restrictive and force the API consumer to request only the types the server supports. The 406 status code is created for this purpose. You can find more details about that in our HTTP Reference article, or if you want to go even deeper you can check out the RFC2616.

Now, let’s try fetching the text/css media type using Postman to see what happens:

And as expected, there is no response body, and all we get is a nice 406 Not Acceptable status code.

Custom Formatters in ASP.NET Core

Let’s imagine we are making a public REST API and it needs to support content negotiation for a type that is not “in the box”.

ASP.NET Core supports the creation of custom formatters. Their purpose is to give you the flexibility to create your own formatter for any media types you need to support.

We can make the custom formatter using the following method:

  • Create an output formatter class that inherits the TextOutputFormatter class
  • Create an input formatter class that inherits the TextInputformatter class
  • Add input and output classes to InputFormatters and OutputFormatters collections the same way as we did for the XML formatter

Let’s implement a custom CSV output formatter for our example.

Implementing a Custom Formatter in ASP.NET Core

Since we are only interested in formatting responses in this article, we need to implement only an output formatter. We would need an input formatter only if a request body contained a corresponding type.

The idea is to format a response to return the list of blogs and their corresponding list of blog posts in a CSV format.

Let’s add a CsvOutputFormatter class to our project:

public class CsvOutputFormatter : TextOutputFormatter
{
public CsvOutputFormatter()
{
SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/csv"));
SupportedEncodings.Add(Encoding.UTF8);
SupportedEncodings.Add(Encoding.Unicode);
}
protected override bool CanWriteType(Type? type)
=> typeof(Blog).IsAssignableFrom(type)
|| typeof(IEnumerable<Blog>).IsAssignableFrom(type);
public override async Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
{
var response = context.HttpContext.Response;
var buffer = new StringBuilder();
if (context.Object is IEnumerable<Blog>)
{
foreach (var Blog in (IEnumerable<Blog>)context.Object)
{
FormatCsv(buffer, Blog);
}
}
else
{
FormatCsv(buffer, (Blog)context.Object);
}
await response.WriteAsync(buffer.ToString(), selectedEncoding);
}
private static void FormatCsv(StringBuilder buffer, Blog blog)
{
foreach (var blogPost in blog.BlogPosts)
{
buffer.AppendLine($"{blog.Name},\"{blog.Description},\"{blogPost.Title},\"{blogPost.Published}\"");
}
}
}

There are a few things to note here.

In the constructor, we define which media type this formatter should parse as well as encodings.

The CanWriteType method is overridden, and it indicates whether or not the Blog type can be written by this serializer. The WriteResponseBodyAsync method constructs the response. And finally, we have the FormatCsv method that formats a response the way we want it.

The class is pretty straightforward to implement, and the main thing that you should focus on is the FormatCsv method logic.

Now, we just need to add the newly made CsvOutputFormatter to the list of OutputFormatters in the AddMvcOptions() method:

builder.Services.AddControllers(options =>
{
options.RespectBrowserAcceptHeader = true;
options.ReturnHttpNotAcceptable = true;
}).AddXmlSerializerFormatters()
.AddMvcOptions(options => options.OutputFormatters.Add(new CsvOutputFormatter()));

Now let’s run this and see if it actually works. This time we will put the application/csv as the value for the Accept header in a Postman request:

It works, our only entry is formatted as a CSV response.

There is a great page about custom formatters in ASP.NET Core if you want to learn more about them. 

Conclusion

In this blog post, we went through a concrete implementation of the content negotiation mechanism in an ASP.NET Core project. We have learned about formatters and how to make a custom one, and how to set them up in your project configuration as well.

We have also learned how to restrict an application only to certain content types, and not accept any others.