React + ASP.NET Core - Related Tables CRUD
This guide demonstrates how to perform CRUD operations in a full-stack app with React and ASP.NET Core Web API using two related tables: Category
and Product
. Each product belongs to one category.
Backend: ASP.NET Core Web API
Model: Category.cs
public class Category
{
public int Id { get; set; }
public string Name { get; set; }
public ICollection<Product> Products { get; set; }
}
Model: Product.cs
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public int CategoryId { get; set; }
public Category Category { get; set; }
}
DbContext Configuration
using Microsoft.EntityFrameworkCore;
using YourNamespace.Models; // Replace with actual namespace
namespace YourNamespace.Data
{
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions options)
: base(options) { }
public DbSet Categories { get; set; }
public DbSet Products { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Category has many Products
modelBuilder.Entity()
.HasMany(c => c.Products)
.WithOne(p => p.Category)
.HasForeignKey(p => p.CategoryId)
.OnDelete(DeleteBehavior.Restrict); // or .Cascade
// Optional: Configure field properties
modelBuilder.Entity()
.Property(c => c.Name)
.IsRequired()
.HasMaxLength(100);
modelBuilder.Entity()
.Property(p => p.Name)
.IsRequired()
.HasMaxLength(100);
modelBuilder.Entity()
.Property(p => p.Price)
.HasColumnType("decimal(18,2)");
}
}
}
ProductsController.cs
[HttpGet]
public async Task<ActionResult<IEnumerable<Product>>> Get() =>
await _context.Products.Include(p => p.Category).ToListAsync();
[HttpPost]
public async Task<ActionResult<Product>> Post(Product product)
{
_context.Products.Add(product);
await _context.SaveChangesAsync();
return CreatedAtAction(nameof(Get), new { id = product.Id }, product);
}
[HttpPut("{id}")]
public async Task<IActionResult> Put(int id, Product product)
{
if (id != product.Id) return BadRequest();
_context.Entry(product).State = EntityState.Modified;
await _context.SaveChangesAsync();
return NoContent();
}
[HttpDelete("{id}")]
public async Task<IActionResult> Delete(int id)
{
var product = await _context.Products.FindAsync(id);
if (product == null) return NotFound();
_context.Products.Remove(product);
await _context.SaveChangesAsync();
return NoContent();
}
CategoriesController.cs
[HttpGet]
public async Task<ActionResult<IEnumerable<Category>>> Get() =>
await _context.Categories.ToListAsync();
[HttpPost]
public async Task<ActionResult<Category>> Post(Category category)
{
_context.Categories.Add(category);
await _context.SaveChangesAsync();
return CreatedAtAction(nameof(Get), new { id = category.Id }, category);
}
Frontend: React Application
ProductComponent.js with Category Link
import React, { useEffect, useState } from 'react';
import axios from 'axios';
const API = "https://localhost:5001/api";
function ProductComponent() {
const [products, setProducts] = useState([]);
const [categories, setCategories] = useState([]);
const [form, setForm] = useState({ id: 0, name: "", price: "", categoryId: "" });
const [editing, setEditing] = useState(false);
useEffect(() => {
axios.get(`${API}/categories`).then(res => setCategories(res.data));
axios.get(`${API}/products`).then(res => setProducts(res.data));
}, []);
const handleChange = e => setForm({ ...form, [e.target.name]: e.target.value });
const handleSubmit = e => {
e.preventDefault();
const method = editing ? "put" : "post";
const url = `${API}/products${editing ? `/${form.id}` : ""}`;
axios[method](url, form).then(() => {
resetForm();
axios.get(`${API}/products`).then(res => setProducts(res.data));
});
};
const handleEdit = p => {
setForm(p);
setEditing(true);
};
const handleDelete = id => {
axios.delete(`${API}/products/${id}`).then(() =>
setProducts(prev => prev.filter(p => p.id !== id))
);
};
const resetForm = () => {
setForm({ id: 0, name: "", price: "", categoryId: "" });
setEditing(false);
};
return (
<div>
<h3>{editing ? "Edit Product" : "Add Product"}</h3>
<form onSubmit={handleSubmit}>
<input name="name" value={form.name} onChange={handleChange} placeholder="Name" required />
<input name="price" value={form.price} onChange={handleChange} placeholder="Price" type="number" required />
<select name="categoryId" value={form.categoryId} onChange={handleChange} required>
<option value="">-- Select Category --</option>
{categories.map(c => (
<option key={c.id} value={c.id}>{c.name}</option>
))}
</select>
<button type="submit">{editing ? "Update" : "Add"}</button>
{editing && <button type="button" onClick={resetForm}>Cancel</button>}
</form>
<h4>Product List</h4>
<ul>
{products.map(p => (
<li key={p.id}>
{p.name} (${p.price}) - Category: {p.category?.name}
<button onClick={() => handleEdit(p)}>Edit</button>
<button onClick={() => handleDelete(p.id)}>Delete</button>
</li>
))}
</ul>
</div>
);
}
export default ProductComponent;
Summary
This setup demonstrates how to build and link related entities in a full-stack app:
- Products have a foreign key linking them to Categories
- Categories can exist independently, and products are created under them
- Product list includes category name using Entity Framework's
Include
- React form uses
<select>
for category assignment - Full CRUD supported for both tables, including Edit and Delete