6.6 KiB
+++ categories = ["software"] tags = ["mssql"] date = 2024-10-01T14:20:28Z description = "Moving a Blazor recipe application from an in-memory store to containerized SQL Server with Entity Framework, users, and protected connection strings." draft = false slug = "mssql-db-config" title = "💽 Configuring Microsoft SQL Server Database" +++
I am using Microsoft's Blazor framework to create a Webassembly app that manages my recipes. I began building this web app using an in-memory database, which means the data does not persist after the program stops running. Each time I build, the app rebuilds the database with brand new data. This is annoying and unnecessary now that the general structure of the database has been established. I will transition the app to instead use a database on an on-premise server.
Docker
As with nearly every other service I host, I will run an instance of SQL Server in a Docker container. Microsoft provides a shell command that will get an instance of Linux-based SQL Server running.
docker run -e "ACCEPT_EULA=Y" -e "MSSQL_SA_PASSWORD" \
-p 1433:1433 --name sql1 --hostname sql1 \
-d \
mcr.microsoft.com/mssql/server:2022-latest
I typically use Docker Compose to manage my services, so I will transform this command into a docker-compose.yml file. There are additional pieces of configuration missing here: timezone, product ID, and data persistence across container restarts. I add them in the final configuration file. Additionally, the System Administrator (user sa) password is stored in a .env file that lives in the root directory of the host.
nicholas@nas:/$ cd /hdd1/nicholas/docker/dockerfiles/
nicholas@nas:/hdd1/nicholas/docker/dockerfiles/$ mkdir mssql && cd mssql
nicholas@nas:/hdd1/nicholas/docker/dockerfiles/$ nano docker-compose.yml
services:
mssql:
image: mcr.microsoft.com/mssql/server:2022-latest
container_name: mssql
hostname: mssql
environment:
- ACCEPT_EULA=Y
- MSSQL_SA_PASSWORD=${MSSQL_SA_PASSWORD}
- TZ=America/Chicago
restart: always
volumes:
- ./data:/var/opt/mssql/data
- ./logs:/var/opt/mssql/log
- ./secrets:/var/opt/mssql/secrets
ports:
- "1433:1433"
The container runs as user mssql, so I will need to give my container directory permissions to this user.
sudo chown -R 10001:10001 ./mssql/
sudo chmod -R 755 ./mssql/
Next I start the container.
sudo docker compose up -d
[+] Running 2/4
⠙ mssql [⣿⡀⣿] 168.3MB / 578.3MB Pulling 11.2s
✔ 3d29464607d8 Pull complete 5.9s
⠙ b14d3f02d050 Downloading [======> ]... 10.1s
✔ c4ebe057087c Download complete 3.4sl
The server is ready to accept connections now.
Microsoft SQL Server Management Studio
I will be using SSMS to connect to and configure the server.
The server name is the host name of my server, which is nas. I already configured the login credentials in the docker-compose.yml and .env files.
Here I create a database Formulation and the two tables: Ingredient and IngredientRelationship. Like I said, this could have been a spreadsheet.
CREATE DATABASE Formulation;
USE Formulation;
CREATE TABLE Ingredient (
Id UNIQUEIDENTIFIER PRIMARY KEY,
Name NVARCHAR(100) NOT NULL,
Calories FLOAT NOT NULL,
Protein FLOAT NOT NULL,
Fat FLOAT NOT NULL,
Carbohydrates FLOAT NOT NULL,
Fiber FLOAT NOT NULL,
Sugar FLOAT NOT NULL,
Sodium FLOAT NOT NULL
);
CREATE TABLE IngredientRelationship (
Id UNIQUEIDENTIFIER PRIMARY KEY,
ParentId UNIQUEIDENTIFIER NOT NULL,
ChildId UNIQUEIDENTIFIER NOT NULL,
Proportion FLOAT NOT NULL,
FOREIGN KEY (ParentId) REFERENCES Ingredient(Id),
FOREIGN KEY (ChildId) REFERENCES Ingredient(Id)
);
Mapping Entity Classes to Database Tables with Entity Framework
I am using Entity Framework in my web app, so I needed to define my database structure so that it mirrored the C# Entity Class I already defined. The Ingredient table in the database maps onto the C# Entity Class Ingredient.cs. The same is true for the IngredientRelationship table and IngredientRelationship.cs.
namespace formulation.Common.Models
{
[Table("Ingredient")]
public class Ingredient
{
[Key]
public Guid Id { get; set; }
[Required]
[StringLength(100, ErrorMessage = "Name cannot be longer than 100 characters.")]
public string Name { get; set; } = string.Empty;
public double Calories { get; set; } // in kcal
public double Protein { get; set; } // in grams
public double Fat { get; set; } // in grams
public double Carbohydrates { get; set; } // in grams
public double Fiber { get; set; } // in grams
public double Sugar { get; set; } // in grams
public double Sodium { get; set; } // in milligrams
public ICollection? Ingredients { get; set; }
public ICollection? ParentIngredients { get; set; }
public Ingredient()
{
Ingredients = new List();
ParentIngredients = new List();
}
}
}
Create SQL Server User for App Connection
Now I need to create a dedicated SQL Server user for my app – one with only limited privileges. It is best practice to abide by the principle of least privilege, which states that a user should be granted the minimum level of access necessary to perform the required function. Therefore it is not recommended to use the sa user or any high-privilege accounts for an app connection.
CREATE LOGIN Formulation WITH PASSWORD = '';
USE Formulation;
CREATE USER Formulation FOR LOGIN Formulation;
EXEC sp_addrolemember 'db_datareader', 'Formulation'; -- Read permissions
EXEC sp_addrolemember 'db_datawriter', 'Formulation'; -- Write permissions
Connection Strings & Secrets
The next step is to configure a new Connected Service in Visual Studio. First I select the correct service dependency: SQL Server Database - On-premise
Next I enter the login credentials User name, Password, Server name, Database name which were configured in the steps above. I click test connection to verify that this is working as expected. It is successful.
Visual Studio automatically writes the connection string using this information. I save the connection strings in a local secrets file Secrets.json.
Using the App Connection User
Finally I can reference this connection string in Program.cs to connect to the SQL Server.
builder.Services.AddDbContext(opt => opt.UseSqlServer(builder.Configuration.GetConnectionString("Formulation")));
Done.