In this article, we are going to create a minimal ASP.NET Core 6 Web API that supports the creation of multiple resources via a POST request. Network communication can be (or is) one of the bottlenecks in the microservice architecture, so creating multiple resources at once can save a large number of roundtrips.
Instead of having a ‘standard’ endpoint that creates a single resource:
API | Description | Request body | Request response |
---|---|---|---|
POST api/resources | Create a new resource | Resource | Created resource |
We will end up with an endpoint:
API | Description | Request body | Request response |
---|---|---|---|
POST api/resources/batch | Create new resources | Array of resources | An array of created resources and their statuses |
Let’s start
Does Creating Multiple Resources Conform to REST?
REST principles don’t say anything specific, so we should be fine if we conform to the REST constraints. However, supporting the multiple creations introduces a certain complexity and questions.
Should we process all resources as a single operation or not, i.e., do we want a request to fail if at least one resource fails, or do we want to return separate status codes for each resource. The former has a more straightforward implementation since our response will only have one status for all resources. The latter means we need to return each resource status or possible error message to conform to the REST constraints.
Also, it is debatable what the endpoint should look like.
We could use the ‘standard’ POST api/resources
endpoint to support multiple resource creation. Instead of having one resource in the request, we could have an array of resources. We could also support single creation by providing one resource in the array. If we build an API from scratch and know that our consumers will create multiple resources as a standard business flow, this could be one way of designing it.
Another way of doing it is to have a different POST api/resources/batch
endpoint for batch creation. If we deal with existing APIs or want to explicitly support both operations, having a different endpoint is another approach we could use.
ASP.NET 6 Web API Example
Let’s prepare a web API to create and get some books for some imaginary web book store. Since our book store will receive books in batches, our API needs to be able to create multiple books in the web store. Also, API shouldn’t process multiple books as a single operation since this could reduce its usability.
In the end, we will end up having one endpoint that supports our requirements: POST api/books/batch
.
The prerequisite to follow along with the article is having Visual Studio 2022 with the ASP.NET and web development workload.
Creating a Minimal Web API that Supports Multiple Resources
To create a minimal Web API project, we are going to select a usual ASP.NET Core Web API template but uncheck the Use controllers
and Enable OpenAPI
support options in the Additional information dialog.
After the project creation, let’s remove the weatherforecast
code in the Program
class:
var builder = WebApplication.CreateBuilder(args); // Add services to the container. var app = builder.Build(); // Configure the HTTP request pipeline. app.UseHttpsRedirection(); app.Run();
Also, let’s modify the launchSettings.json
file by setting the launchBrowser
property to false
.
To keep our implementation as simple as possible, we are going to use an in-memory database. Let’s start by adding the Microsoft.EntityFrameworkCore.InMemory
NuGet package:
Install-Package Microsoft.EntityFrameworkCore.InMemory
Then, let’s create a new ApiContext
class inside the Infrastracture
folder in the root of the project:
public class ApiContext : DbContext { public DbSet<BookModel> BookModels { get; set; } public ApiContext(DbContextOptions<ApiContext> options) : base(options) {} protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<BookModel>(entity => entity.Property(p => p.Isbn).IsRequired() ); } }
And finally, let’s register DbContext
in the Program
class to use the in-memory database:
var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddDbContext<ApiContext>(opt => opt.UseInMemoryDatabase("api")); var app = builder.Build(); // Configure the HTTP request pipeline. app.UseHttpsRedirection(); app.Run();
We are ready to implement endpoints in the API.
Adding Endpoints to the API
Please note that we are going to keep things simple in the next implementation and we are not going to complicate things with concepts like async
, await
and Tasks
. Yet, we always recommend async
await
when creating APIs that have heavy IO or CPU-bound operations like communicating with the database. If you want, you can read more about the async-await implementation in our Asynchronous Programming article.
Let’s first add models that will be used in our API. All records are in the Models
folder in our project:
public record BookRequest(string Name, string Isbn); public record BookModel(int Id, string Name, string Isbn); public record MultipleBooksBase(string Status); public record MultipleBooksErrorModel(string Status, string Message): MultipleBooksBase(Status); public record MultipleBooksModel(string Status, BookModel Message) : MultipleBooksBase(Status);
BookRequest
is a record that API should receive in POST
requests.
Since API is going to return a status code for each requested book in the response, we create MultipleBooksBase
record. MultipleBooksModel
and MultipleBooksErrorModel
inherits from that base class, so the IBookService
service can return different records as a result:
public interface IBookService { IEnumerable<MultipleBooksBase> CreateBooks(IEnumerable<BookRequest> bookRequests); }
We are going to place the IBookService
interface in the Services
folder along with its BookService
implementation:
public class BookService : IBookService { private readonly ApiContext _context; public BookService(ApiContext context) { _context = context; } public IEnumerable<MultipleBooksBase> CreateBooks(IEnumerable<BookRequest> bookRequests) { var books = new List<MultipleBooksBase>(); foreach (var bookRequest in bookRequests) { try { var bookModel = _context.Add(new BookModel(0, bookRequest.Name, bookRequest.Isbn)); _context.SaveChanges(); books.Add(new MultipleBooksModel("201", bookModel.Entity)); } catch (Exception) { books.Add(new MultipleBooksErrorModel("500", "Internal error while creating a resource")); _context.ChangeTracker.Clear(); } } return books; } }
We can see that in the CreateBooks
method implementation, we create MultipleBooksModel
record with the 201
created status on success, and in case of any error, we create MultipleBooksErrorModel
record. For the example sake, it will always contain 500
status with Internal error while creating a resource
message on all errors. But of course, you can/should modify that to fit your needs. In our Ultimate ASP.NET Core Web API (second edition) book, you can read about different ways to handle errors from the service layer.
Now, we are going to amend our Program.cs
with configured endpoints:
var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddDbContext<ApiContext>(opt => opt.UseInMemoryDatabase("api")); builder.Services.AddScoped<IBookService, BookService>(); var app = builder.Build(); // Configure the HTTP request pipeline. app.UseHttpsRedirection(); app.MapPost("/api/books/batch", (BookRequest[] bookRequests, IBookService bookService) => { var createdBooks = bookService.CreateBooks(bookRequests); return createdBooks.Select(x => (object)x); }); app.Run();
The Program
class shows the beauty of the minimal API. In order to have our POST API endpoint exposed we only need to call a MapPost
method on the app
variable where we provide the route pattern and a delegate that will be called when the route is hit.
Running the API
Let’s run our API and create some books. We are going to send three books to the api/books/batch
where one of them will be invalid, i.e., it will miss Isbn
property. To accomplish that, we are going to run our project and use Postman to send a POST request to the https://localhost:7255/api/books/batch
endpoint:
[ { "name":"book1", "isbn":"123456" }, { "name":"book2" }, { "name":"book3", "isbn":"456789" } ]
After we send the request, we can inspect the response that we receive from our API:
[ { "message": { "id": 1, "name": "book1", "isbn": "123456" }, "status": "201" }, { "message": "Internal error while creating a resource", "status": "500" }, { "message": { "id": 3, "name": "book3", "isbn": "456789" }, "status": "201" } ]
The Postman response has the 200
status code. But as we return the status for each book, we could make the endpoint even better by returning a more appropriate 207 Multi-status status code.
Let’s modify MapPost
in the Program
class and show one way of doing it:
app.MapPost("/api/books/batch", (BookRequest[] bookRequests, IBookService bookService, HttpResponse response) => { var createdBooks = bookService.CreateBooks(bookRequests); response.StatusCode = StatusCodes.Status207MultiStatus; return createdBooks.Select(x => (object)x); }); app.Run();
If we run the same request as before we are going to see the desired 207 Multi-status
code of the response.
Conclusion
In this article, we talked about creating multiple resources in the API. Although there are several approaches, we have covered one and saw a basic example implementation.