1. Create Project
# Check .NET SDK installation
dotnet --version
# Create project
dotnet new webapi -n my-api --no-https
cd my-api dotnet new webapi creates an ASP.NET Core Web API project.
The --no-https option disables HTTPS for local development.
2. Write Your First API
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
var builder = WebApplication.CreateBuilder(args);
// CORS configuration
builder.Services.AddCors(options =>
{
options.AddDefaultPolicy(policy =>
{
policy.WithOrigins("http://localhost:5173")
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials();
});
});
var app = builder.Build();
app.UseCors();
app.MapGet("/health", () => Results.Ok(new { status = "healthy" }));
app.MapGet("/api/hello", (string? name) =>
Results.Ok(new { message = $"Hello, {name ?? "World"}!" }));
app.Run();Run
dotnet run
# Test (in a new terminal)
curl http://localhost:5000/health
curl "http://localhost:5000/api/hello?name=Choorai"3. Add CRUD API
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
// Model definition
record Project(Guid Id, string Name, string Description, DateTime CreatedAt);
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddCors();
var app = builder.Build();
app.UseCors();
// In-memory storage
var projects = new List<Project>();
var lockObj = new object();
var api = app.MapGroup("/api/v1/projects");
// List all
api.MapGet("/", () =>
{
lock (lockObj)
{
return Results.Ok(new { items = projects, total = projects.Count });
}
});
// Get by ID
api.MapGet("/{id:guid}", (Guid id) =>
{
lock (lockObj)
{
var project = projects.FirstOrDefault(p => p.Id == id);
return project is not null
? Results.Ok(project)
: Results.NotFound(new { error = "not found" });
}
});
// Create
api.MapPost("/", (CreateProjectRequest req) =>
{
var project = new Project(
Guid.NewGuid(),
req.Name,
req.Description,
DateTime.UtcNow
);
lock (lockObj)
{
projects.Add(project);
}
return Results.Created($"/api/v1/projects/{project.Id}", project);
});
// Update
api.MapPut("/{id:guid}", (Guid id, UpdateProjectRequest req) =>
{
lock (lockObj)
{
var index = projects.FindIndex(p => p.Id == id);
if (index == -1)
return Results.NotFound(new { error = "not found" });
projects[index] = projects[index] with
{
Name = req.Name,
Description = req.Description
};
return Results.Ok(projects[index]);
}
});
// Delete
api.MapDelete("/{id:guid}", (Guid id) =>
{
lock (lockObj)
{
var removed = projects.RemoveAll(p => p.Id == id);
return removed > 0
? Results.NoContent()
: Results.NotFound(new { error = "not found" });
}
});
app.Run();
// Request DTOs
record CreateProjectRequest(string Name, string Description);
record UpdateProjectRequest(string Name, string Description);Minimal API and Record Types
C#'s record type defines immutable data objects concisely.
The with expression creates a new instance with only specific fields changed.
4. Deploy with Docker
# Build stage
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY *.csproj .
RUN dotnet restore
COPY . .
RUN dotnet publish -c Release -o /app
# Runtime stage
FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine
WORKDIR /app
COPY --from=build /app .
ENV ASPNETCORE_URLS=http://+:8080
EXPOSE 8080
ENTRYPOINT ["dotnet", "my-api.dll"]Multi-stage builds reduce from SDK (~700MB) to runtime (~100MB). Alpine-based images are used to minimize size.
# Local test
docker build -t my-dotnet-api .
docker run -p 8080:8080 my-dotnet-api
# Check image size (~100MB)
docker images my-dotnet-api
# Cloud Run deployment
gcloud run deploy my-dotnet-api \
--source . \
--region asia-northeast3 \
--allow-unauthenticated \
--min-instances 0 \
--max-instances 1Framework Comparison
| Category | .NET (Minimal API) | Go (Chi) | FastAPI |
|---|---|---|---|
| Image Size | ~100MB | ~15MB | ~200MB |
| Cold Start | ~200ms | ~100ms | ~500ms |
| Memory Usage | ~40MB | ~20MB | ~80MB |
| Learning Curve | Medium | Medium | Easy |
| Type System | Static | Static | Dynamic |