Skip to content

Bits of .NET

Daily micro-tips for C#, SQL, performance, and scalable backend engineering.

  • Asp.Net Core
  • C#
  • SQL
  • JavaScript
  • CSS
  • About
  • ErcanOPAK.com
  • No Access
  • Privacy Policy
Asp.Net Core

Step-by-Step: Create ASP.NET Core 8 Web API with Docker and PostgreSQL

- 19.08.26 | 19.08.26 - ErcanOPAK

Complete guide to building a production-ready Web API with ASP.NET Core 8, Entity Framework Core, PostgreSQL, and Docker.

Step 1: Create the Project

# Create new Web API project
dotnet new webapi -n ProductAPI --no-https
cd ProductAPI

# Add required packages
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.EntityFrameworkCore.Tools
dotnet add package Newtonsoft.Json

Step 2: Create Models

// Models/Product.cs
public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    public decimal Price { get; set; }
    public int StockQuantity { get; set; }
    public DateTime CreatedAt { get; set; }
    public DateTime? UpdatedAt { get; set; }
    public bool IsActive { get; set; }
    public string Category { get; set; }
}

// Models/Category.cs
public class Category
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    public List Products { get; set; }
}

Step 3: Configure Database Context

// Data/AppDbContext.cs
using Microsoft.EntityFrameworkCore;

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

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

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        // Product configuration
        modelBuilder.Entity<Product>(entity =>
        {
            entity.HasKey(e => e.Id);
            entity.Property(e => e.Name).IsRequired().HasMaxLength(200);
            entity.Property(e => e.Description).HasMaxLength(1000);
            entity.Property(e => e.Price).HasPrecision(18, 2);
            entity.HasIndex(e => e.Name);
            entity.HasIndex(e => e.Category);
        });

        // Category configuration
        modelBuilder.Entity<Category>(entity =>
        {
            entity.HasKey(e => e.Id);
            entity.Property(e => e.Name).IsRequired().HasMaxLength(100);
            entity.HasIndex(e => e.Name).IsUnique();
        });

        // Seed data
        modelBuilder.Entity<Category>().HasData(
            new Category { Id = 1, Name = "Electronics", Description = "Electronic devices and accessories" },
            new Category { Id = 2, Name = "Books", Description = "Books and publications" },
            new Category { Id = 3, Name = "Clothing", Description = "Apparel and fashion items" }
        );
    }
}

Step 4: Create Repository and Service

// Repositories/IProductRepository.cs
public interface IProductRepository
{
    Task<Product> GetByIdAsync(int id);
    Task<IEnumerable<Product>> GetAllAsync();
    Task<IEnumerable<Product>> GetByCategoryAsync(string category);
    Task<Product> AddAsync(Product product);
    Task UpdateAsync(Product product);
    Task DeleteAsync(int id);
    Task<bool> ExistsAsync(int id);
}

// Repositories/ProductRepository.cs
public class ProductRepository : IProductRepository
{
    private readonly AppDbContext _context;

    public ProductRepository(AppDbContext context)
    {
        _context = context;
    }

    public async Task< Product> GetByIdAsync(int id)
    {
        return await _context.Products.FindAsync(id);
    }

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

    public async Task< IEnumerable<Product>> GetByCategoryAsync(string category)
    {
        return await _context.Products
            .Where(p => p.Category == category && p.IsActive)
            .ToListAsync();
    }

    public async Task< Product> AddAsync(Product product)
    {
        product.CreatedAt = DateTime.UtcNow;
        await _context.Products.AddAsync(product);
        await _context.SaveChangesAsync();
        return product;
    }

    public async Task UpdateAsync(Product product)
    {
        product.UpdatedAt = DateTime.UtcNow;
        _context.Entry(product).State = EntityState.Modified;
        await _context.SaveChangesAsync();
    }

    public async Task DeleteAsync(int id)
    {
        var product = await GetByIdAsync(id);
        if (product != null)
        {
            // Soft delete
            product.IsActive = false;
            product.UpdatedAt = DateTime.UtcNow;
            await _context.SaveChangesAsync();
        }
    }

    public async Task< bool> ExistsAsync(int id)
    {
        return await _context.Products.AnyAsync(p => p.Id == id);
    }
}

// Services/IProductService.cs
public interface IProductService
{
    Task<Product> GetProductAsync(int id);
    Task<IEnumerable<Product>> GetProductsAsync();
    Task<IEnumerable<Product>> GetProductsByCategoryAsync(string category);
    Task<Product> CreateProductAsync(Product product);
    Task UpdateProductAsync(Product product);
    Task DeleteProductAsync(int id);
}

// Services/ProductService.cs
public class ProductService : IProductService
{
    private readonly IProductRepository _repository;
    private readonly ILogger<ProductService> _logger;

    public ProductService(IProductRepository repository, ILogger< ProductService> logger)
    {
        _repository = repository;
        _logger = logger;
    }

    public async Task<Product & gt; GetProductAsync(int id)
    {
    var product = await _repository.GetByIdAsync(id);
    if (product == null || !product.IsActive)
        throw new KeyNotFoundException($"Product with ID {id} not found");

    return product;
}

public async Task< IEnumerable & lt; Product & gt; > GetProductsAsync()
    {
    var products = await _repository.GetAllAsync();
    return products.Where(p => p.IsActive);
}

public async Task< IEnumerable & lt; Product & gt; > GetProductsByCategoryAsync(string category)
    {
    return await _repository.GetByCategoryAsync(category);
}

public async Task< Product & gt; CreateProductAsync(Product product)
    {
    // Validate
    if (string.IsNullOrEmpty(product.Name))
        throw new ArgumentException("Product name is required");

    if (product.Price <= 0)
        throw new ArgumentException("Price must be greater than zero");

    product.IsActive = true;
    var created = await _repository.AddAsync(product);
    _logger.LogInformation("Created product {ProductId} - {ProductName}", created.Id, created.Name);
    return created;
}

public async Task UpdateProductAsync(Product product)
{
    var existing = await _repository.GetByIdAsync(product.Id);
    if (existing == null)
        throw new KeyNotFoundException($"Product with ID {product.Id} not found");

    // Update only allowed fields
    existing.Name = product.Name;
    existing.Description = product.Description;
    existing.Price = product.Price;
    existing.StockQuantity = product.StockQuantity;
    existing.Category = product.Category;
    existing.UpdatedAt = DateTime.UtcNow;

    await _repository.UpdateAsync(existing);
    _logger.LogInformation("Updated product {ProductId}", existing.Id);
}

public async Task DeleteProductAsync(int id)
{
    var product = await _repository.GetByIdAsync(id);
    if (product == null)
        throw new KeyNotFoundException($"Product with ID {id} not found");

    await _repository.DeleteAsync(id);
    _logger.LogInformation("Deleted product {ProductId}", id);
}
}
</ pre>

Step 5: Create API Controllers

// Controllers/ProductsController.cs
[ApiController]
[Route("api/[controller]")]
[Produces("application/json")]
public class ProductsController : ControllerBase
{
    private readonly IProductService _productService;
    private readonly ILogger<ProductsController> _logger;

    public ProductsController(IProductService productService, ILogger< ProductsController> logger)
    {
        _productService = productService;
        _logger = logger;
    }

[HttpGet]
public async Task< ActionResult & lt; IEnumerable & lt; ProductDto & gt; > > GetProducts()
    {
    var products = await _productService.GetProductsAsync();
    return Ok(products.Select(p => new ProductDto(p)));
}

[HttpGet("{id}")]
public async Task< ActionResult & lt; ProductDto & gt; > GetProduct(int id)
    {
    try
    {
        var product = await _productService.GetProductAsync(id);
        return Ok(new ProductDto(product));
    }
    catch (KeyNotFoundException ex)
    {
        return NotFound(ex.Message);
    }
}

[HttpGet("category/{category}")]
public async Task< ActionResult & lt; IEnumerable & lt; ProductDto & gt; > > GetProductsByCategory(string category)
    {
    var products = await _productService.GetProductsByCategoryAsync(category);
    return Ok(products.Select(p => new ProductDto(p)));
}

[HttpPost]
public async Task< ActionResult & lt; ProductDto & gt; > CreateProduct(CreateProductRequest request)
    {
    try
    {
        var product = new Product
        {
            Name = request.Name,
            Description = request.Description,
            Price = request.Price,
            StockQuantity = request.StockQuantity,
            Category = request.Category
        };

        var created = await _productService.CreateProductAsync(product);
        return CreatedAtAction(nameof(GetProduct), new { id = created.Id }, new ProductDto(created));
    }
    catch (ArgumentException ex)
    {
        return BadRequest(ex.Message);
    }
}

[HttpPut("{id}")]
public async Task< IActionResult & gt; UpdateProduct(int id, UpdateProductRequest request)
    {
    try
    {
        var product = new Product
        {
            Id = id,
            Name = request.Name,
            Description = request.Description,
            Price = request.Price,
            StockQuantity = request.StockQuantity,
            Category = request.Category
        };

        await _productService.UpdateProductAsync(product);
        return NoContent();
    }
    catch (KeyNotFoundException ex)
    {
        return NotFound(ex.Message);
    }
}

[HttpDelete("{id}")]
public async Task< IActionResult & gt; DeleteProduct(int id)
    {
    try
    {
        await _productService.DeleteProductAsync(id);
        return NoContent();
    }
    catch (KeyNotFoundException ex)
    {
        return NotFound(ex.Message);
    }
}
}

// DTOs/ProductDto.cs
public class ProductDto
{
    public ProductDto(Product product)
    {
        Id = product.Id;
        Name = product.Name;
        Description = product.Description;
        Price = product.Price;
        StockQuantity = product.StockQuantity;
        Category = product.Category;
        CreatedAt = product.CreatedAt;
        IsActive = product.IsActive;
    }

    public int Id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    public decimal Price { get; set; }
    public int StockQuantity { get; set; }
    public string Category { get; set; }
    public DateTime CreatedAt { get; set; }
    public bool IsActive { get; set; }
}

Step 6: Docker Configuration

# Dockerfile
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR / src

# Copy csproj and restore
COPY ProductAPI.csproj .
RUN dotnet restore

# Copy source and publish
COPY . .
RUN dotnet publish -c Release -o /app/publish

# Runtime image
FROM mcr.microsoft.com/dotnet/aspnet:8.0
WORKDIR / app

# Install curl for health checks
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*

COPY --from=build /app/publish .

EXPOSE 80
EXPOSE 443

ENTRYPOINT ["dotnet", "ProductAPI.dll"]
# docker-compose.yml
version: '3.8'

services:
  postgres:
    image: postgres:16-alpine
    container_name: productapi-db
    environment:
      POSTGRES_USER: productuser
      POSTGRES_PASSWORD: productpassword
      POSTGRES_DB: productdb
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U productuser"]
      interval: 10s
      timeout: 5s
      retries: 5

  api:
    build: .
    container_name: productapi
    environment:
      - ASPNETCORE_ENVIRONMENT=Development
      - ConnectionStrings__DefaultConnection=Host=postgres;Port=5432;Database=productdb;Username=productuser;Password=productpassword
    ports:
      - "5000:80"
    depends_on:
      postgres:
        condition: service_healthy
    volumes:
      - ./logs:/app/logs
    restart: unless-stopped

volumes:
  postgres_data:

Step 7: Configure Program.cs

// Program.cs
var builder = WebApplication.CreateBuilder(args);

// Add services
builder.Services.AddControllers()
    .AddJsonOptions(options =>
    {
        options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles;
        options.JsonSerializerOptions.WriteIndented = true;
    });

// Database configuration
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseNpgsql(connectionString));

// Dependency Injection
builder.Services.AddScoped<IProductRepository, ProductRepository>();
builder.Services.AddScoped<IProductService, ProductService>();

// CORS
builder.Services.AddCors(options =>
{
    options.AddPolicy("AllowAll",
        policy => policy.AllowAnyOrigin()
                        .AllowAnyMethod()
                        .AllowAnyHeader());
});

// Swagger
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c =>
{
    c.SwaggerDoc("v1", new OpenApiInfo
    {
        Title = "Product API",
        Version = "v1",
        Description = "Product management API built with ASP.NET Core 8 and PostgreSQL"
    });
});

var app = builder.Build();

// Apply migrations and seed database
using (var scope = app.Services.CreateScope())
{
    var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
    await dbContext.Database.MigrateAsync();
}

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();
app.UseCors("AllowAll");
app.UseAuthorization();
app.MapControllers();

app.Run();

Step 8: Run the Application

# Build and run with Docker Compose
docker-compose up --build

# Or run directly with dotnet
dotnet run

# Test the API
curl http://localhost:5000/api/products

# Create a product
curl -X POST http://localhost:5000/api/products \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Laptop",
    "description": "High performance laptop",
    "price": 999.99,
    "stockQuantity": 50,
    "category": "Electronics"
  }'

# Get product by ID
curl http://localhost:5000/api/products/1

Step 9: Health Check Endpoint

// Controllers/HealthController.cs
[ApiController]
[Route("api/health")]
public class HealthController : ControllerBase
{
    private readonly AppDbContext _context;

    public HealthController(AppDbContext context)
    {
        _context = context;
    }

    [HttpGet]
    public async Task<IActionResult> CheckHealth()
    {
        var canConnect = await _context.Database.CanConnectAsync();
        if (!canConnect)
            return StatusCode(503, new { status = "unhealthy", database = "unreachable" });
        
        return Ok(new
        {
            status = "healthy",
            database = "connected",
            timestamp = DateTime.UtcNow,
            version = "1.0.0"
        });
    }
}

// In Dockerfile, add health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD curl -f http://localhost/health || exit 1

Step 10: Production Deployment

# Build for production
docker build -t productapi:latest .

# Run with production settings
docker run -d \
  --name productapi \
  -p 5000:80 \
  -e ASPNETCORE_ENVIRONMENT=Production \
  -e ConnectionStrings__DefaultConnection="Host=prod-db;Port=5432;Database=productdb;Username=produser;Password=prodpass" \
  productapi:latest

# Or use Docker Compose with environment variables
docker-compose -f docker-compose.prod.yml up -d

Related posts:

.NET Core — Kestrel Has Request Limits You Might Hit

.NET Core: Use Entity Framework for Data Access

.NET Core: Use Response Caching Middleware for Static APIs

Post Views: 7

Post navigation

SQL Server Execution Plan Analysis: Find and Fix Slow Queries Like a Pro
Step-by-Step: Deploy .NET Microservice to Kubernetes with Helm

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

October 2026
M T W T F S S
 1234
567891011
12131415161718
19202122232425
262728293031  
« Sep    

Most Viewed Posts

  • Get the User Name and Domain Name from an Email Address in SQL (973)
  • How to make theater mode the default for Youtube (952)
  • How to add default value for Entity Framework migrations for DateTime and Bool (939)
  • Get the First and Last Word from a String or Sentence in SQL (847)
  • How to select distinct rows in a datatable in C# (837)
  • How to enable, disable and check if Service Broker is enabled on a database in SQL Server (624)
  • Add Constraint to SQL Table to ensure email contains @ (590)
  • Average of all values in a column that are not zero in SQL (553)
  • How to use Map Mode for Vertical Scroll Mode in Visual Studio (526)
  • Find numbers with more than two decimal places in SQL (468)

Recent Posts

  • CSS: Fix a prefers-color-scheme Media Query That Gets Silently Overridden by a Browser Extension’s Forced Dark Mode
  • Git: Fix a Merge Commit That Silently Drops a File Because Both Branches Deleted It Differently
  • HTML5: Fix a Native Lazy-Loading Image That Never Loads Because It Sits Inside a Hidden Tab Until the User Clicks It
  • The AI Prompt That Traces a Null Reference Exception Back to the Exact Line That First Produced the Null
  • The AI Prompt That Turns a Gym Membership Contract’s Fine Print Into a Plain-English List of Cancellation Steps
  • Photoshop: Fix a Color Profile Mismatch That Makes Printed Output Look Nothing Like What You Saw On Screen
  • WordPress: Fix Search Results That Return Pages From a Theme You Deactivated Months Ago
  • Visual Studio: Fix a Test Project That Builds Fine Alone but Fails to Discover Any Tests After a NuGet Restore
  • ASP.NET Core: Fix a File Upload That Times Out on Slow Connections Only Because Kestrel’s Minimum Data Rate Feature Kicked In
  • JavaScript: Fix an Array Destructuring Default Value That Silently Never Applies Because null Was Passed Instead of Undefined

Most Viewed Posts

  • Get the User Name and Domain Name from an Email Address in SQL (973)
  • How to make theater mode the default for Youtube (952)
  • How to add default value for Entity Framework migrations for DateTime and Bool (939)
  • Get the First and Last Word from a String or Sentence in SQL (847)
  • How to select distinct rows in a datatable in C# (837)

Recent Posts

  • CSS: Fix a prefers-color-scheme Media Query That Gets Silently Overridden by a Browser Extension’s Forced Dark Mode
  • Git: Fix a Merge Commit That Silently Drops a File Because Both Branches Deleted It Differently
  • HTML5: Fix a Native Lazy-Loading Image That Never Loads Because It Sits Inside a Hidden Tab Until the User Clicks It
  • The AI Prompt That Traces a Null Reference Exception Back to the Exact Line That First Produced the Null
  • The AI Prompt That Turns a Gym Membership Contract’s Fine Print Into a Plain-English List of Cancellation Steps

Social

  • ErcanOPAK.com
  • GoodReads
  • LetterBoxD
  • Linkedin
  • The Blog
  • Twitter
© 2026 Bits of .NET | Built with Xblog Plus free WordPress theme by wpthemespace.com