diff --git a/DALTests/UserAccountRepositoryTests.cs b/DALTests/UserAccountRepositoryTests.cs index 771b948..0d39440 100644 --- a/DALTests/UserAccountRepositoryTests.cs +++ b/DALTests/UserAccountRepositoryTests.cs @@ -9,7 +9,7 @@ namespace DALTests { public class UserAccountRepositoryTests { - private readonly UserAccountRepository _repository; + private readonly IUserAccountRepository _repository; public UserAccountRepositoryTests() { diff --git a/DataAccessLayer/entities/UserAccount.cs b/DataAccessLayer/Entities/UserAccount.cs similarity index 100% rename from DataAccessLayer/entities/UserAccount.cs rename to DataAccessLayer/Entities/UserAccount.cs diff --git a/DataAccessLayer/entities/UserCredential.cs b/DataAccessLayer/Entities/UserCredential.cs similarity index 100% rename from DataAccessLayer/entities/UserCredential.cs rename to DataAccessLayer/Entities/UserCredential.cs diff --git a/DataAccessLayer/entities/UserVerification.cs b/DataAccessLayer/Entities/UserVerification.cs similarity index 100% rename from DataAccessLayer/entities/UserVerification.cs rename to DataAccessLayer/Entities/UserVerification.cs diff --git a/DataAccessLayer/Repositories/IUserAccountRepository.cs b/DataAccessLayer/Repositories/IUserAccountRepository.cs new file mode 100644 index 0000000..8ffc1d4 --- /dev/null +++ b/DataAccessLayer/Repositories/IUserAccountRepository.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using DataAccessLayer.Entities; + +namespace DataAccessLayer +{ + public interface IUserAccountRepository : IRepository + { + IEnumerable GetAll(); + UserAccount? GetByUsername(string username); + UserAccount? GetByEmail(string email); + } +} diff --git a/DataAccessLayer/Repositories/UserAccountRepository.cs b/DataAccessLayer/Repositories/UserAccountRepository.cs new file mode 100644 index 0000000..652ddf0 --- /dev/null +++ b/DataAccessLayer/Repositories/UserAccountRepository.cs @@ -0,0 +1,163 @@ +using System; +using System.Collections.Generic; +using DataAccessLayer.Entities; +using Microsoft.Data.SqlClient; + +namespace DataAccessLayer +{ + public class UserAccountRepository : IUserAccountRepository + { + private readonly string _connectionString; + public UserAccountRepository() + { + // Retrieve the connection string from environment variables + _connectionString = + Environment.GetEnvironmentVariable("DB_CONNECTION_STRING") + ?? throw new InvalidOperationException( + "The connection string is not set in the environment variables." + ); + } + + public void Add(UserAccount userAccount) + { + using SqlConnection connection = new(_connectionString); + using SqlCommand command = new("usp_CreateUserAccount", connection); + command.CommandType = System.Data.CommandType.StoredProcedure; + AddUserAccountCreateParameters(command, userAccount); + connection.Open(); + command.ExecuteNonQuery(); + } + + public UserAccount? GetById(Guid id) + { + using SqlConnection connection = new(_connectionString); + using SqlCommand command = new( + "usp_GetUserAccountById", + connection + ); + command.CommandType = System.Data.CommandType.StoredProcedure; + command.Parameters.AddWithValue("@UserAccountId", id); + connection.Open(); + + using SqlDataReader reader = command.ExecuteReader(); + return reader.Read() ? MapUserAccount(reader) : null; + } + + public void Update(UserAccount userAccount) + { + using SqlConnection connection = new(_connectionString); + using SqlCommand command = new("usp_UpdateUserAccount", connection); + command.CommandType = System.Data.CommandType.StoredProcedure; + AddUserAccountUpdateParameters(command, userAccount); + connection.Open(); + command.ExecuteNonQuery(); + } + + public void Delete(Guid id) + { + using SqlConnection connection = new(_connectionString); + using SqlCommand command = new( + "usp_DeleteUserAccount", + connection + ); + command.CommandType = System.Data.CommandType.StoredProcedure; + command.Parameters.AddWithValue("@UserAccountId", id); + connection.Open(); + command.ExecuteNonQuery(); + } + + public IEnumerable GetAll() + { + using SqlConnection connection = new(_connectionString); + using SqlCommand command = new( + "usp_GetAllUserAccounts", + connection + ); + command.CommandType = System.Data.CommandType.StoredProcedure; + connection.Open(); + + using SqlDataReader reader = command.ExecuteReader(); + List users = new(); + while (reader.Read()) + { + users.Add(MapUserAccount(reader)); + } + + return users; + } + + public UserAccount? GetByUsername(string username) + { + using SqlConnection connection = new(_connectionString); + using SqlCommand command = new( + "usp_GetUserAccountByUsername", + connection + ); + command.CommandType = System.Data.CommandType.StoredProcedure; + command.Parameters.AddWithValue("@Username", username); + connection.Open(); + + using SqlDataReader reader = command.ExecuteReader(); + return reader.Read() ? MapUserAccount(reader) : null; + } + + public UserAccount? GetByEmail(string email) + { + using SqlConnection connection = new(_connectionString); + using SqlCommand command = new( + "usp_GetUserAccountByEmail", + connection + ); + command.CommandType = System.Data.CommandType.StoredProcedure; + command.Parameters.AddWithValue("@Email", email); + connection.Open(); + + using SqlDataReader reader = command.ExecuteReader(); + return reader.Read() ? MapUserAccount(reader) : null; + } + + private static void AddUserAccountCreateParameters( + SqlCommand command, + UserAccount userAccount + ) + { + command.Parameters.AddWithValue( + "@UserAccountId", + userAccount.UserAccountID + ); + command.Parameters.AddWithValue("@Username", userAccount.Username); + command.Parameters.AddWithValue("@FirstName", userAccount.FirstName); + command.Parameters.AddWithValue("@LastName", userAccount.LastName); + command.Parameters.AddWithValue("@Email", userAccount.Email); + command.Parameters.AddWithValue("@DateOfBirth", userAccount.DateOfBirth); + } + + private static void AddUserAccountUpdateParameters( + SqlCommand command, + UserAccount userAccount + ) + { + AddUserAccountCreateParameters(command, userAccount); + command.Parameters.AddWithValue( + "@UserAccountId", + userAccount.UserAccountID + ); + } + + private static UserAccount MapUserAccount(SqlDataReader reader) + { + return new UserAccount + { + UserAccountID = reader.GetGuid(0), + Username = reader.GetString(1), + FirstName = reader.GetString(2), + LastName = reader.GetString(3), + Email = reader.GetString(4), + CreatedAt = reader.GetDateTime(5), + UpdatedAt = reader.IsDBNull(6) ? null : reader.GetDateTime(6), + DateOfBirth = reader.GetDateTime(7), + Timer = reader.IsDBNull(8) ? null : (byte[])reader[8], + }; + } + } +} diff --git a/DataAccessLayer/Class1.cs b/DataAccessLayer/Sql/DatabaseHelper.cs similarity index 98% rename from DataAccessLayer/Class1.cs rename to DataAccessLayer/Sql/DatabaseHelper.cs index 7a301f2..53171a0 100644 --- a/DataAccessLayer/Class1.cs +++ b/DataAccessLayer/Sql/DatabaseHelper.cs @@ -2,7 +2,7 @@ using System.Data; using Microsoft.Data.SqlClient; -namespace DataAccessLayer +namespace DataAccessLayer.Sql { public class DatabaseHelper { diff --git a/DataLayer/crud/UserAccount.sql b/DataAccessLayer/Sql/crud/UserAccount.sql similarity index 52% rename from DataLayer/crud/UserAccount.sql rename to DataAccessLayer/Sql/crud/UserAccount.sql index 06b19e4..61b85dc 100644 --- a/DataLayer/crud/UserAccount.sql +++ b/DataAccessLayer/Sql/crud/UserAccount.sql @@ -3,6 +3,7 @@ GO CREATE OR ALTER PROCEDURE usp_CreateUserAccount ( + @UserAccountId UNIQUEIDENTIFIER = NULL, @Username VARCHAR(64), @FirstName NVARCHAR(128), @LastName NVARCHAR(128), @@ -17,6 +18,7 @@ BEGIN INSERT INTO UserAccount ( + UserAccountID, Username, FirstName, LastName, @@ -25,6 +27,7 @@ BEGIN ) VALUES ( + COALESCE(@UserAccountId, NEWID()), @Username, @FirstName, @LastName, @@ -40,7 +43,7 @@ GO CREATE OR ALTER PROCEDURE usp_DeleteUserAccount ( - @UserAccountId INT + @UserAccountId UNIQUEIDENTIFIER ) AS BEGIN @@ -70,7 +73,7 @@ CREATE OR ALTER PROCEDURE usp_UpdateUserAccount @LastName NVARCHAR(128), @DateOfBirth DATETIME, @Email VARCHAR(128), - @UserAccountId GUID + @UserAccountId UNIQUEIDENTIFIER ) AS BEGIN @@ -98,3 +101,87 @@ BEGIN COMMIT TRANSACTION END; GO + +CREATE OR ALTER PROCEDURE usp_GetUserAccountById +( + @UserAccountId UNIQUEIDENTIFIER +) +AS +BEGIN + SET NOCOUNT ON; + + SELECT UserAccountID, + Username, + FirstName, + LastName, + Email, + CreatedAt, + UpdatedAt, + DateOfBirth, + Timer + FROM dbo.UserAccount + WHERE UserAccountID = @UserAccountId; +END; +GO + +CREATE OR ALTER PROCEDURE usp_GetAllUserAccounts +AS +BEGIN + SET NOCOUNT ON; + + SELECT UserAccountID, + Username, + FirstName, + LastName, + Email, + CreatedAt, + UpdatedAt, + DateOfBirth, + Timer + FROM dbo.UserAccount; +END; +GO + +CREATE OR ALTER PROCEDURE usp_GetUserAccountByUsername +( + @Username VARCHAR(64) +) +AS +BEGIN + SET NOCOUNT ON; + + SELECT UserAccountID, + Username, + FirstName, + LastName, + Email, + CreatedAt, + UpdatedAt, + DateOfBirth, + Timer + FROM dbo.UserAccount + WHERE Username = @Username; +END; +GO + +CREATE OR ALTER PROCEDURE usp_GetUserAccountByEmail +( + @Email VARCHAR(128) +) +AS +BEGIN + SET NOCOUNT ON; + + SELECT UserAccountID, + Username, + FirstName, + LastName, + Email, + CreatedAt, + UpdatedAt, + DateOfBirth, + Timer + FROM dbo.UserAccount + WHERE Email = @Email; +END; +GO diff --git a/DataAccessLayer/UserAccountRepository.cs b/DataAccessLayer/UserAccountRepository.cs deleted file mode 100644 index 83c8779..0000000 --- a/DataAccessLayer/UserAccountRepository.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Data; -using DataAccessLayer.Entities; -using Microsoft.Data.SqlClient; - -namespace DataAccessLayer -{ - public class UserAccountRepository : IRepository - { - private readonly string _connectionString; - - public UserAccountRepository() - { - // Retrieve the connection string from environment variables - _connectionString = - Environment.GetEnvironmentVariable("DB_CONNECTION_STRING") - ?? throw new InvalidOperationException( - "The connection string is not set in the environment variables." - ); - } - - public void Add(UserAccount userAccount) - { - - } - - public UserAccount? GetById(Guid id) - { - - return null; - } - - public void Update(UserAccount userAccount) - { - - } - - public void Delete(Guid id) - { - - } - - public IEnumerable GetAll() - { - return new List - { - }; - } - } -} diff --git a/README.md b/README.md new file mode 100644 index 0000000..7b62e9d --- /dev/null +++ b/README.md @@ -0,0 +1,51 @@ +# Biergarten SQL Server - Architecture Overview + +This solution is a monolith-oriented Web API with a layered structure. The current focus is a SQL Server-backed data layer with stored procedures and a repository-based DAL. + +## High-level projects + +- `WebAPI/` - ASP.NET Core API endpoints (controllers) and application entrypoint. +- `BusinessLayer/` - Intended home for domain/business logic (currently minimal). +- `DataAccessLayer/` - Repository implementations, entities (POCOs), and SQL helpers. +- `DataLayer/` - Database schema, seed scripts, and data sources. +- `WebCrawler/` - Separate crawler executable. +- `DALTests/` - Data access tests. + +## Data access architecture + +- **Entities (POCOs)** live in `DataAccessLayer/Entities/`. +- **Repositories** live in `DataAccessLayer/Repositories/` and implement interfaces like `IUserAccountRepository`. +- **SQL execution** lives in `DataAccessLayer/Sql/`. +- **Stored procedures** for CRUD live under `DataAccessLayer/Sql/crud/` and are invoked by repositories. + +Example flow: + +``` +WebAPI Controller -> IUserAccountRepository -> UserAccountRepository -> stored procedure +``` + +The repositories are currently responsible for: +- Opening connections using `DB_CONNECTION_STRING` +- Executing stored procedures +- Mapping result sets to POCOs + +## Database schema and seed + +- `DataLayer/schema.sql` contains the database schema definitions. +- `DataLayer/seed/SeedDB.cs` provides seeding and stored procedure/function loading. +- Stored procedure scripts are organized under `DataAccessLayer/Sql/crud/` (UserAccount and related). + +## Key conventions + +- **Environment variables**: `DB_CONNECTION_STRING` is required for DAL and seed tooling. +- **Stored procedures**: CRUD operations use `usp_*` procedures. +- **Rowversion** columns are represented as `byte[]` in entities (e.g., `Timer`). + +## Suggested dependency direction + +``` +WebAPI -> BusinessLayer -> DataAccessLayer -> SQL Server + -> DataLayer (schema/seed/scripts) +``` + +Keep business logic in `BusinessLayer` and avoid direct SQL or ADO code outside `DataAccessLayer`. diff --git a/WebAPI/Controllers/BeersController.cs b/WebAPI/Controllers/BeersController.cs deleted file mode 100644 index efeea56..0000000 --- a/WebAPI/Controllers/BeersController.cs +++ /dev/null @@ -1,75 +0,0 @@ -using Microsoft.AspNetCore.Mvc; - -namespace WebAPI.Controllers -{ - [ApiController] - [Route("api/beers")] - public class BeersController : ControllerBase - { - [HttpGet] - public IActionResult GetBeers([FromQuery] int page_num, [FromQuery] int page_size) - { - return Ok(); - } - - [HttpGet("search")] - public IActionResult SearchBeers([FromQuery] string search) - { - return Ok(); - } - - [HttpGet("styles")] - public IActionResult GetBeerStyles([FromQuery] int page_num, [FromQuery] int page_size) - { - return Ok(); - } - - [HttpPost("styles/create")] - public IActionResult CreateBeerStyle([FromBody] BeerStyleCreateRequest request) - { - return Ok(); - } - - [HttpPut("{postId}")] - public IActionResult EditBeer(string postId, [FromBody] BeerEditRequest request) - { - return Ok(); - } - - [HttpDelete("{postId}")] - public IActionResult DeleteBeer(string postId) - { - return Ok(); - } - - [HttpGet("{postId}/recommendations")] - public IActionResult GetBeerRecommendations([FromQuery] int page_num, [FromQuery] int page_size, string postId) - { - return Ok(); - } - - [HttpPost("{postId}/comments")] - public IActionResult AddBeerComment(string postId, [FromBody] BeerCommentRequest request) - { - return Ok(); - } - - [HttpGet("{postId}/comments")] - public IActionResult GetBeerComments([FromQuery] int page_num, [FromQuery] int page_size, string postId) - { - return Ok(); - } - - [HttpPut("{postId}/comments/{commentId}")] - public IActionResult EditBeerComment(string postId, string commentId, [FromBody] BeerCommentRequest request) - { - return Ok(); - } - - [HttpDelete("{postId}/comments/{commentId}")] - public IActionResult DeleteBeerComment(string postId, string commentId) - { - return Ok(); - } - } -} \ No newline at end of file diff --git a/WebAPI/Controllers/BreweriesController.cs b/WebAPI/Controllers/BreweriesController.cs deleted file mode 100644 index dd6bff0..0000000 --- a/WebAPI/Controllers/BreweriesController.cs +++ /dev/null @@ -1,57 +0,0 @@ -using Microsoft.AspNetCore.Mvc; - -namespace WebAPI.Controllers -{ - [ApiController] - [Route("api/breweries")] - public class BreweriesController : ControllerBase - { - [HttpGet] - public IActionResult GetBreweries([FromQuery] int page_num, [FromQuery] int page_size) - { - return Ok(); - } - - [HttpGet("map")] - public IActionResult GetBreweriesMap([FromQuery] int page_num, [FromQuery] int page_size) - { - return Ok(); - } - - [HttpPut("{postId}")] - public IActionResult EditBrewery(string postId, [FromBody] BreweryEditRequest request) - { - return Ok(); - } - - [HttpDelete("{postId}")] - public IActionResult DeleteBrewery(string postId) - { - return Ok(); - } - - [HttpPost("{postId}/comments")] - public IActionResult AddBreweryComment(string postId, [FromBody] BreweryCommentRequest request) - { - return Ok(); - } - - [HttpGet("{postId}/comments")] - public IActionResult GetBreweryComments([FromQuery] int page_num, [FromQuery] int page_size, string postId) - { - return Ok(); - } - - [HttpPut("{postId}/comments/{commentId}")] - public IActionResult EditBreweryComment(string postId, string commentId, [FromBody] BreweryCommentRequest request) - { - return Ok(); - } - - [HttpDelete("{postId}/comments/{commentId}")] - public IActionResult DeleteBreweryComment(string postId, string commentId) - { - return Ok(); - } - } -} \ No newline at end of file diff --git a/WebAPI/Controllers/UsersController.cs b/WebAPI/Controllers/UsersController.cs index 155d2e6..0526c9d 100644 --- a/WebAPI/Controllers/UsersController.cs +++ b/WebAPI/Controllers/UsersController.cs @@ -1,5 +1,6 @@ -using Microsoft.AspNetCore.Mvc; using DataAccessLayer; +using DataAccessLayer.Entities; +using Microsoft.AspNetCore.Mvc; namespace WebAPI.Controllers { @@ -7,7 +8,7 @@ namespace WebAPI.Controllers [Route("api/users")] public class UsersController : ControllerBase { - private readonly UserAccountRepository _userAccountRepository; + private readonly IUserAccountRepository _userAccountRepository; public UsersController() { @@ -15,6 +16,7 @@ namespace WebAPI.Controllers } // all users + [HttpGet] [HttpGet("users")] public IActionResult GetAllUsers() { @@ -22,5 +24,61 @@ namespace WebAPI.Controllers return Ok(users); } + [HttpGet("{id:guid}")] + public IActionResult GetUserById(Guid id) + { + var user = _userAccountRepository.GetById(id); + return user is null ? NotFound() : Ok(user); + } + + [HttpGet("by-username/{username}")] + public IActionResult GetUserByUsername(string username) + { + var user = _userAccountRepository.GetByUsername(username); + return user is null ? NotFound() : Ok(user); + } + + [HttpGet("by-email/{email}")] + public IActionResult GetUserByEmail(string email) + { + var user = _userAccountRepository.GetByEmail(email); + return user is null ? NotFound() : Ok(user); + } + + [HttpPost] + public IActionResult CreateUser([FromBody] UserAccount userAccount) + { + if (userAccount.UserAccountID == Guid.Empty) + { + userAccount.UserAccountID = Guid.NewGuid(); + } + + _userAccountRepository.Add(userAccount); + return CreatedAtAction( + nameof(GetUserById), + new { id = userAccount.UserAccountID }, + userAccount + ); + } + + [HttpPut("{id:guid}")] + public IActionResult UpdateUser(Guid id, [FromBody] UserAccount userAccount) + { + if (userAccount.UserAccountID != Guid.Empty && userAccount.UserAccountID != id) + { + return BadRequest("UserAccountID does not match route id."); + } + + userAccount.UserAccountID = id; + _userAccountRepository.Update(userAccount); + return NoContent(); + } + + [HttpDelete("{id:guid}")] + public IActionResult DeleteUser(Guid id) + { + _userAccountRepository.Delete(id); + return NoContent(); + } } -} \ No newline at end of file +} diff --git a/WebAPI/Program.cs b/WebAPI/Program.cs index 5421649..5806052 100644 --- a/WebAPI/Program.cs +++ b/WebAPI/Program.cs @@ -30,17 +30,22 @@ catch var builder = WebApplication.CreateBuilder(args); -// Add services to the container. -// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi -builder.Services.AddOpenApi(); +// Add services to the container. +// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi +builder.Services.AddControllers(); +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); +builder.Services.AddOpenApi(); var app = builder.Build(); // Configure the HTTP request pipeline. -if (app.Environment.IsDevelopment()) -{ - app.MapOpenApi(); -} +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); + app.MapOpenApi(); +} app.UseHttpsRedirection(); @@ -58,24 +63,6 @@ var summaries = new[] "Scorching", }; -app.MapGet( - "/weatherforecast", - () => - { - var forecast = Enumerable - .Range(1, 5) - .Select(index => new WeatherForecast( - DateOnly.FromDateTime(DateTime.Now.AddDays(index)), - Random.Shared.Next(-20, 55), - summaries[Random.Shared.Next(summaries.Length)] - )) - .ToArray(); - return forecast; - } - ) - .WithName("GetWeatherForecast"); - -// Register controllers app.MapControllers(); app.Run(); diff --git a/WebAPI/WebAPI.csproj b/WebAPI/WebAPI.csproj index 521ee8f..36eab33 100644 --- a/WebAPI/WebAPI.csproj +++ b/WebAPI/WebAPI.csproj @@ -5,9 +5,10 @@ enable - - - + + + + diff --git a/WebAPI/WebAPI.http b/WebAPI/WebAPI.http index f3c8e91..5f2b96f 100644 --- a/WebAPI/WebAPI.http +++ b/WebAPI/WebAPI.http @@ -1,6 +1,64 @@ @WebAPI_HostAddress = http://localhost:5069 -GET {{WebAPI_HostAddress}}/weatherforecast/ -Accept: application/json - -### +GET {{WebAPI_HostAddress}}/weatherforecast/ +Accept: application/json + +### + +GET {{WebAPI_HostAddress}}/api/users +Accept: application/json + +### + +GET {{WebAPI_HostAddress}}/api/users/{{userId}} +Accept: application/json + +### + +GET {{WebAPI_HostAddress}}/api/users/by-username/{{username}} +Accept: application/json + +### + +GET {{WebAPI_HostAddress}}/api/users/by-email/{{email}} +Accept: application/json + +### + +POST {{WebAPI_HostAddress}}/api/users +Content-Type: application/json +Accept: application/json + +{ + "userAccountID": "00000000-0000-0000-0000-000000000000", + "username": "testuser", + "firstName": "Test", + "lastName": "User", + "email": "testuser@example.com", + "createdAt": "2025-01-01T00:00:00Z", + "updatedAt": null, + "dateOfBirth": "1990-01-01T00:00:00Z", + "timer": null +} + +### + +PUT {{WebAPI_HostAddress}}/api/users/{{userId}} +Content-Type: application/json +Accept: application/json + +{ + "userAccountID": "{{userId}}", + "username": "testuser", + "firstName": "Updated", + "lastName": "User", + "email": "testuser@example.com", + "createdAt": "2025-01-01T00:00:00Z", + "updatedAt": "2025-02-01T00:00:00Z", + "dateOfBirth": "1990-01-01T00:00:00Z", + "timer": null +} + +### + +DELETE {{WebAPI_HostAddress}}/api/users/{{userId}} diff --git a/docker-compose.yml b/docker-compose.yml index 9e9e004..a4e7d68 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,40 +17,6 @@ services: retries: 12 networks: - devnet - - redis: - image: redis:7 - container_name: redis - env_file: - - .env - networks: - - devnet - healthcheck: - test: ["CMD", "redis-cli", "ping"] - interval: 10s - timeout: 5s - retries: 5 - - dotnet: - image: mcr.microsoft.com/dotnet/sdk:10.0 - container_name: dotnet-sdk - tty: true - stdin_open: true - volumes: - - ./:/home/dev/projects - - nuget-cache:/home/dev/.nuget/packages - - ~/.gitconfig:/home/dev/.gitconfig:ro - working_dir: /home/dev/projects - environment: - DOTNET_CLI_TELEMETRY_OPTOUT: "1" - HOME: /home/dev - USER: dev - DB_CONNECTION_STRING: "Server=sqlserver,1433;User Id=sa;Password=${SA_PASSWORD};Encrypt=True;TrustServerCertificate=True;Connection Timeout=30;Database=${DB_NAME};" - REDIS_URL: "${REDIS_URL}" - user: root - networks: - - devnet - volumes: sqlserverdata: nuget-cache: