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:

APIDescriptionRequest bodyRequest response
POST api/resourcesCreate a new resourceResourceCreated resource

We will end up with an endpoint:

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!
APIDescriptionRequest bodyRequest response
POST api/resources/batchCreate new resourcesArray of resourcesAn array of created resources and their statuses
To download the source code for this article, you can visit our GitHub repository.

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.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!