Table of contents
- Setup .NET MVC with Docker and MSSQL on Mac
- Prerequisites
- Installing Docker Desktop
- Installing .NET SDK
- Setting up MSSQL with Docker
- Creating ASP.NET Core MVC project
- Configuring database connection
- Database migrations
- Implementing CRUD operations
- Running the application
- Docker cleanup
- Troubleshooting
Setup .NET MVC with Docker and MSSQL on Mac
This guide walks you through setting up a complete ASP.NET Core MVC application with MSSQL database running in Docker on Mac using VS Code. We'll build a simple Product Catalog with full CRUD operations to demonstrate the database integration.
Prerequisites
Before starting, ensure you have:
- macOS (Intel or Apple Silicon)
- Homebrew package manager installed
- VS Code installed
- Basic understanding of C# and MVC pattern
Installing Docker Desktop
Docker Desktop is required to run MSSQL in a container on Mac.
brew install --cask docker
After installation, open Docker Desktop from Applications and wait for it to start completely. You'll see a green icon in the menu bar when it's ready.
Installing .NET SDK
Install the latest .NET SDK using Homebrew:
brew install dotnet
Verify the installation:
dotnet --version
Setting up MSSQL with Docker
Running MSSQL container
Run MSSQL Server 2022 in a Docker container with persistent storage:
docker run \
--platform linux/amd64 \
-e "ACCEPT_EULA=Y" \
-e "SA_PASSWORD=YourStrong!Passw0rd" \
-p 1433:1433 \
--name mssql-db \
-v "$(pwd)/mssql-data":/var/opt/mssql \
-d mcr.microsoft.com/mssql/server:2022-latest
Verifying MSSQL connection
Check if the container is running:
docker ps
You should see
To view container logs:
docker logs mssql-db
Look for "SQL Server is now ready for client connections" message.
Managing the container
Stop the container:
docker stop mssql-db
Start the container:
docker start mssql-db
Remove the container and data:
docker stop mssql-db
docker rm mssql-db
rm -rf mssql-data
Helper scripts for container management
To simplify container management, create helper scripts in your project root.
Create
#!/bin/bash
docker start mssql-db || docker run \
--platform linux/amd64 \
-e "ACCEPT_EULA=Y" \
-e "SA_PASSWORD=YourStrong!Passw0rd" \
-p 1433:1433 \
--name mssql-db \
-v "$(pwd)/mssql-data":/var/opt/mssql \
-d mcr.microsoft.com/mssql/server:2022-latest
Create
#!/bin/bash
docker stop mssql-db
Create
#!/bin/bash
docker stop mssql-db
docker rm mssql-db
rm -rf mssql-data
Make scripts executable:
chmod +x start-db.sh stop-db.sh clean-db.sh
Now you can easily manage the database:
./start-db.sh # Start or create the container
./stop-db.sh # Stop the container
./clean-db.sh # Remove container and data
Creating ASP.NET Core MVC project
Create a new MVC project:
dotnet new mvc -n ProductCatalog
cd ProductCatalog
Project structure
The project structure includes:
Controllers/ - MVC controllersModels/ - Data modelsViews/ - Razor viewsProgram.cs - Application entry pointappsettings.json - Configuration
Installing required packages
Install Entity Framework Core packages for MSSQL:
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.EntityFrameworkCore.Tools
Configuring database connection
Connection string setup
Open
{
"ConnectionStrings": {
"DefaultConnection": "Server=localhost,1433;Database=ProductCatalogDB;User Id=sa;Password=YourStrong!Passw0rd;TrustServerCertificate=True;Encrypt=True;"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
Creating the model
Create a new file
using System.ComponentModel.DataAnnotations;
namespace ProductCatalog.Models
{
public class Product
{
public int Id { get; set; }
[Required]
[StringLength(100)]
public string Name { get; set; } = string.Empty;
[StringLength(500)]
public string Description { get; set; } = string.Empty;
[Required]
[Range(0.01, 10000.00)]
public decimal Price { get; set; }
public int Stock { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.Now;
}
}
Creating DbContext
Create
using Microsoft.EntityFrameworkCore;
using ProductCatalog.Models;
namespace ProductCatalog.Data
{
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
public DbSet<Product> Products { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Product>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.Price).HasColumnType("decimal(18,2)");
entity.HasIndex(e => e.Name);
});
}
}
}
Registering DbContext
Open
using Microsoft.EntityFrameworkCore;
using ProductCatalog.Data;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllersWithViews();
// Register DbContext
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Products}/{action=Index}/{id?}");
app.Run();
Database migrations
Installing EF Core tools
Install EF Core tools globally:
dotnet tool install --global dotnet-ef
If already installed, update to the latest version:
dotnet tool update --global dotnet-ef
Creating initial migration
Create the initial database migration:
dotnet ef migrations add InitialCreate
This creates a
Applying migrations
Apply the migration to create the database:
dotnet ef database update
Implementing CRUD operations
Creating the controller
Create
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using ProductCatalog.Data;
using ProductCatalog.Models;
namespace ProductCatalog.Controllers
{
public class ProductsController : Controller
{
private readonly ApplicationDbContext _context;
public ProductsController(ApplicationDbContext context)
{
_context = context;
}
// GET: Products
public async Task<IActionResult> Index()
{
var products = await _context.Products
.OrderByDescending(p => p.CreatedAt)
.ToListAsync();
return View(products);
}
// GET: Products/Details/5
public async Task<IActionResult> Details(int? id)
{
if (id == null)
{
return NotFound();
}
var product = await _context.Products
.FirstOrDefaultAsync(m => m.Id == id);
if (product == null)
{
return NotFound();
}
return View(product);
}
// GET: Products/Create
public IActionResult Create()
{
return View();
}
// POST: Products/Create
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create([Bind("Name,Description,Price,Stock")] Product product)
{
if (ModelState.IsValid)
{
product.CreatedAt = DateTime.Now;
_context.Add(product);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
return View(product);
}
// GET: Products/Edit/5
public async Task<IActionResult> Edit(int? id)
{
if (id == null)
{
return NotFound();
}
var product = await _context.Products.FindAsync(id);
if (product == null)
{
return NotFound();
}
return View(product);
}
// POST: Products/Edit/5
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("Id,Name,Description,Price,Stock,CreatedAt")] Product product)
{
if (id != product.Id)
{
return NotFound();
}
if (ModelState.IsValid)
{
try
{
_context.Update(product);
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!ProductExists(product.Id))
{
return NotFound();
}
throw;
}
return RedirectToAction(nameof(Index));
}
return View(product);
}
// GET: Products/Delete/5
public async Task<IActionResult> Delete(int? id)
{
if (id == null)
{
return NotFound();
}
var product = await _context.Products
.FirstOrDefaultAsync(m => m.Id == id);
if (product == null)
{
return NotFound();
}
return View(product);
}
// POST: Products/Delete/5
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{
var product = await _context.Products.FindAsync(id);
if (product != null)
{
_context.Products.Remove(product);
await _context.SaveChangesAsync();
}
return RedirectToAction(nameof(Index));
}
private bool ProductExists(int id)
{
return _context.Products.Any(e => e.Id == id);
}
}
}
Index view - List all items
Create
@model IEnumerable<ProductCatalog.Models.Product>
@{
ViewData["Title"] = "Products";
}
<h1>Products</h1>
<p>
<a asp-action="Create" class="btn btn-primary">Create New Product</a>
</p>
<table class="table table-striped">
<thead>
<tr>
<th>Name</th>
<th>Description</th>
<th>Price</th>
<th>Stock</th>
<th>Created At</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@foreach (var item in Model)
{
<tr>
<td>@Html.DisplayFor(modelItem => item.Name)</td>
<td>@Html.DisplayFor(modelItem => item.Description)</td>
<td>$@item.Price.ToString("F2")</td>
<td>@Html.DisplayFor(modelItem => item.Stock)</td>
<td>@item.CreatedAt.ToString("yyyy-MM-dd HH:mm")</td>
<td>
<a asp-action="Edit" asp-route-id="@item.Id" class="btn btn-sm btn-warning">Edit</a>
<a asp-action="Details" asp-route-id="@item.Id" class="btn btn-sm btn-info">Details</a>
<a asp-action="Delete" asp-route-id="@item.Id" class="btn btn-sm btn-danger">Delete</a>
</td>
</tr>
}
</tbody>
</table>
Create view - Add new item
Create
@model ProductCatalog.Models.Product
@{
ViewData["Title"] = "Create Product";
}
<h1>Create Product</h1>
<hr />
<div class="row">
<div class="col-md-6">
<form asp-action="Create">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group mb-3">
<label asp-for="Name" class="control-label"></label>
<input asp-for="Name" class="form-control" />
<span asp-validation-for="Name" class="text-danger"></span>
</div>
<div class="form-group mb-3">
<label asp-for="Description" class="control-label"></label>
<textarea asp-for="Description" class="form-control" rows="3"></textarea>
<span asp-validation-for="Description" class="text-danger"></span>
</div>
<div class="form-group mb-3">
<label asp-for="Price" class="control-label"></label>
<input asp-for="Price" class="form-control" type="number" step="0.01" />
<span asp-validation-for="Price" class="text-danger"></span>
</div>
<div class="form-group mb-3">
<label asp-for="Stock" class="control-label"></label>
<input asp-for="Stock" class="form-control" type="number" />
<span asp-validation-for="Stock" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-primary" />
<a asp-action="Index" class="btn btn-secondary">Back to List</a>
</div>
</form>
</div>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
Edit view - Update item
Create
@model ProductCatalog.Models.Product
@{
ViewData["Title"] = "Edit Product";
}
<h1>Edit Product</h1>
<hr />
<div class="row">
<div class="col-md-6">
<form asp-action="Edit">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<input type="hidden" asp-for="Id" />
<input type="hidden" asp-for="CreatedAt" />
<div class="form-group mb-3">
<label asp-for="Name" class="control-label"></label>
<input asp-for="Name" class="form-control" />
<span asp-validation-for="Name" class="text-danger"></span>
</div>
<div class="form-group mb-3">
<label asp-for="Description" class="control-label"></label>
<textarea asp-for="Description" class="form-control" rows="3"></textarea>
<span asp-validation-for="Description" class="text-danger"></span>
</div>
<div class="form-group mb-3">
<label asp-for="Price" class="control-label"></label>
<input asp-for="Price" class="form-control" type="number" step="0.01" />
<span asp-validation-for="Price" class="text-danger"></span>
</div>
<div class="form-group mb-3">
<label asp-for="Stock" class="control-label"></label>
<input asp-for="Stock" class="form-control" type="number" />
<span asp-validation-for="Stock" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Save" class="btn btn-primary" />
<a asp-action="Index" class="btn btn-secondary">Back to List</a>
</div>
</form>
</div>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
Delete confirmation
Create
@model ProductCatalog.Models.Product
@{
ViewData["Title"] = "Delete Product";
}
<h1>Delete Product</h1>
<h3>Are you sure you want to delete this product?</h3>
<div>
<hr />
<dl class="row">
<dt class="col-sm-3">Name</dt>
<dd class="col-sm-9">@Html.DisplayFor(model => model.Name)</dd>
<dt class="col-sm-3">Description</dt>
<dd class="col-sm-9">@Html.DisplayFor(model => model.Description)</dd>
<dt class="col-sm-3">Price</dt>
<dd class="col-sm-9">$@Model.Price.ToString("F2")</dd>
<dt class="col-sm-3">Stock</dt>
<dd class="col-sm-9">@Html.DisplayFor(model => model.Stock)</dd>
<dt class="col-sm-3">Created At</dt>
<dd class="col-sm-9">@Model.CreatedAt.ToString("yyyy-MM-dd HH:mm")</dd>
</dl>
<form asp-action="Delete">
<input type="hidden" asp-for="Id" />
<input type="submit" value="Delete" class="btn btn-danger" />
<a asp-action="Index" class="btn btn-secondary">Back to List</a>
</form>
</div>
Create
@model ProductCatalog.Models.Product
@{
ViewData["Title"] = "Product Details";
}
<h1>Product Details</h1>
<div>
<hr />
<dl class="row">
<dt class="col-sm-3">Name</dt>
<dd class="col-sm-9">@Html.DisplayFor(model => model.Name)</dd>
<dt class="col-sm-3">Description</dt>
<dd class="col-sm-9">@Html.DisplayFor(model => model.Description)</dd>
<dt class="col-sm-3">Price</dt>
<dd class="col-sm-9">$@Model.Price.ToString("F2")</dd>
<dt class="col-sm-3">Stock</dt>
<dd class="col-sm-9">@Html.DisplayFor(model => model.Stock)</dd>
<dt class="col-sm-3">Created At</dt>
<dd class="col-sm-9">@Model.CreatedAt.ToString("yyyy-MM-dd HH:mm")</dd>
</dl>
</div>
<div>
<a asp-action="Edit" asp-route-id="@Model.Id" class="btn btn-warning">Edit</a>
<a asp-action="Index" class="btn btn-secondary">Back to List</a>
</div>
Running the application
Start the application:
dotnet run
Or with hot reload for development:
dotnet watch run
The application will be available at:
https://localhost:5001 (HTTPS)http://localhost:5000 (HTTP)
Open your browser and navigate to the Products page to test CRUD operations.
Docker cleanup
When you're done working with the database, you can clean up Docker resources:
Stop and remove the container:
docker stop mssql-db
docker rm mssql-db
Remove the data volume:
rm -rf mssql-data
To restart fresh:
docker run \
--platform linux/amd64 \
-e "ACCEPT_EULA=Y" \
-e "SA_PASSWORD=YourStrong!Passw0rd" \
-p 1433:1433 \
--name mssql-db \
-v "$(pwd)/mssql-data":/var/opt/mssql \
-d mcr.microsoft.com/mssql/server:2022-latest
Troubleshooting
Connection timeout errors:
- Verify Docker container is running:
docker ps - Check container logs:
docker logs mssql-db - Ensure port 1433 is not in use by another service
Migration errors:
- Delete
Migrations/ folder and recreate:dotnet ef migrations add InitialCreate - Drop the database and reapply:
dotnet ef database drop thendotnet ef database update
Platform issues on Apple Silicon:
- Always use
--platform linux/amd64 flag when running MSSQL container - Enable Rosetta in Docker Desktop: Settings → Features in development → Use Rosetta for x86/amd64 emulation
VS Code extensions for better experience:
- C# Dev Kit
- C# Extensions
- Docker
- SQL Server (mssql)
With this setup, you have a fully functional ASP.NET Core MVC application with CRUD operations connected to MSSQL running in Docker on Mac.
