File Upload with Data using ASP.NET Core Web API

Last Updated on October 14, 2022 by Aram

Most of the web or mobile forms like signup or submit a post include a file upload with form data to the API for further processing for saving into database. Of course, you won’t be saving the file itself into the database but you will be saving the file into some storage location, on a server or a cloud storage and then you will save the file path into your database table.

Let’s say you have some social media platform, and you want to let your users create a post with a description, and an image, so in this tutorial we will how to build an endpoint that will take the user’s submitted post containing the image and data, validate them, save the image into a physical storage and the rest of data along with the relative path of the saved image to be persisted into a SQL Server relational database.

Follow this tutorial to learn how to implement file upload with data using ASP.NET Core Web API.

Make sure you are using the latest version of Visual Studio alongside the latest stable version of .NET which is .NET 6, and for this tutorial we will require to use SQL Server Express, SQL Server Management Studio and for testing we will use Postman.

Database – SQL Server Express

Let’s first start by creating our database and the required table for this tutorial.

We will a database with name ‘SocialDb’ , why? Because we are trying to showcase how can a user create a Post from a social media site and this post will be containing the post description and an uploaded Image.

So start by opening MS SQL Service Management Studio and then connect to your local machine or whatever setup you have.

From the Database explorer on the left panel, right-click and choose New Database, and input SocialDb as name:

Then press Ctrl + N to create a new query tab, inside it add the below script to create the Post Table:

USE [SocialDb]
GO
/****** Object:  Table [dbo].[Post]    Script Date: 9/20/2022 12:22:30 AM ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[Post](
	[Id] [int] IDENTITY(1,1) NOT NULL,
	[UserId] [int] NOT NULL,
	[Description] [nvarchar](1000) NOT NULL,
	[Imagepath] [nvarchar](255) NULL,
	[TS] [smalldatetime] NOT NULL,
	[Published] [bit] NOT NULL,
 CONSTRAINT [PK_Post] PRIMARY KEY CLUSTERED 
(
	[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO

After running the above script, you should see this in the databases explorer:

Created Database and Table to persist the Post data along with the Image Path

Note, because this is a targeted tutorial about File Upload with Data in ASP.NET Core Web API and just to stay focused on the topic, there is no other table or data structure, but in the usual social-media related business scenarios you will have many more database and relationships such as User, PostDetail, Page, Group … etc.

File Upload API in Visual Studio 2022

Open Visual Studio and create a new project, choose ASP.NET Core Web API

Give your project a name like ‘FileUploadApi’ , and then press next:

Keep all settings as default with .NET 6 as the framework then choose Create.

Wait until the project is loaded, then delete the template endpoint WeatherForecastController along with WeatherForecast class

Preparing the Request

From the solution explorer, on the project level, create a new folder with name Requests, and inside it create a new class with name PostRequest.

This will represent the object that will receive the request from the client, which will contain the post data alongside the uploaded image file:

using System.Text.Json.Serialization;

namespace FileUploadApi.Requests
{
    public class PostRequest
    {
        public int UserId { get; set; }
        public string Description { get; set; }
        public IFormFile Image { get; set; }
        [JsonIgnore(Condition = JsonIgnoreCondition.Always)]
        public string? ImagePath { get; set; }
    }
}

Note that the Image object is of type IFormFile , which is an interface representing the file or the image that will be sent over the Http Post Request.

Responses

Next comes preparing the DTO or the model that will contain the response that will be sent back to the client

So let’s create a new folder Responses and inside it we will add a class for the PostResponse and another class for the BaseResponse which is a base class for handling the general response of endpoints

Also we will have another subfolder for the Models that will be encapsulated inside the response:

BaseResponse
using System.Text.Json.Serialization;

namespace FileUploadApi.Response
{
    public class BaseResponse
    {
        [JsonIgnore(Condition = JsonIgnoreCondition.Always)]
        public bool Success { get; set; }
        [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
        public string ErrorCode { get; set; }
        [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
        public string Error { get; set; }
    }
}

PostResponse

using FileUploadApi.Response.Models;

namespace FileUploadApi.Response
{
    public class PostResponse : BaseResponse
    {
        public PostModel Post { get; set; }
    }
}

Models

PostModel , we will use this to return the saved post to the client, which will contain the id of the post as well as the physical saved location of the image provided with the post:

namespace FileUploadApi.Response.Models
{
    public class PostModel
    {
        public int Id { get; set; }
        public int UserId { get; set; }
        public string Description { get; set; }
        public string Imagepath { get; set; }
        public DateTime Ts { get; set; }
    }
}

Entities

The Entities folder will include all the ORM related classes, mappings as well as the DbContext.

Entity Framework Core

For this tutorial we will be connecting to the database created earlier through EntityFramework Core, so let’s make sure we get the EF Core Nuget packages to be able to connect and map to the database table:

To connect to SQL Server database, we will need both EntityFrameworkCore and EntityFrameworkCore.SqlServer packages as the below:

//  This file has been auto generated by EF Core Power Tools. 
#nullable disable
using Microsoft.EntityFrameworkCore;

namespace FileUploadApi.Entities
{
    public partial class SocialDbContext : DbContext
    {
        public SocialDbContext()
        {
        }

        public SocialDbContext(DbContextOptions options)
            : base(options)
        {
        }

        public virtual DbSet Post { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity(entity =>
            {
                entity.Property(e => e.Description)
                    .IsRequired()
                    .HasMaxLength(1000);

                entity.Property(e => e.Imagepath).HasMaxLength(255);

                entity.Property(e => e.Ts)
                    .HasColumnType("smalldatetime")
                    .HasColumnName("TS");

            });

            OnModelCreatingPartial(modelBuilder);
        }

        partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
    }
}

For our tutorial, we will have the Post class mapped with the Post Table under SocialDb Database, defined via the the EF Core DbContext:

//  This file has been auto generated by EF Core Power Tools. 
#nullable disable
using System;
using System.Collections.Generic;

namespace FileUploadApi.Entities
{
    public partial class Post
    {
        public int Id { get; set; }
        public int UserId { get; set; }
        public string Description { get; set; }
        public string Imagepath { get; set; }
        public DateTime Ts { get; set; }
        public bool Published { get; set; }
    }
}

The DbContext class will contain the definition for the Post as DbSet as well as the mapping configuration for each field.

Note that here we are using the UserId just as a reference that the post usually would be associated with a user.

So in the normal scenarios, you would also require a User table with foreign key relationship with the Post table, and you should also apply some sort of authentication for the user who will be creating the post that they have to be securely logged in and authorized to per form this action.

To have a full idea about the authentication and authorization, please take a look at my tutorial Apply JWT Access Tokens and Refresh Tokens in ASP.NET Core Web API 6.

appsettings.json

Here we will add the database connection string to our appsettings.json file, so open the file and add the below before or after the logging section:

"ConnectionStrings": {
    "SocialDbConnectionString": "Server=Home\\SQLEXPRESS;Database=SocialDb;Trusted_Connection=True;MultipleActiveResultSets=true"
  },

Requests

Below is the code for PostRequest class, it will be the container that will bind all the multipart form-data from the client to the API

using System.Text.Json.Serialization;

namespace FileUploadApi.Requests
{
    public class PostRequest
    {
        public int UserId { get; set; }
        public string Description { get; set; }
        public IFormFile Image { get; set; }
        [JsonIgnore(Condition = JsonIgnoreCondition.Always)]
        public string? ImagePath { get; set; }
    }
}

Helpers

The below helper class will be used to extract a unique filename from the provided file, by appending the 4 characters of a newly generated Guid:

namespace FileUploadApi.Helpers
{
    public class FileHelper
    {
        public static string GetUniqueFileName(string fileName)
        {
            fileName = Path.GetFileName(fileName);
            return string.Concat(Path.GetFileNameWithoutExtension(fileName)
                                , "_"
                                , Guid.NewGuid().ToString().AsSpan(0, 4)
                                , Path.GetExtension(fileName));
        }
    }
}

Interfaces

We will define 2 methods inside the IPostService interface:

using FileUploadApi.Requests;
using FileUploadApi.Response;
namespace FileUploadApi.Interfaces
{
    public interface IPostService
    {
        Task SavePostImageAsync(PostRequest postRequest);
        Task CreatePostAsync(PostRequest postRequest);
    }
}

Services

PostService includes the implementation for the 2 methods that we have in the above IPostService interface, one for saving the image into the physical storage and the other is to create the post inside the database through the EF Core SocialDbContext

The SavePostImageAsync method will take the postRequest Image object that is defined as IFormFile, extract a unique file name from it and then save it into the API’s local storage while creating the need subfolder

using FileUploadApi.Entities;
using FileUploadApi.Helpers;
using FileUploadApi.Interfaces;
using FileUploadApi.Requests;
using FileUploadApi.Response;
using FileUploadApi.Response.Models;
namespace FileUploadApi.Services
{
    public class PostService : IPostService
    {
        private readonly SocialDbContext socialDbContext;
        private readonly IWebHostEnvironment environment;
        public PostService(SocialDbContext socialDbContext, IWebHostEnvironment environment)
        {
            this.socialDbContext = socialDbContext;
            this.environment = environment;
        }
        public async Task CreatePostAsync(PostRequest postRequest)
        {
            var post = new Entities.Post
            {
                UserId = postRequest.UserId,
                Description = postRequest.Description,
                Imagepath = postRequest.ImagePath,
                Ts = DateTime.Now,
                Published = true
            };
            var postEntry = await socialDbContext.Post.AddAsync(post);
            var saveResponse = await socialDbContext.SaveChangesAsync();
            if (saveResponse < 0)
            {
                return new PostResponse { Success = false, Error = "Issue while saving the post", ErrorCode = "CP01" };
            }
            var postEntity = postEntry.Entity;
            var postModel = new PostModel
            {
                Id = postEntity.Id,
                Description = postEntity.Description,
                Ts = postEntity.Ts,
                Imagepath = Path.Combine(postEntity.Imagepath),
                UserId = postEntity.UserId
            };
            return new PostResponse { Success = true, Post = postModel };
        }
        public async Task SavePostImageAsync(PostRequest postRequest)
        {
            var uniqueFileName = FileHelper.GetUniqueFileName(postRequest.Image.FileName);
            
            var uploads = Path.Combine(environment.WebRootPath, "users", "posts", postRequest.UserId.ToString());
            
            var filePath = Path.Combine(uploads, uniqueFileName);
            
            Directory.CreateDirectory(Path.GetDirectoryName(filePath));
            await postRequest.Image.CopyToAsync(new FileStream(filePath, FileMode.Create));
            
            postRequest.ImagePath = filePath;
            return;
        }
    }
}

Controllers

Now after preparing all the core functionality for our File Upload API, we will build our controller to expose the endpoint for the file upload with data

Below is the controller code that includes the SubmitPost endpoint

using FileUploadApi.Interfaces;
using FileUploadApi.Requests;
using FileUploadApi.Response;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Mvc;
namespace FileUploadApi.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class PostsController : ControllerBase
    {
        private readonly ILogger logger;
        private readonly IPostService postService;
        public PostsController(ILogger logger, IPostService postService)
        {
            this.logger = logger;
            this.postService = postService;
        }
        [HttpPost]
        [Route("")]
        [RequestSizeLimit(5 * 1024 * 1024)]
        public async Task SubmitPost([FromForm] PostRequest postRequest)
        {
            if (postRequest == null)
            {
                return BadRequest(new PostResponse { Success = false, ErrorCode = "S01", Error = "Invalid post request" });
            }
            if (string.IsNullOrEmpty(Request.GetMultipartBoundary()))
            {
                return BadRequest(new PostResponse { Success = false, ErrorCode = "S02", Error = "Invalid post header" });
            }
            if (postRequest.Image != null)
            {
                await postService.SavePostImageAsync(postRequest);
            }
            var postResponse = await postService.CreatePostAsync(postRequest);
            if (!postResponse.Success)
            {
                return NotFound(postResponse);
            }
            return Ok(postResponse.Post);
        }
    }
}

The attribute RequestSizeLimit , from its name specifies the size of the request that is allowed to be posted on this endpoint, anything larger than the defined number, which in this case is 5 MB, will yield a 400 bad request.

The GetMultipartBoundary detects if the request has the Content-Type multipart/form-data header passed, which indicates that there is a file upload.

Program.cs

Lastly, we will have to apply some configurations in the program.cs file to include the dbcontext using the appsettings connection string , also we will define the dependency injection bindings for PostService through the interface IPostService.

And also don't forget to add a CORS policy to make sure you are allowing the correct origins/methods/headers to connect to your API, this tutorial only defines the localhost with port number as the origin (just to showcase its usage):

using FileUploadApi.Entities;
using FileUploadApi.Interfaces;
using FileUploadApi.Services;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
const string AllowAllHeadersPolicy = "AllowAllHeadersPolicy";
builder.Services.AddCors(options =>
{
    options.AddPolicy(AllowAllHeadersPolicy,
        builder =>
        {
            builder.WithOrigins("http://localhost:7296")
                    .AllowAnyMethod()
                   .AllowAnyHeader();
        });
});
// Add services to the container.
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddDbContext(options => options.UseSqlServer(builder.Configuration.GetConnectionString("SocialDbConnectionString")));
builder.Services.AddTransient();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();

To learn more about Cors in ASP.NET Core, follow this tutorial by Code Maze

Testing the API

Run your project to see the below swagger UI on your browser:

Swagger ui running on browser for our tutorial

Postman

If you don't have Postman, make sure you download it from here. It is an amazing tool for testing and simulating your APIs.

Open Postman and create a new request.

Your request cURL should look like the below:

curl --location --request POST 'https://localhost:7296/posts' \
--form 'Image=@"/C:/Users/user/Desktop/blog/codingsonata-logo.png"' \
--form 'Description="Coding Sonata"' \
--form 'UserId="123"'

And in Postman request editor you can do it as the below:

Choose POST, enter the endpoint URL, and from Body tab, choose form-data, and then start adding the Key, Value pairs as the below:

Testing the Create Post endpoint using Postman - part 1

Note related to Image Key, to make its type as File, you have to hover your mouse on field to bring up the small arrow from which you will choose 'File' instead of 'text':

Testing the Create Post endpoint using Postman - part 2

And checking the database table, you can see the record created under the Post table , with the Imagepath set to the physical location of the saved image:

Database table updated with a new record related to the Post submitted by the client

And below is the folder structure, see how the folders are appearing inside the wwwroot folder:

wwwroot folders structure after uploading and saving a file

Testing the RequestSizeLimit Validation

If we try to post some large file that exceeds the set request size which is 5 MB in our tutorial, it will throw a 400 bad request as mentioned previously in this tutorial, see the below screenshot for the repsonse details:

Testing with Postman - Sending Image larger that the request limit

Summary

So in this tutorial we learned how to implement a file upload with data using ASP.NET Core Web API. We built an API that will take input from client that includes both a File and data all contained inside a request object and passed via a POST Body, and then we processed the input to take the uploaded file and saved it in some storage location, while taking the path and filename and persisted it in a table with its associated records.

This is very useful whenever you are building a submit form or app screen that will require the user to fill some data alongside providing some image (like ID or profile picture) or any file to be associated with the data.

Finally, we managed to run some tests on localhost using Postman by mimicking a request with POST body passed as form-data in key-value pairs

You can find the source code published in my GitHub account.

For further reading about uploading files in ASP.NET Core Web API, check out Microsoft's official documentation.

Let me know in the comments section down if you have any question or note.

If you think this tutorial added some value, please share it using the social media buttons on the left side of this screen

If you want to learn more about ASP.NET Core Web API in .NET 6, please feel free to check my other tutorials

Bonus

Entertain your soul by the brilliant tones of Mozart's enchanting piano sonatas.

Mozart - Piano Sonata No. 5 K.283 (1775) - Played by Ingrid Haebler

2 Comments on “File Upload with Data using ASP.NET Core Web API”

  1. gwood says:

    Thank you for the awesome example.
    await postRequest.Image.CopyToAsync(new FileStream(filePath, FileMode.Create));
    Leaves the file stream open until application close.
    Instead:
    using (FileStream stream = new FileStream(filePath, FileMode.Create))
    {
    await postRequest.File.CopyToAsync(stream);
    }

    1. Aram says:

      Thanks for your comment, yes I agree, better to keep it inside a using block.

Leave a Reply