Image upload/CRUD operations in .net core APIs

Ravindra Devrani
6 min readApr 17, 2024
Upload images in .net web api
Upload images in .net core apis

In this article, we will learn how to upload/update/delete/read images in .net core APIs. I hate to give long and unnecessary intros, So let’s come to the point’s.

💻 You can get the code from this github repo.

What is the logic behind it? 🤔

When you upload the image (or any file), we will generate the unique image name with it’s extension (eg. uniquename.png). Save the image name in the database and save the file with the same name inside the project. In production apps, you can not simply upload files to the server, you have to give permission to that folder. Or you can use other file storage service, where you will save your files.

Why don’t I prefer to save image in database? 🤔

You can definitely save the image in the database in the binary format, base-64 string format. But it will increase the size of your database. Which can increase your bills and lead some performance issue.

Project Structure

I am assuming that, you know how to create a solution file, with multiple projects.

Solution name: ImageManipulation

Project 1: ImageManipulation.API (.net core web api)

Project 2: ImageManipulation.Data (Class Library)

⚠️Note: Don’t forget to add the reference of ImageManipulation.Data in the ImageManipulation.API project.

Models:

Create a class Product.cs

// ImageManipulation.Data/Models/Prodcut.cs
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ImageManipulation.Data.Models;

[Table("Product")]
public class Product
{
public int Id { get; set; }

[Required]
[MaxLength(30)]
public string? ProductName { get; set; }

[Required]
[MaxLength(50)]
public string? ProductImage { get; set; }
}

Add another class ApplicationDbContext.cs

// ImageManipulation.Data/Models/ApplicationDbContext.cs

using Microsoft.EntityFrameworkCore;

namespace ImageManipulation.Data.Models;

public class ApplicationDbContext: DbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options):base(options)
{

}

public DbSet<Product> Products { get; set; }
}

Nuget Packages

👉 In ImageManipulation.API

  • Microsoft.EntityFrameworkCore.Tools

👉 In ImageManipulation.Data

  • Microsoft.EntityFrameworkCore.SqlServer
  • Microsoft.Extensions.Configuration.Abstractions

appsettings.json file:

Add connection string in appsettings.json file

"ConnectionStrings": {
"default": "server=your_sql_server_instance_name;database=ImageManipulation;integrated security=true; encrypt=false"
},

Program.cs file

Update the program.cs file, with following code

builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("default"))
);

Migrations

Open package manage console (Tools->Nuget Package Manager->Package Manager Console) and run these command in a successive manner.

add-migration initialCreate
update-database

At this stage, we have successfully created our database.

DTOs:

// ImageManipulation.Data/Models/DTO/ProductDTO.cs

using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Http;
// NOTE: for using above namespace include this line in csproj
// of this class library :
//<FrameworkReference Include="Microsoft.AspNetCore.App" />

namespace ImageManipulation.Data.Models.DTO;
public class ProductDTO
{
[Required]
[MaxLength(30)]
public string? ProductName { get; set; }

[Required]
public IFormFile? ImageFile { get; set; }


}

public class ProductUpdateDTO
{

[Required]
public int Id { get; set; }

[Required]
[MaxLength(30)]
public string? ProductName { get; set; }

[Required]
[MaxLength(50)]
public string? ProductImage { get; set; }

public IFormFile? ImageFile { get; set; }
}

👉 IFormFile comes under the Microsoft.AspNetCore.Http namespace and this namespace does not included in project by default. You have to add it manually. So open the ImageManipulation.Data.csproj file or double click on ImageManipulation.Data project, which is shown in solution explorer. Add follwing line in the file inside the ItemGroup tag.

<FrameworkReference Include=”Microsoft.AspNetCore.App” />

// ImageManipulation.Data.csproj

//..... previous lines

<ItemGroup>
// ...... previous lines
// new line 👇
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
// ..... ..... previous lines

ProductRepository

// ImageManipulation.Data/Repositorie/ProductRepository.cs

using ImageManipulation.Data.Models;
using Microsoft.EntityFrameworkCore;

namespace ImageManipulation.Data.Repositories;

public interface IProductRepository
{
Task<Product> AddProductAsync(Product product);
Task<Product> UpdateProductAsync(Product product);
Task<IEnumerable<Product>> GetProductsAsync();
Task<Product?> FindProductByIdAsync(int id);
Task DeleteProductAsync(Product product);
}
public class ProductRepository(ApplicationDbContext context) : IProductRepository
{
public async Task<Product> AddProductAsync(Product product)
{
context.Products.Add(product);
await context.SaveChangesAsync();
return product; // returning created product, it will automatically fetch `Id`
}

public async Task<Product> UpdateProductAsync(Product product)
{
context.Products.Update(product);
await context.SaveChangesAsync();
return product;
}

public async Task DeleteProductAsync(Product product)
{
context.Products.Remove(product);
await context.SaveChangesAsync();
}

public async Task<Product?> FindProductByIdAsync(int id)
{
var product = await context.Products.FindAsync(id);
return product;
}

public async Task<IEnumerable<Product>> GetProductsAsync()
{
return await context.Products.ToListAsync();
}


}

FileService

// ImageManipulation.Data/Repositorie/.cs

using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;

namespace ImageManipulation.Data.Services;

public interface IFileService
{
Task<string> SaveFileAsync(IFormFile imageFile, string[] allowedFileExtensions);
void DeleteFile(string fileNameWithExtension);
}

public class FileService(IWebHostEnvironment environment) : IFileService
{

public async Task<string> SaveFileAsync(IFormFile imageFile, string[] allowedFileExtensions)
{
if (imageFile == null)
{
throw new ArgumentNullException(nameof(imageFile));
}

var contentPath = environment.ContentRootPath;
var path = Path.Combine(contentPath, "Uploads");
// path = "c://projects/ImageManipulation.Ap/uploads" ,not exactly, but something like that

if (!Directory.Exists(path))
{
Directory.CreateDirectory(path);
}

// Check the allowed extenstions
var ext = Path.GetExtension(imageFile.FileName);
if (!allowedFileExtensions.Contains(ext))
{
throw new ArgumentException($"Only {string.Join(",", allowedFileExtensions)} are allowed.");
}

// generate a unique filename
var fileName = $"{Guid.NewGuid().ToString()}{ext}";
var fileNameWithPath = Path.Combine(path, fileName);
using var stream = new FileStream(fileNameWithPath, FileMode.Create);
await imageFile.CopyToAsync(stream);
return fileName;
}


public void DeleteFile(string fileNameWithExtension)
{
if (string.IsNullOrEmpty(fileNameWithExtension))
{
throw new ArgumentNullException(nameof(fileNameWithExtension));
}
var contentPath = environment.ContentRootPath;
var path = Path.Combine(contentPath, $"Uploads", fileNameWithExtension);

if (!File.Exists(path))
{
throw new FileNotFoundException($"Invalid file path");
}
File.Delete(path);
}

}

=> Above code for uploading file is simple, according to logic, which I have explained in the starting, If you haven’t read it, please read it first. File will be saved in Uploads folder, in the root directory.

Program.cs

Add these lines.

// .... previous lines
builder.Services.AddTransient<ApplicationDbContext>();
builder.Services.AddTransient<IProductRepository, ProductRepository>();
builder.Services.AddTransient<IFileService, FileService>();
builder.Services.AddCors(options =>
{
options.AddDefaultPolicy(
policy =>
{
policy.WithOrigins("*").AllowAnyMethod().AllowAnyHeader(); ;
});
});
// .... other lines

// mapping Uploads folder to Resources folder
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(
Path.Combine(builder.Environment.ContentRootPath, "Uploads")),
RequestPath = "/Resources"
});

app.UseCors(); // for enabling cross origin requests

// .... other lines

Actual file path is basePath/uploads/imageName.extension

Our files will be saved in Uploads folder, but will not expose this name to the client. But, we have mapped Uploads to Resources. So file path for client will be basePath/resources/imageName.extension

Controllers

//ImageManipulation.API/Controllers/ProductController.cs

using ImageManipulation.Data.Models;
using ImageManipulation.Data.Models.DTO;
using ImageManipulation.Data.Repositories;
using ImageManipulation.Data.Services;
using Microsoft.AspNetCore.Mvc;

namespace ImageManipulation.API.Controllers;

[Route("api/products")]
[ApiController]
public class ProductController(IFileService fileService, IProductRepository productRepo, ILogger<ProductController> logger) : ControllerBase
{
[HttpPost]
public async Task<IActionResult> CreateProduct([FromForm] ProductDTO productToAdd)
{
try
{
if (productToAdd.ImageFile?.Length > 1 * 1024 * 1024)
{
return StatusCode(StatusCodes.Status400BadRequest, "File size should not exceed 1 MB");
}
string[] allowedFileExtentions = [".jpg", ".jpeg", ".png"];
string createdImageName = await fileService.SaveFileAsync(productToAdd.ImageFile, allowedFileExtentions);

// mapping `ProductDTO` to `Product` manually. You can use automapper.
var product = new Product
{
ProductName = productToAdd.ProductName,
ProductImage = createdImageName
};
var createdProduct = await productRepo.AddProductAsync(product);
return CreatedAtAction(nameof(CreateProduct), createdProduct);
}
catch (Exception ex)
{
logger.LogError(ex.Message);
return StatusCode(StatusCodes.Status500InternalServerError, ex.Message);
}
}


// api/products/1
[HttpPut("{id}")]
public async Task<IActionResult> UpdateProduct(int id, [FromForm] ProductUpdateDTO productToUpdate)
{
try
{
if (id != productToUpdate.Id)
{
return StatusCode(StatusCodes.Status400BadRequest, $"id in url and form body does not match.");
}

var existingProduct = await productRepo.FindProductByIdAsync(id);
if (existingProduct == null)
{
return StatusCode(StatusCodes.Status404NotFound, $"Product with id: {id} does not found");
}
string oldImage = existingProduct.ProductImage;
if (productToUpdate.ImageFile != null)
{
if (productToUpdate.ImageFile?.Length > 1 * 1024 * 1024)
{
return StatusCode(StatusCodes.Status400BadRequest, "File size should not exceed 1 MB");
}
string[] allowedFileExtentions = [".jpg", ".jpeg", ".png"];
string createdImageName = await fileService.SaveFileAsync(productToUpdate.ImageFile, allowedFileExtentions);
productToUpdate.ProductImage = createdImageName;
}

// mapping `ProductDTO` to `Product` manually. You can use automapper.
existingProduct.Id = productToUpdate.Id;
existingProduct.ProductName = productToUpdate.ProductName;
existingProduct.ProductImage = productToUpdate.ProductImage;

var updatedProduct = await productRepo.UpdateProductAsync(existingProduct);

// if image is updated, then we have to delete old image from directory
if (productToUpdate.ImageFile != null)
fileService.DeleteFile(oldImage);

return Ok(updatedProduct);
}
catch (Exception ex)
{
logger.LogError(ex.Message);
return StatusCode(StatusCodes.Status500InternalServerError, ex.Message);
}
}


// api/products/1
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteProduct(int id)
{
try
{
var existingProduct = await productRepo.FindProductByIdAsync(id);
if (existingProduct == null)
{
return StatusCode(StatusCodes.Status404NotFound, $"Product with id: {id} does not found");
}

await productRepo.DeleteProductAsync(existingProduct);
// After deleting product from database,remove file from directory.
fileService.DeleteFile(existingProduct.ProductImage);
return NoContent(); // return 204
}
catch (Exception ex)
{
logger.LogError(ex.Message);
return StatusCode(StatusCodes.Status500InternalServerError, ex.Message);
}
}

// api/products/1
[HttpGet("{id}")]
public async Task<IActionResult> GetProduct(int id)
{
var product = await productRepo.FindProductByIdAsync(id);
if (product == null)
{
return StatusCode(StatusCodes.Status404NotFound, $"Product with id: {id} does not found");
}
return Ok(product);
}


// api/products
[HttpGet]
public async Task<IActionResult> GetProducts()
{
var products = await productRepo.GetProductsAsync();
return Ok(products);
}


}

⚠️ Note: When you are using post and put apis, then please make sure to pass your data from the `Form`. Since we posting the file too, our content type must be multipart/form-data.

Test the Post API with swagger

If you find this article useful, then consider to clap 👏 and share it. You can also follow me on YouTube, I have created lots of content related to .net core and angular.

Connect with me
👉 YouTube: https://youtube.com/@ravindradevrani
👉 Twitter: https://twitter.com/ravi_devrani
👉 GitHub: https://github.com/rd003

Become a supporter ❣️:
You can buy me a coffee 🍵 : https://www.buymeacoffee.com/ravindradevrani

Thanks a lot 🙂🙂

--

--