- Create a form with the following input
- Title of image (required)
- A file input that accept only "jpeg, png or gif". (required)
- Input Validation
- When submission is successful, redirect it to a page with unique id
- This page will show the title of the image and the image itself
- The url should be in the form of "/picture/{xxxx}" where xxxx is uploaded image id
- We are not using any databases, store the information about the uploaded images/title in a JSON format stored in disk.
- Create a form with the required input
- Serving Static Files
- Async Programming
- HttpContext
- Data Validation
- Storing image in disk
- Store the information in a JSON format stored in disk
- Display the form content
To get started, let's create a new project using the following command:
dotnet new web -n ImageUploaderThis command will create a new Minimal API project named MyMinimalApi.
Now, let's update the code in the Program.cs file to include a form with required input, instead of returning "Hello World" message. Replace the existing code with the following:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () =>
{
var html = @"
<html>
<body>
<h1>Image Uploader</h1>
<form method='POST' action='/submit' enctype='multipart/form-data'>
<label for='imageTitle'>Title of Image:</label>
<input type='text' id='imageTitle' name='imageTitle' required><br><br>
<label for='imageFile'>Image File (JPEG, PNG, GIF):</label>
<input type='file' id='imageFile' name='imageFile' accept='.jpeg, .jpg, .png, .gif' required><br><br>
<input type='submit' value='Submit'>
</form>
</body>
</html>
";
return Results.Content(html, "text/html");
});
app.MapPost("/submit", (HttpContext context) =>
{
// Access form data
var form = context.Request.Form;
var title = form["imageTitle"];
var imageFile = form.Files.GetFile("imageFile");
var responseHtml = $@"
<html>
<body>
<h1>Image Uploaded</h1>
<p>Title: {title}</p>
</body>
</html>
";
return Results.Content(responseHtml, "text/html");
});
app.Run();- The "/" route handles the GET request and returns an HTML form with fields for title and image.
- The "/submit" route handles the POST request when the form is submitted. It retrieves the form data and generates a response HTML showing the submitted data.
- Now, when you run the project using dotnet run, you should see the initial form when you navigate to http://localhost:5xxx in your browser.
I suggest using for this command to automatically restart whenever changes are detected in the source code.
dotnet watchWhen you sumbit the form you are redirected to the route mentionned in :
<form method='POST' action='/submit' enctype='multipart/form-data'>Note: This is not what we want to do, we shouldn't take an action, unless we are sure that the image format is valid.
You may wonder, we added this checks in the html, why do we need another check?!
You are right! While we have incorporated checks in the HTML form to validate the image format on the client side, it's crucial to emphasize that client-side validation alone is not sufficient. We must implement server-side checks as well
In the client-side, it's only limiting the available formats for image selection, it's essential to recognize that these checks can be bypassed or modified
We will apply some modification in the code:
- Remove the action from the form post request
- Separate the HTML in an index.html
- Add some css, make a logo for our project
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () =>
{
string html = File.ReadAllText("index.html");
return Results.Content(html, "text/html");
});
app.MapPost("/", () =>
{
// Todo:
// 1. Make input validation,
// if valid:
// 2. Store the image to upload it after redirection
// 3. redirect it to a page with unique id with this url format "/picture/{xxxx}"
// where xxxx is uploaded image id
});
app.Run();Go copy index.html and style.css, add them in the same directory of program.cs.
The interface remains unstyled, without any updated logo :(

You should now be able to see this error, the server is not able to locate the resources although they are here, in our project directory.
By default, static files (These files include HTML files, CSS stylesheets, JavaScript files, images, fonts, and other static assets) are served only from the web root directory and its sub-directories.
Create a directory called wwwroot, by default this item content is added to .csproj. If it's not configured by default, add it by yourself.
<ItemGroup>
<Content Include="wwwroot\**" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>Move your assets directory containing your logos and style.css to wwwroot.
By calling this method in Program.cs, static files are enabled to be served.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.UseStaticFiles();
app.MapGet("/", () =>
{
string html = File.ReadAllText("./index.html");
return Results.Content(html, "text/html");
});
app.MapPost("/", () =>
{
// Todo:
// 1. Make input validation,
// if valid:
// 2. Store the image to upload it after redirection
// 3. redirect it to a page with unique id with this url format "/picture/{xxxx}"
// where xxxx is uploaded image id
});
app.Run();Serving Static files from wwwroot directory.
Using asynchronous file operations is generally recommended to improve the responsiveness of our application, especially when dealing with potentially long-running I/O operations.
- It allows your program to continue executing other tasks while waiting for the asynchronous operation to complete.
We can use the ReadAllTextAsync method instead of ReadAllText. Here's an updated version of your code using asynchronous file reading:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.UseStaticFiles();
app.MapGet("/", async () =>
{
string html = await File.ReadAllTextAsync("./index.html");
return Results.Content(html, "text/html");
});The HttpContext object represents the context of an HTTP request and provides access to various properties and methods related to the current request and response. It allows you to access information about the incoming HTTP request, such as headers, query parameters, cookies, and form data (what we care about).
Before diving into form data handling, let's explore the HttpContext.Response object. While it shares functionalities with the Results class, there is a notable distinction that sets them apart.
we can write this function:
app.MapGet("/", async () =>
{
string html = await File.ReadAllTextAsync("./index.html");
return Results.Content(html, "text/html");
});as:
app.MapGet("/", async context =>
{
string html = await File.ReadAllTextAsync("./index.html");
await context.Response.WriteAsync(html);
});-
The context.Response.WriteAsync(formContent) method is used to asynchronously write the provided content directly to the response body stream. It allows you to manually write the response content in a custom format or structure.
-
On the other hand, Results.Content(html, "text/html") is a helper method that generates an IResult object representing the response content. The IResult interface defines a contract for generating an HTTP response. In this case, Results.Content creates an IResult that will return the specified html content with a MIME type of "text/html". The IResult object can be returned from the handler to indicate the desired response content and type.
The main difference between using context.Response.WriteAsync and Results.Content is the level of abstraction
To get a deeper understanding on HttpsContext.Request, we will do it with some code and debug.
Let's add a break point in this post request and check our variables by running the debugger.
- The debugger should stop after submitting the form
This is how the context variable looks like:
As we said, we are focused on Request and Response

Notice that the Form is null in the response object although I fill it :(
- And here is why we should use any async function with precautions, we should add await for not continue to the next line before actually reading all the data from the form.
app.MapPost("/", async (HttpContext context) =>
{
var request = await context.Request.ReadFormAsync();
});Form is no longer null, and data is passed to the request correctly.
The highlighted data is important, we will use them later
Note that the HttpContext object is available only in the scope of the request being processed. Each request will have its own unique HttpContext instance, allowing you to handle multiple concurrent requests independently.
app.MapPost("/", async (HttpContext context) =>
{
var form = await context.Request.ReadFormAsync();
var title = form["imageTitle"];
var imgFile = form.Files.GetFile("imageFile");
// Check for title
if (form.Keys.Count == 0 || string.IsNullOrEmpty(title))
{
return Results.BadRequest(new { error = "Empty title string"});
}
// Check for file
if (form.Files.Count == 0 || imgFile is null)
{
return Results.BadRequest(new { error = "No file uploaded"});
}
// Check for extension
string imgName = Path.GetFileName(imgFile.FileName);
string extension = Path.GetExtension(imgName).ToLower();
if (extension != ".png" && extension != ".jpg" && extension != ".jpeg" && extension != ".gif")
{
return Results.BadRequest(new { error = "Invalid uploaded format"});
}
return Results.Ok();
});Images are stored with new name(a generated ID).
Store all images in a new directory called uploads.
string imageID = Guid.NewGuid().ToString();
string targetDirectory = Path.Combine(Directory.GetCurrentDirectory(), "uploads");
string targetFilePath = Path.Combine(targetDirectory, imageID + extension);
if (!Directory.Exists(targetDirectory))
{
Directory.CreateDirectory(targetDirectory);
}
using (var stream = new FileStream(targetFilePath, FileMode.Create))
{
await imgFile.CopyToAsync(stream);
}The using statement ensures that the file stream is properly closed and disposed of after the image file is copied to the target location.
- include this ->
using System.Text.Json;
string jsonFile = Path.Combine(Directory.GetCurrentDirectory(), "data.json");
// Create a new ImageDetails class for easy Serialization and Deseralization
var imageDetails = new ImageDetails
{
ID = imageID,
Title = title.ToString(),
Path = targetFilePath
};
var options = new JsonSerializerOptions
{
WriteIndented = true
};ImageDetails class
public class ImageDetails
{
public string? ID { get; set; }
public string? Title { get; set; }
public string? Path { get; set; }
}You can't make this by keeping appending to the file by every JSON object, as it won't keep the format of JSON list, which will cause an exception during deserialization.
{
ID = "63471deb-b0f5-4d77-b58c-a0590c7af8cb",
Title = "Image 1",
Path = "/path/to/63471deb-b0f5-4d77-b58c-a0590c7af8cb.jpg"
}
{
ID = "64f8bd4e-f004-4ab3-99b8-8a2e055225cf",
Title = "Image 2",
Path = "/path/to/64f8bd4e-f004-4ab3-99b8-8a2e055225cf.jpg"
}
[
{
ID = "63471deb-b0f5-4d77-b58c-a0590c7af8cb",
Title = "Image 1",
Path = "/path/to/63471deb-b0f5-4d77-b58c-a0590c7af8cb.jpg"
}
{
ID = "64f8bd4e-f004-4ab3-99b8-8a2e055225cf",
Title = "Image 2",
Path = "/path/to/64f8bd4e-f004-4ab3-99b8-8a2e055225cf.jpg"
}
]
So we will need to get all the data from the file, append to them the new object, then write all back at once.
string jsonFile = Path.Combine(Directory.GetCurrentDirectory(), "data.json");
var imageDetails = new ImageDetails
{
ID = imageID,
Title = title.ToString(),
Path = targetFilePath
};
var options = new JsonSerializerOptions
{
WriteIndented = true
};
var imageList = new List<ImageDetails>();;
bool fileExists = File.Exists(jsonFile);
if (fileExists)
{
string json = await File.ReadAllTextAsync(jsonFile);
imageList = JsonSerializer.Deserialize<List<ImageDetails>>(json);
}
imageList.Add(imageDetails);
string updatedJson = JsonSerializer.Serialize(imageList, options);
await File.WriteAllTextAsync(jsonFile, updatedJson);Return an HTTP redirect response with the imageID
return Results.Redirect($"/picture/{imageID}");Let's move to last step(last problem xD)
One last problem is showing the uploaded image in the new page
Let's write our startup code
app.MapGet("/picture/{id}", async (string id) =>
{
string jsonFile = Path.Combine(Directory.GetCurrentDirectory(), "data.json");
string json = await File.ReadAllTextAsync(jsonFile);
List<ImageDetails>? imageList = JsonSerializer.Deserialize<List<ImageDetails>>(json);
var image = imageList.FirstOrDefault(i => i.ID == id);
if (image != null)
{
var html = $@"
<html>
<head></head>
<body>
<h2>Title: {image.Title}</h2>
<img src=""{image.Path}"" alt=""{image.Title}"" />
<br/><br/>
<button class=""goback-btn"" onclick=""window.location.href='/';"">Go Back</button>
</body>
</html>
";
return Results.Content(html, "text/html");
}
else
{
return Results.StatusCode(404);
}
});The very first approach that you may think of, is deserialize data.json, LINQ the ImageDetails having the same id as follow:
var image = imageList.FirstOrDefault(i => i.ID == id);We have previously mentioned a similar problem when we were trying to upload our logo and the style.css and we said that by default ASP.NET only serve static files located in wwwroot, not in a local resource.
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(
Path.Combine(Directory.GetCurrentDirectory(), "uploads")),
RequestPath = "/uploads"
});- include this ->
using Microsoft.Extensions.FileProviders;
Using this configuration, you can serve files from the uploads directory in your ASP.NET Core application using the /uploads URL prefix.
- Don't forget to change the path being stored in data.JSON to:
Path = $"/uploads/{imageID}{extension}"
Not the actual path of the uploaded file
And here is the final function
app.MapGet("/picture/{id}", async (string id) =>
{
string jsonFile = Path.Combine(Directory.GetCurrentDirectory(), "data.json");
string json = await File.ReadAllTextAsync(jsonFile);
List<ImageDetails>? imageList = JsonSerializer.Deserialize<List<ImageDetails>>(json);
var image = imageList.FirstOrDefault(i => i.ID == id);
if (image != null)
{
var html = $@"
<html>
<head>
<style>
body {{
font-family: Arial, sans-serif;
background-color: #f2f2f2;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
padding: 20px;
}}
.content {{
text-align: center;
}}
h2 {{
color: #333;
margin-bottom: 20px;
}}
img {{
max-width: 500px;
height: auto;
margin-bottom: 20px;
}}
.goback-btn {{
padding: 10px 20px;
background-color: #333;
color: #fff;
text-decoration: none;
border: none;
border-radius: 4px;
cursor: pointer;
}}
.goback-btn:hover {{
background-color: #555;
}}
</style>
</head>
<body>
<div class=""content"">
<h2>Title: {image.Title}</h2>
<img src=""{image.Path}"" alt=""{image.Title}"" />
<br/><br/>
<button class=""goback-btn"" onclick=""window.location.href='/';"">Go Back</button>
</div>
</body>
</html>
";
return Results.Content(html, "text/html");
}
else
{
return Results.StatusCode(404);
}
});app.MapGet("/picture/{id}", async (string id) => {}
Think of how you can pass {image.Path} and {image.Title} to that file.













