ASP.NET Core API #9: (Bonus) Upload Gambar untuk Daftar Wisata
Halo lagi, calon fotografer API! 👋
Selama ini data wisata kita cuma berupa teks. Padahal, biar makin menarik, kita perlu foto-foto tempat wisata. Nah, di artikel bonus ini, kita akan menambahkan fitur upload gambar untuk setiap tempat wisata. Anggap saja kita kasih album foto untuk setiap destinasi.
Pilihan Penyimpanan: Database atau File System?
Ada dua cara umum menyimpan gambar:
- Di database – kolom tipe
byte[]atauVARBINARY(MAX). Kelebihannya backup gampang, tapi bikin database cepat besar dan lambat. - Di file system (folder) – simpan file di folder server, simpan path-nya di database. Lebih ringan dan cepat.
Kita pilih cara kedua: simpan file di folder wwwroot/uploads (atau di luar wwwroot kalau mau lebih aman). Path file akan kita simpan di tabel DaftarWisata sebagai string.
Langkah 1: Tambahkan Kolom ImagePath di Model
Buka DaftarWisata.cs di folder Models, tambahkan properti baru:
public string? ImagePath { get; set; }
Boleh nullable karena tidak semua wisata punya foto.
Selanjutnya, kita perlu membuat migration untuk menambah kolom ini. Buka Package Manager Console, jalankan:
Add-Migration AddImagePathToWisata
Update-Database
Sekarang tabel DaftarWisata punya kolom ImagePath.
Langkah 2: Siapkan Folder untuk Menyimpan Gambar
Di root project (di samping folder Controllers, Models, dll), buat folder baru bernama wwwroot (kalau belum ada). Di dalam wwwroot, buat folder uploads. Nanti semua gambar akan disimpan di sana.
Kenapa wwwroot? Karena folder ini secara default bisa diakses secara publik via browser (misal https://localhost:7000/uploads/gambar.jpg). Tapi kalau mau lebih aman, bisa simpan di luar wwwroot dan buat endpoint khusus untuk mengambil gambar. Untuk sederhananya, kita pakai wwwroot.
Langkah 3: Konfigurasi Static Files (Opsional)
Supaya file di wwwroot bisa diakses browser, kita perlu menambahkan middleware static files. Biasanya sudah otomatis, tapi pastikan di Program.cs ada baris:
app.UseStaticFiles();
Biasanya sudah ada. Jika belum, tambahkan sebelum app.UseAuthorization();.
Langkah 4: Install Package (Jika Perlu)
Untuk upload file, kita butuh package Microsoft.AspNetCore.Http.Features dan Microsoft.AspNetCore.Http. Biasanya sudah include dalam framework. Tapi pastikan tidak ada error.
Langkah 5: Update WisataController untuk Menerima Upload
Kita akan membuat dua endpoint baru:
- POST /api/wisata/upload/{id} – untuk upload gambar ke wisata yang sudah ada.
- POST /api/wisata (dengan gambar) – langsung upload gambar saat membuat wisata baru.
Untuk memudahkan, kita buat method terpisah untuk menangani penyimpanan file.
Pertama, tambahkan using Microsoft.AspNetCore.Hosting; dan using Microsoft.AspNetCore.Http; di bagian atas WisataController.
Tambahkan dependency IWebHostEnvironment ke constructor:
private readonly WisataDbContext _context;
private readonly IWebHostEnvironment _environment;
public WisataController(WisataDbContext context, IWebHostEnvironment environment)
{
_context = context;
_environment = environment;
}
Kemudian buat method helper untuk menyimpan file:
private async Task<string> SaveFileAsync(IFormFile file)
{
if (file == null || file.Length == 0)
return null;
// Tentukan folder uploads
string uploadsFolder = Path.Combine(_environment.WebRootPath, "uploads");
if (!Directory.Exists(uploadsFolder))
Directory.CreateDirectory(uploadsFolder);
// Buat nama file unik (timestamp + random) untuk hindari duplikat
string uniqueFileName = Guid.NewGuid().ToString() + "_" + file.FileName;
string filePath = Path.Combine(uploadsFolder, uniqueFileName);
using (var stream = new FileStream(filePath, FileMode.Create))
{
await file.CopyToAsync(stream);
}
return "/uploads/" + uniqueFileName; // path relatif untuk disimpan di database
}
Endpoint 1: Upload Gambar untuk Wisata yang Sudah Ada
[HttpPost("upload/{id}")]
[Authorize]
public async Task<IActionResult> UploadImage(int id, IFormFile file)
{
var wisata = await _context.DaftarWisata.FindAsync(id);
if (wisata == null)
return NotFound();
if (file == null || file.Length == 0)
return BadRequest("File tidak boleh kosong.");
// Optional: validasi tipe file (hanya gambar)
var allowedExtensions = new[] { ".jpg", ".jpeg", ".png", ".gif" };
var extension = Path.GetExtension(file.FileName).ToLowerInvariant();
if (!allowedExtensions.Contains(extension))
return BadRequest("Tipe file tidak didukung. Hanya JPG, PNG, GIF.");
// Batasi ukuran file (misal 5 MB)
if (file.Length > 5 * 1024 * 1024)
return BadRequest("Ukuran file maksimal 5 MB.");
// Hapus gambar lama jika ada (optional)
if (!string.IsNullOrEmpty(wisata.ImagePath))
{
var oldPath = Path.Combine(_environment.WebRootPath, wisata.ImagePath.TrimStart('/'));
if (System.IO.File.Exists(oldPath))
System.IO.File.Delete(oldPath);
}
var imagePath = await SaveFileAsync(file);
wisata.ImagePath = imagePath;
_context.Entry(wisata).State = EntityState.Modified;
await _context.SaveChangesAsync();
return Ok(new { imagePath });
}
Endpoint 2: POST Wisata Baru + Gambar
Kita perlu menangani multipart/form-data. Di method POST yang sudah ada, kita perlu mengubah parameternya agar bisa menerima file. Karena sebelumnya method POST menerima DaftarWisata dari body JSON, kita tidak bisa langsung campur dengan file. Solusinya: buat method POST baru yang menerima [FromForm] dengan data dan file. Atau kita bisa menggunakan DTO khusus.
Buat class DTO baru di folder DTOs: WisataWithImageDto.cs:
using Microsoft.AspNetCore.Http;
using System.ComponentModel.DataAnnotations;
namespace WisataAPI.DTOs
{
public class WisataWithImageDto
{
[Required]
[MaxLength(100)]
public string Nama { get; set; }
public string? Deskripsi { get; set; }
[MaxLength(150)]
public string? Lokasi { get; set; }
public decimal? HargaTiket { get; set; }
[Required]
public int KategoriId { get; set; }
public IFormFile? Gambar { get; set; }
}
}
Kemudian di WisataController, tambahkan method POST baru (atau ubah yang lama). Kita akan buat method baru agar tidak merusak yang lama:
[HttpPost("withimage")]
[Authorize]
public async Task<ActionResult<DaftarWisata>> PostWisataWithImage([FromForm] WisataWithImageDto dto)
{
// Validasi kategori
var kategori = await _context.KategoriWisata.FindAsync(dto.KategoriId);
if (kategori == null)
return BadRequest("KategoriId tidak valid.");
var wisata = new DaftarWisata
{
Nama = dto.Nama,
Deskripsi = dto.Deskripsi,
Lokasi = dto.Lokasi,
HargaTiket = dto.HargaTiket,
KategoriId = dto.KategoriId
};
// Simpan file jika ada
if (dto.Gambar != null)
{
// Validasi file
var allowedExtensions = new[] { ".jpg", ".jpeg", ".png", ".gif" };
var extension = Path.GetExtension(dto.Gambar.FileName).ToLowerInvariant();
if (!allowedExtensions.Contains(extension))
return BadRequest("Tipe file tidak didukung. Hanya JPG, PNG, GIF.");
if (dto.Gambar.Length > 5 * 1024 * 1024)
return BadRequest("Ukuran file maksimal 5 MB.");
wisata.ImagePath = await SaveFileAsync(dto.Gambar);
}
_context.DaftarWisata.Add(wisata);
await _context.SaveChangesAsync();
// Return dengan include kategori
var createdWisata = await _context.DaftarWisata
.Include(w => w.Kategori)
.FirstOrDefaultAsync(w => w.Id == wisata.Id);
return CreatedAtAction(nameof(GetWisata), new { id = wisata.Id }, createdWisata);
}
Perhatikan: method ini diakses via POST /api/wisata/withimage dengan content-type multipart/form-data. Kita bisa menggunakan method POST yang lama untuk data tanpa gambar.
(Opsional) Update PUT untuk Mengganti Gambar
Kita juga bisa menambahkan endpoint PUT yang menerima file. Caranya mirip, gunakan DTO dari form, dan proses file.
📬 Langkah 6: Uji Coba di Postman
Jalankan API (F5). Buka Postman.
Uji Upload Gambar ke Wisata yang Sudah Ada
- Login dan dapatkan token.
- Buat request POST ke
https://localhost:7000/api/wisata/upload/1(ganti 1 dengan id wisata yang ada). - Di tab Body, pilih form-data.
- Tambahkan key dengan nama file, type File, lalu pilih file gambar dari komputer.
- Tambahkan header
Authorization: Bearer <token>. - Klik Send. Response akan mengembalikan path gambar.
Uji POST Wisata Baru dengan Gambar
- Buat request POST ke
https://localhost:7000/api/wisata/withimage. - Body: form-data.
- Tambahkan key untuk setiap field: Nama (text), Deskripsi (text), Lokasi (text), HargaTiket (text), KategoriId (text), Gambar (file).
- Isi nilai-nilainya, misal: Nama = "Pantai Parangtritis", KategoriId = 1, dll.
- Sertakan token di header.
- Klik Send. Jika berhasil, akan mengembalikan data wisata baru dengan ImagePath.
Uji Akses Gambar via Browser
Setelah upload, buka browser dan akses https://localhost:7000/uploads/namafile.jpg (sesuai path yang dikembalikan). Gambar harus tampil.
Memperbarui GET untuk Menyertakan ImagePath
Di method GET, kita sudah menyertakan semua field, jadi ImagePath akan otomatis terbawa. Tidak perlu perubahan.
⚠️ Catatan Penting
- Pastikan folder
wwwroot/uploadsada atau akan dibuat otomatis oleh kode. - Hati-hati dengan nama file: menggunakan Guid untuk menghindari tabrakan nama.
- Ukuran file maksimal 5 MB (bisa diubah).
- Validasi ekstensi file untuk keamanan (jangan sampai file berbahaya diupload).
- Jika ingin menghapus gambar lama saat update, kita sudah lakukan di endpoint upload. Untuk endpoint PUT nanti bisa ditambahkan.
- Di production, pertimbangkan untuk menyimpan gambar di cloud storage (Azure Blob, AWS S3) agar lebih scalable.
Selamat! Sekarang Tempat Wisata Bisa Berfoto
API kita sekarang bisa menerima gambar. Pengguna bisa mengunggah foto tempat wisata, dan gambar bisa diakses melalui URL. Ini menambah nilai aplikasi kita.
Di artikel berikutnya (terakhir) ASP.NET Core API #10, kita akan membahas cara deploy API ke IIS atau Azure agar bisa diakses dari internet. Sampai jumpa!
Ditulis oleh Kakak programmer yang dulu juga suka foto-foto waktu liburan. Kalau ada pertanyaan, tulis di komentar ya!