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