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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
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:
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
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
1
2
3
4
5
6
7
8
9
|
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:
1
2
3
4
5
6
7
8
9
10
11
|
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
// <auto-generated> This file has been auto generated by EF Core Power Tools. </auto-generated>
#nullable disable
using Microsoft.EntityFrameworkCore;
namespace FileUploadApi.Entities
{
public partial class SocialDbContext : DbContext
{
public SocialDbContext()
{
}
public SocialDbContext(DbContextOptions<socialdbcontext> options)
: base(options)
{
}
public virtual DbSet<post> Post { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<post>(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);
}
}
</post></post></socialdbcontext>
|
For our tutorial, we will have the Post class mapped with the Post Table under SocialDb Database, defined via the the EF Core DbContext:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
// <auto-generated> This file has been auto generated by EF Core Power Tools. </auto-generated>
#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:
1
2
3
|
"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
1
2
3
4
5
6
7
8
9
10
11
12
13
|
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
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:
1
2
3
4
5
6
7
8
9
10
|
using FileUploadApi.Requests;
using FileUploadApi.Response;
namespace FileUploadApi.Interfaces
{
public interface IPostService
{
Task SavePostImageAsync(PostRequest postRequest);
Task<PostResponse> 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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
|
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<PostResponse> 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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
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<PostsController> logger;
private readonly IPostService postService;
public PostsController(ILogger<PostsController> logger, IPostService postService)
{
this.logger = logger;
this.postService = postService;
}
[HttpPost]
[Route("")]
[RequestSizeLimit(5 * 1024 * 1024)]
public async Task<IActionResult> 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):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
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<SocialDbContext>(options => options.UseSqlServer(builder.Configuration.GetConnectionString("SocialDbConnectionString")));
builder.Services.AddTransient<IPostService, PostService>();
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:
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:
1
2
3
4
|
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:
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’:
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:
And below is the folder structure, see how the folders are appearing inside the wwwroot folder:
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:
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
- Logging with Serilog in ASP.NET Core Web API
- Secure Angular Site using JWT Authentication with ASP.NET Core Web API
- Localization in ASP.NET Core Web API
- Google reCAPTCHA v3 Server Verification in ASP.NET Core Web API
- Apply JWT Access Tokens and Refresh Tokens in ASP.NET Core Web API 6
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
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);
}
Thanks for your comment, yes I agree, better to keep it inside a using block.