Files
OnlineShop/AppWithAzureQueueStorage/OnlineShop/OnlineShop.ApiService/Program.cs
T
2026-06-19 18:53:29 +01:00

554 lines
16 KiB
C#

using Azure.Data.Tables;
using Azure.Storage.Blobs;
using Azure.Storage.Queues;
using CsvHelper;
using CsvHelper.Configuration;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Data.SqlClient;
using MongoDB.Driver;
using OnlineShop.ApiService;
using OnlineShop.ApiService.Model;
using OnlineShop.ServiceDefaults.Dtos;
using System.Data;
using System.Globalization;
using System.Text;
using System.Text.Json;
var builder = WebApplication.CreateBuilder(args);
builder.AddServiceDefaults();
builder.Services.AddProblemDetails();
builder.Services.AddOpenApi();
builder.Services.AddSignalR();
builder.Services.AddHttpClient(
"OidcBackchannel", o => o.BaseAddress = new("http://idp"));
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme =
JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme =
JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer()
.ConfigureApiJwt();
builder.AddSqlServerClient("sqldb");
builder.AddMongoDBClient("mongodb");
builder.AddAzureTableServiceClient("tables");
builder.AddAzureBlobServiceClient("blobs");
builder.AddAzureQueueServiceClient("queues");
builder.Services.AddSingleton<LocationUpdater>();
builder.Services.AddHostedService(
sp => sp
.GetRequiredService<LocationUpdater>());
var app = builder.Build();
using (var scope = app.Services.CreateScope())
{
var connection = scope.ServiceProvider.GetRequiredService<SqlConnection>();
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;
-- Products table
IF NOT EXISTS (
SELECT *
FROM sysobjects
WHERE name='Products' AND xtype='U'
)
BEGIN
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
);
END
-- Orders table
IF NOT EXISTS (
SELECT *
FROM sysobjects
WHERE name='Orders' AND xtype='U'
)
BEGIN
CREATE TABLE Orders (
Id INT IDENTITY PRIMARY KEY,
TotalAmount DECIMAL(18,2) NOT NULL
);
END
-- OrderItems table
IF NOT EXISTS (
SELECT *
FROM sysobjects
WHERE name='OrderItems' AND xtype='U'
)
BEGIN
CREATE TABLE OrderItems (
OrderId INT NOT NULL,
ProductId INT NOT NULL,
Quantity INT NOT NULL,
CONSTRAINT PK_OrderItems PRIMARY KEY (OrderId, ProductId),
CONSTRAINT FK_OrderItems_Orders
FOREIGN KEY (OrderId) REFERENCES Orders(Id),
CONSTRAINT FK_OrderItems_Products
FOREIGN KEY (ProductId) REFERENCES Products(Id)
);
END
", 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();
}
var mongoClient = scope.ServiceProvider
.GetRequiredService<IMongoClient>();
var database = mongoClient
.GetDatabase("ShopDB");
var collection = database
.GetCollection<ProductReviewsDocument>("ProductReviews");
var docCount = collection.CountDocuments(FilterDefinition<ProductReviewsDocument>.Empty);
if (docCount == 0)
{
var productReviews = new List<ProductReviewsDocument>();
foreach (var productId in Enumerable.Range(1, 10))
{
var reviews = new List<ProductReview>();
var numberOfReviews = Random.Shared.Next(1, 5);
for (var i = 0; i < numberOfReviews; i++)
{
reviews.Add(new ProductReview
{
UserId = $"user-{Random.Shared.Next(1, 100)}",
Rating = Random.Shared.Next(3, 6), // 35
Comment = "Great product, works exactly as expected.",
CreatedAt = DateTime.UtcNow.AddDays(-Random.Shared.Next(1, 30)),
VerifiedPurchase = Random.Shared.Next(0, 2) == 1,
Status = ReviewStatus.Approved
});
}
var averageRating = reviews.Average(r => r.Rating);
productReviews.Add(new ProductReviewsDocument
{
ProductId = productId, // matches SQL Products.Id
AverageRating = Math.Round(averageRating, 2),
TotalReviews = reviews.Count,
Reviews = reviews
});
}
collection.InsertMany(productReviews);
}
var tableServiceClient =
scope.ServiceProvider.GetRequiredService<TableServiceClient>();
var tableClient = tableServiceClient
.GetTableClient("ProductMetadata");
await tableClient.CreateIfNotExistsAsync();
var existing = tableClient
.Query<ProductMetadataEntity>(x => x.PartitionKey == "Product")
.Take(1)
.Any();
if (!existing)
{
var entities = new List<ProductMetadataEntity>();
foreach (var productId in Enumerable.Range(1, 10))
{
entities.Add(new ProductMetadataEntity
{
PartitionKey = "Product",
RowKey = productId.ToString(),
ReviewsEnabled = true,
Featured = productId % 2 == 0,
MaxReviewsPerUser = 1
});
}
foreach (var entity in entities)
{
await tableClient.AddEntityAsync(entity);
}
}
var blobServiceClient =
scope.ServiceProvider
.GetRequiredService<BlobServiceClient>();
var containerClient = blobServiceClient
.GetBlobContainerClient("products");
// Create the container if it doesn't exist
await containerClient.CreateIfNotExistsAsync();
var blobClient = containerClient
.GetBlobClient("products-specs.csv");
// Check if the blob already exists
if (await blobClient.ExistsAsync())
{
return;
}
var productSpecs = new List<ProductSpecCsvRow>();
foreach (var productId in Enumerable.Range(1, 10))
{
productSpecs.Add(new ProductSpecCsvRow
{
ProductId = productId,
ReviewsEnabled = true,
Featured = productId % 2 == 0,
MaxReviewsPerUser = 1,
Category = productId % 2 == 0 ? "Laptop" : "Peripheral",
WarrantyMonths = productId % 2 == 0 ? 24 : 12
});
}
using (var memoryStream = new MemoryStream())
using (var writer = new StreamWriter(memoryStream, Encoding.UTF8, leaveOpen: true))
using (var csv = new CsvWriter(writer, new CsvConfiguration(CultureInfo.InvariantCulture)))
{
csv.WriteRecords(productSpecs);
writer.Flush();
memoryStream.Position = 0;
await blobClient.UploadAsync(memoryStream, overwrite: true);
}
}
app.UseExceptionHandler();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
app.MapGet("/", () => "API service is running.");
app.MapGet("/products",
([FromServices] SqlConnection connection) =>
{
connection.Open();
var command = new SqlCommand(@"
USE Shop;
SELECT
Id,
Title,
Summary,
Price
FROM Products", connection);
var products = new List<ProductDto>();
using (var reader = command.ExecuteReader())
{
while (reader.Read())
{
products.Add(new ProductDto(
Id: reader.GetInt32(0),
Title: reader.GetString(1),
Summary: reader.GetString(2),
Price: reader.GetDecimal(3)
));
}
return products.ToArray();
}
});
app.MapGet("/product-reviews",
([FromServices] IMongoClient mongoClient) =>
{
var database = mongoClient
.GetDatabase("ShopDB");
var collection = database
.GetCollection<ProductReviewsDocument>(
"ProductReviews");
var productReviews = collection
.Find(FilterDefinition<ProductReviewsDocument>.Empty)
.ToList();
return productReviews.ToArray();
});
app.MapGet("/product-metadata",
async (TableServiceClient tableServiceClient) =>
{
var tableClient = tableServiceClient
.GetTableClient("ProductMetadata");
var metadata = new List<ProductMetadataEntity>();
var entities = tableClient
.QueryAsync<ProductMetadataEntity>(
x => x.PartitionKey == "Product");
await foreach (var entity in entities)
{
metadata.Add(entity);
}
return metadata.ToArray();
});
app.MapGet("/product-specs", async (
BlobServiceClient blobServiceClient) =>
{
var containerClient = blobServiceClient
.GetBlobContainerClient("products");
var blobClient = containerClient
.GetBlobClient("product-specs.csv");
if (!await blobClient.ExistsAsync())
{
return Array.Empty<ProductSpecCsvRow>();
}
List<ProductSpecCsvRow> productSpecs;
// Download the CSV from the blob
var downloadResponse = await blobClient.DownloadAsync();
using (var stream = downloadResponse.Value.Content)
using (var reader = new StreamReader(stream))
using (var csv = new CsvReader(reader, CultureInfo.InvariantCulture))
{
productSpecs = csv
.GetRecords<ProductSpecCsvRow>()
.ToList();
}
return productSpecs.ToArray();
});
app.MapPost("/api/orders", async (
Dictionary<int, int> basket,
[FromServices] SqlConnection dbConnection,
[FromServices] QueueServiceClient queueServiceClient) =>
{
if (basket is null || basket.Count == 0)
return Results.BadRequest("Basket is empty.");
var items = basket
.Where(kvp => kvp.Value > 0)
.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
if (items.Count == 0)
return Results.BadRequest("Basket contains no items with quantity > 0.");
if (dbConnection.State != ConnectionState.Open)
await dbConnection.OpenAsync();
int orderId;
decimal totalAmount;
await using var tx =
(SqlTransaction)await dbConnection.BeginTransactionAsync(IsolationLevel.ReadCommitted);
try
{
var productIds = items.Keys.ToList();
var inParams = string.Join(", ", productIds.Select((_, i) => $"@p{i}"));
var priceLookup = new Dictionary<int, decimal>();
await using (var cmd = new SqlCommand($@"
SELECT Id, Price
FROM Products
WHERE Id IN ({inParams});",
dbConnection, tx))
{
for (int i = 0; i < productIds.Count; i++)
cmd.Parameters.AddWithValue($"@p{i}", productIds[i]);
await using var reader = await cmd.ExecuteReaderAsync();
while (await reader.ReadAsync())
priceLookup[reader.GetInt32(0)] = reader.GetDecimal(1);
}
totalAmount = items.Sum(i => priceLookup[i.Key] * i.Value);
await using (var cmd = new SqlCommand(@"
INSERT INTO Orders (TotalAmount)
VALUES (@totalAmount);
SELECT CAST(SCOPE_IDENTITY() AS INT);",
dbConnection, tx))
{
var p = cmd.Parameters.Add("@totalAmount", SqlDbType.Decimal);
p.Precision = 18;
p.Scale = 2;
p.Value = totalAmount;
orderId = (int)(await cmd.ExecuteScalarAsync() ?? 0);
}
if (orderId <= 0)
return Results.Problem("Failed to create order.");
await using (var cmd = new SqlCommand(@"
INSERT INTO OrderItems (OrderId, ProductId, Quantity)
VALUES (@orderId, @productId, @quantity);",
dbConnection, tx))
{
cmd.Parameters.Add("@orderId", SqlDbType.Int).Value = orderId;
var pProductId = cmd.Parameters.Add("@productId", SqlDbType.Int);
var pQuantity = cmd.Parameters.Add("@quantity", SqlDbType.Int);
foreach (var (productId, qty) in items)
{
pProductId.Value = productId;
pQuantity.Value = qty;
await cmd.ExecuteNonQueryAsync();
}
}
await tx.CommitAsync();
}
catch (Exception ex)
{
await tx.RollbackAsync();
return Results.Problem($"Order creation failed: {ex.Message}");
}
var queueClient = queueServiceClient
.GetQueueClient("orders-created");
queueClient.CreateIfNotExists();
var message = JsonSerializer.Serialize(new { orderId });
queueClient.SendMessage(message);
return Results.Created($"/api/orders/{orderId}", new
{
OrderId = orderId,
TotalAmount = totalAmount
});
});
app.MapDefaultEndpoints();
app.MapHub<LocationHub>("/locationHub");
app.Run();