diff --git a/AppWithSqlServer/OnlineShop/OnlineShop.ApiService/Model/Product.cs b/AppWithSqlServer/OnlineShop/OnlineShop.ApiService/Model/Product.cs new file mode 100644 index 0000000..da14c68 --- /dev/null +++ b/AppWithSqlServer/OnlineShop/OnlineShop.ApiService/Model/Product.cs @@ -0,0 +1,19 @@ +using System.ComponentModel.DataAnnotations; + +namespace OnlineShop.ApiService.Model; + +public class Product +{ + [Key] + public int Id { get; set; } + + [MaxLength(100)] + string Title { get; set; } = string.Empty; + + [MaxLength(2100)] + public string Summary { get; set; } = string.Empty; + + public decimal Price { get; set; } + + public DateTime DateAdded { get; set; } +} diff --git a/AppWithSqlServer/OnlineShop/OnlineShop.ApiService/OnlineShop.ApiService.csproj b/AppWithSqlServer/OnlineShop/OnlineShop.ApiService/OnlineShop.ApiService.csproj index 94b4456..a2a53f3 100644 --- a/AppWithSqlServer/OnlineShop/OnlineShop.ApiService/OnlineShop.ApiService.csproj +++ b/AppWithSqlServer/OnlineShop/OnlineShop.ApiService/OnlineShop.ApiService.csproj @@ -1,4 +1,4 @@ - + net10.0 @@ -11,6 +11,7 @@ + diff --git a/AppWithSqlServer/OnlineShop/OnlineShop.ApiService/Program.cs b/AppWithSqlServer/OnlineShop/OnlineShop.ApiService/Program.cs index 19a9669..047998d 100644 --- a/AppWithSqlServer/OnlineShop/OnlineShop.ApiService/Program.cs +++ b/AppWithSqlServer/OnlineShop/OnlineShop.ApiService/Program.cs @@ -1,5 +1,8 @@ using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Data.SqlClient; using OnlineShop.ApiService; +using OnlineShop.ServiceDefaults.Dtos; var builder = WebApplication.CreateBuilder(args); @@ -23,9 +26,121 @@ builder.Services.AddAuthentication(options => .AddJwtBearer() .ConfigureApiJwt(); +builder.AddSqlServerClient("sqldb"); var app = builder.Build(); +using (var scope = app.Services.CreateScope()) +{ + var connection = scope.ServiceProvider.GetRequiredService(); + connection.Open(); + + var createDbCommand = new SqlCommand(@" + IF NOT EXISTS (SELECT * + FROM sys.databases + WHERE name = 'Shop') + BEGIN + CREATE DATABASE Shop; + END;", connection); + + createDbCommand.ExecuteNonQuery(); + + var createTableCommand = new SqlCommand(@" + USE Shop; + IF NOT EXISTS (SELECT * + FROM sysobjects + WHERE name='Products' and xtype='U') + CREATE TABLE Products ( + Id INT PRIMARY KEY IDENTITY, + Title VARCHAR(100) NOT NULL, + Summary NVARCHAR(2100) NOT NULL, + Price DECIMAL(18,2) NOT NULL, + DateAdded DATE NOT NULL, + )", connection); + + createTableCommand.ExecuteNonQuery(); + + var checkDataCommand = + new SqlCommand( + "SELECT COUNT(*) FROM Products", + connection); + + var count = (int)checkDataCommand + .ExecuteScalar(); + + if (count == 0) + { + var insertCommand = + new SqlCommand(""" + INSERT INTO Products (Title, Summary, Price, DateAdded) + VALUES + ( + 'Wireless Optical Mouse', + N'Ergonomic wireless optical mouse with adjustable DPI and long battery life, suitable for everyday office and home use.', + 24.99, + '2025-01-05' + ), + ( + 'Mechanical Gaming Keyboard', + N'RGB backlit mechanical keyboard with blue switches, anti-ghosting keys, and durable aluminum frame.', + 129.99, + '2025-01-06' + ), + ( + '27-inch 4K Monitor', + N'27-inch UHD 4K monitor with IPS panel, 3840x2160 resolution, HDR support, and ultra-thin bezels.', + 399.00, + '2025-01-07' + ), + ( + 'USB-C Docking Station', + N'Multi-port USB-C docking station with HDMI, DisplayPort, Ethernet, USB 3.0 ports, and 100W power delivery.', + 179.50, + '2025-01-08' + ), + ( + 'External SSD 1TB', + N'Portable 1TB external SSD with USB 3.2 Gen 2 support, delivering fast read/write speeds in a compact design.', + 149.99, + '2025-01-09' + ), + ( + 'Noise-Cancelling Headphones', + N'Over-ear wireless headphones with active noise cancellation, high-fidelity sound, and 30-hour battery life.', + 249.00, + '2025-01-10' + ), + ( + 'Webcam Full HD 1080p', + N'Full HD 1080p webcam with built-in microphone, autofocus, and low-light correction for video conferencing.', + 69.99, + '2025-01-11' + ), + ( + 'Gaming Laptop Backpack', + N'Water-resistant backpack designed for gaming laptops up to 17 inches, featuring padded compartments and USB charging port.', + 59.95, + '2025-01-12' + ), + ( + 'Wi-Fi 6 Router', + N'Dual-band Wi-Fi 6 router offering high-speed wireless connectivity, improved range, and support for multiple devices.', + 199.00, + '2025-01-13' + ), + ( + 'Portable Laser Printer', + N'Compact monochrome laser printer suitable for small offices, offering fast printing speeds and wireless connectivity.', + 289.99, + '2025-01-14' + ); + """, + connection); + + insertCommand.ExecuteNonQuery(); + } +} + app.UseExceptionHandler(); if (app.Environment.IsDevelopment()) @@ -33,23 +148,36 @@ if (app.Environment.IsDevelopment()) app.MapOpenApi(); } -string[] summaries = ["Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"]; +app.MapGet("/", () => "API service is running."); -app.MapGet("/", () => "API service is running. Navigate to /weatherforecast to see sample data."); +app.MapGet("/products", + ([FromServices] SqlConnection connection) => + { + connection.Open(); -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"); + var command = new SqlCommand(@" + USE Shop; + SELECT + Title, + Summary, + Price + FROM Products", connection); + var products = new List(); + + using (var reader = command.ExecuteReader()) + { + while (reader.Read()) + { + products.Add(new ProductDto( + Title: reader.GetString(0), + Summary: reader.GetString(1), + Price: reader.GetDecimal(2) + )); + } + + return products.ToArray(); + } + }); app.MapDefaultEndpoints(); diff --git a/AppWithSqlServer/OnlineShop/OnlineShop.AppHost/AppHost.cs b/AppWithSqlServer/OnlineShop/OnlineShop.AppHost/AppHost.cs index e9d3ab7..dbcfc5e 100644 --- a/AppWithSqlServer/OnlineShop/OnlineShop.AppHost/AppHost.cs +++ b/AppWithSqlServer/OnlineShop/OnlineShop.AppHost/AppHost.cs @@ -13,17 +13,22 @@ var idp = builder.AddKeycloakContainer( var cache = builder.AddRedis("cache"); +var sql = builder.AddSqlServer("sql").WithLifetime(ContainerLifetime.Persistent); +var sqldb = sql.AddDatabase("sqldb"); + var apiService = builder.AddProject("apiservice") .WithHttpHealthCheck("/health") .WithReference(idp) - .WaitFor(idp); - + .WaitFor(idp) + .WaitFor(sqldb) + .WithReference(sqldb); var webFrontend = builder .AddProject("webfrontend") .WithExternalHttpEndpoints() .WithHttpHealthCheck("/health") .WithReference(apiService) + .WaitFor(apiService) .WithReference(idp, env: "Identity__ClientSecret") .WaitFor(idp) .WithReference(cache) diff --git a/AppWithSqlServer/OnlineShop/OnlineShop.AppHost/OnlineShop.AppHost.csproj b/AppWithSqlServer/OnlineShop/OnlineShop.AppHost/OnlineShop.AppHost.csproj index ec5f46c..dd8fcc0 100644 --- a/AppWithSqlServer/OnlineShop/OnlineShop.AppHost/OnlineShop.AppHost.csproj +++ b/AppWithSqlServer/OnlineShop/OnlineShop.AppHost/OnlineShop.AppHost.csproj @@ -10,14 +10,13 @@ + - + diff --git a/AppWithSqlServer/OnlineShop/OnlineShop.ServiceDefaults/Dtos/ProductDto.cs b/AppWithSqlServer/OnlineShop/OnlineShop.ServiceDefaults/Dtos/ProductDto.cs new file mode 100644 index 0000000..8c91833 --- /dev/null +++ b/AppWithSqlServer/OnlineShop/OnlineShop.ServiceDefaults/Dtos/ProductDto.cs @@ -0,0 +1,7 @@ +namespace OnlineShop.ServiceDefaults.Dtos; + +public record ProductDto( + string Title, + string Summary, + decimal Price +); diff --git a/AppWithSqlServer/OnlineShop/OnlineShop.Web/Components/Layout/NavMenu.razor b/AppWithSqlServer/OnlineShop/OnlineShop.Web/Components/Layout/NavMenu.razor index 6eb2bb9..9cfcc40 100644 --- a/AppWithSqlServer/OnlineShop/OnlineShop.Web/Components/Layout/NavMenu.razor +++ b/AppWithSqlServer/OnlineShop/OnlineShop.Web/Components/Layout/NavMenu.razor @@ -13,17 +13,5 @@ Home - - - - diff --git a/AppWithSqlServer/OnlineShop/OnlineShop.Web/Components/Pages/Counter.razor b/AppWithSqlServer/OnlineShop/OnlineShop.Web/Components/Pages/Counter.razor deleted file mode 100644 index 1a4f8e7..0000000 --- a/AppWithSqlServer/OnlineShop/OnlineShop.Web/Components/Pages/Counter.razor +++ /dev/null @@ -1,19 +0,0 @@ -@page "/counter" -@rendermode InteractiveServer - -Counter - -

Counter

- -

Current count: @currentCount

- - - -@code { - private int currentCount = 0; - - private void IncrementCount() - { - currentCount++; - } -} diff --git a/AppWithSqlServer/OnlineShop/OnlineShop.Web/Components/Pages/Home.razor b/AppWithSqlServer/OnlineShop/OnlineShop.Web/Components/Pages/Home.razor index 9001e0b..f26bdf8 100644 --- a/AppWithSqlServer/OnlineShop/OnlineShop.Web/Components/Pages/Home.razor +++ b/AppWithSqlServer/OnlineShop/OnlineShop.Web/Components/Pages/Home.razor @@ -1,7 +1,47 @@ @page "/" +@using OnlineShop.ServiceDefaults.Dtos +@attribute [StreamRendering(true)] +@attribute [OutputCache(Duration = 5)] -Home +@inject ProductsApiClient ProductsApi -

Hello, world!

+Products + +

Products

+ +@if (products == null) +{ +

Loading...

+} +else +{ + + + + + + + + + + @foreach (var product in products) + { + + + + + + } + +
NamePrice (USD)Summary
@product.Title@product.Price@product.Summary
+} + +@code { + private ProductDto[]? products; + + protected override async Task OnInitializedAsync() + { + products = await ProductsApi.GetProductsAsync(); + } +} -Welcome to your new app. diff --git a/AppWithSqlServer/OnlineShop/OnlineShop.Web/Components/Pages/Weather.razor b/AppWithSqlServer/OnlineShop/OnlineShop.Web/Components/Pages/Weather.razor deleted file mode 100644 index e05ce9a..0000000 --- a/AppWithSqlServer/OnlineShop/OnlineShop.Web/Components/Pages/Weather.razor +++ /dev/null @@ -1,49 +0,0 @@ -@page "/weather" -@attribute [StreamRendering(true)] -@attribute [OutputCache(Duration = 5)] - -@inject WeatherApiClient WeatherApi - -Weather - -

Weather

- -

This component demonstrates showing data loaded from a backend API service.

- -@if (forecasts == null) -{ -

Loading...

-} -else -{ - - - - - - - - - - - @foreach (var forecast in forecasts) - { - - - - - - - } - -
DateTemp. (C)Temp. (F)Summary
@forecast.Date.ToShortDateString()@forecast.TemperatureC@forecast.TemperatureF@forecast.Summary
-} - -@code { - private WeatherForecast[]? forecasts; - - protected override async Task OnInitializedAsync() - { - forecasts = await WeatherApi.GetWeatherAsync(); - } -} diff --git a/AppWithSqlServer/OnlineShop/OnlineShop.Web/ProductsApiClient.cs b/AppWithSqlServer/OnlineShop/OnlineShop.Web/ProductsApiClient.cs new file mode 100644 index 0000000..0bd5dc7 --- /dev/null +++ b/AppWithSqlServer/OnlineShop/OnlineShop.Web/ProductsApiClient.cs @@ -0,0 +1,26 @@ +using OnlineShop.ServiceDefaults.Dtos; + +namespace OnlineShop.Web; + +public class ProductsApiClient(HttpClient httpClient) +{ + public async Task GetProductsAsync(int maxItems = 10, CancellationToken cancellationToken = default) + { + List? products = null; + + await foreach (var product in httpClient.GetFromJsonAsAsyncEnumerable("/products", cancellationToken)) + { + if (products?.Count >= maxItems) + { + break; + } + if (product is not null) + { + products ??= []; + products.Add(product); + } + } + + return products?.ToArray() ?? []; + } +} diff --git a/AppWithSqlServer/OnlineShop/OnlineShop.Web/Program.cs b/AppWithSqlServer/OnlineShop/OnlineShop.Web/Program.cs index feff627..d31b35e 100644 --- a/AppWithSqlServer/OnlineShop/OnlineShop.Web/Program.cs +++ b/AppWithSqlServer/OnlineShop/OnlineShop.Web/Program.cs @@ -17,7 +17,7 @@ builder.Services.AddOutputCache(); builder.Services.AddHttpClient( "OidcBackchannel", o => o.BaseAddress = new("http://idp")); -builder.Services.AddHttpClient(client => +builder.Services.AddHttpClient(client => { client.BaseAddress = new("https+http://apiservice"); }); diff --git a/AppWithSqlServer/OnlineShop/OnlineShop.Web/WeatherApiClient.cs b/AppWithSqlServer/OnlineShop/OnlineShop.Web/WeatherApiClient.cs deleted file mode 100644 index 2ca004b..0000000 --- a/AppWithSqlServer/OnlineShop/OnlineShop.Web/WeatherApiClient.cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace OnlineShop.Web; - -public class WeatherApiClient(HttpClient httpClient) -{ - public async Task GetWeatherAsync(int maxItems = 10, CancellationToken cancellationToken = default) - { - List? forecasts = null; - - await foreach (var forecast in httpClient.GetFromJsonAsAsyncEnumerable("/weatherforecast", cancellationToken)) - { - if (forecasts?.Count >= maxItems) - { - break; - } - if (forecast is not null) - { - forecasts ??= []; - forecasts.Add(forecast); - } - } - - return forecasts?.ToArray() ?? []; - } -} - -public record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) -{ - public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); -}