Skip to content

Commit 216497f

Browse files
committed
Pre-upgrade commit before .NET 10.0 migration
1 parent 9543416 commit 216497f

19 files changed

Lines changed: 841 additions & 154 deletions
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net9.0</TargetFramework>
5+
<OutputType>Exe</OutputType>
6+
</PropertyGroup>
7+
8+
<ItemGroup>
9+
<PackageReference Include="BenchmarkDotNet" Version="0.15.2" />
10+
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.6" />
11+
<PackageReference Include="Microsoft.VisualStudio.DiagnosticsHub.BenchmarkDotNetDiagnosers" Version="18.3.36726.2" />
12+
</ItemGroup>
13+
14+
<ItemGroup>
15+
<ProjectReference Include="..\Products\Products.csproj" />
16+
</ItemGroup>
17+
</Project>
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
using BenchmarkDotNet.Attributes;
2+
using DataEntities;
3+
using Microsoft.EntityFrameworkCore;
4+
using Products.Data;
5+
using System;
6+
using System.Collections.Generic;
7+
using System.Linq;
8+
using System.Threading.Tasks;
9+
10+
public class ProductBenchmark
11+
{
12+
private ProductDataContext _context;
13+
14+
[GlobalSetup]
15+
public void Setup()
16+
{
17+
var options = new DbContextOptionsBuilder<ProductDataContext>()
18+
.UseInMemoryDatabase("BenchmarkDb")
19+
.Options;
20+
_context = new ProductDataContext(options);
21+
22+
// Seed some test data
23+
var products = new List<Product>();
24+
for (int i = 0; i < 1000; i++)
25+
{
26+
products.Add(new Product { Name = $"Test Product {i}", Description = $"Description {i}", Price = 10.99m + i, CreatedDate = DateTime.UtcNow, ModifiedDate = DateTime.UtcNow });
27+
}
28+
_context.Product.AddRange(products);
29+
_context.SaveChanges();
30+
}
31+
32+
[Benchmark]
33+
public async Task<List<Product>> GetAllProductsAsync()
34+
{
35+
return await _context.Product.AsNoTracking().ToListAsync();
36+
}
37+
38+
[Benchmark]
39+
public List<Product> GetAllProductsSync()
40+
{
41+
return _context.Product.AsNoTracking().ToList();
42+
}
43+
44+
[Benchmark]
45+
public List<Product> GetProductsPaginatedSync()
46+
{
47+
return _context.Product.AsNoTracking().OrderBy(p => p.Id).Skip(0).Take(10).ToList();
48+
}
49+
}

src/BenchmarkSuite1/Program.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using BenchmarkDotNet.Running;
2+
3+
namespace BenchmarkSuite1
4+
{
5+
internal class Program
6+
{
7+
static void Main(string[] args)
8+
{
9+
var _ = BenchmarkRunner.Run(typeof(Program).Assembly);
10+
}
11+
}
12+
}

src/Products/Data/ProductDataContext.cs

Lines changed: 60 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
3131
entity.Property(e => e.ModifiedDate).HasDefaultValueSql("GETUTCDATE()");
3232

3333
entity.HasIndex(e => e.Name).HasDatabaseName("IX_Products_Name");
34+
entity.HasIndex(e => e.Price).HasDatabaseName("IX_Products_Price");
3435
});
3536
}
3637
}
@@ -72,23 +73,71 @@ public static async Task InitializeAsync(ProductDataContext context, ILogger log
7273

7374
var products = new List<Product>
7475
{
75-
// Note: ImageUrl is set to null - images will be loaded into ImageData via LoadImages.ps1 script
76+
// Note: images will be loaded into ImageData via LoadImages.ps1 script
7677
// This enables Scenario 2: Database image serving via /api/Product/{id}/image
77-
new Product { Name = "Solar Powered Flashlight", Description = "A fantastic product for outdoor enthusiasts", Price = 19.99m, ImageUrl = null, CreatedDate = DateTime.UtcNow, ModifiedDate = DateTime.UtcNow },
78-
new Product { Name = "Hiking Poles", Description = "Ideal for camping and hiking trips", Price = 24.99m, ImageUrl = null, CreatedDate = DateTime.UtcNow, ModifiedDate = DateTime.UtcNow },
79-
new Product { Name = "Outdoor Rain Jacket", Description = "This product will keep you warm and dry in all weathers", Price = 49.99m, ImageUrl = null, CreatedDate = DateTime.UtcNow, ModifiedDate = DateTime.UtcNow },
80-
new Product { Name = "Survival Kit", Description = "A must-have for any outdoor adventurer", Price = 99.99m, ImageUrl = null, CreatedDate = DateTime.UtcNow, ModifiedDate = DateTime.UtcNow },
81-
new Product { Name = "Outdoor Backpack", Description = "This backpack is perfect for carrying all your outdoor essentials", Price = 39.99m, ImageUrl = null, CreatedDate = DateTime.UtcNow, ModifiedDate = DateTime.UtcNow },
82-
new Product { Name = "Camping Cookware", Description = "This cookware set is ideal for cooking outdoors", Price = 29.99m, ImageUrl = null, CreatedDate = DateTime.UtcNow, ModifiedDate = DateTime.UtcNow },
83-
new Product { Name = "Camping Stove", Description = "This stove is perfect for cooking outdoors", Price = 49.99m, ImageUrl = null, CreatedDate = DateTime.UtcNow, ModifiedDate = DateTime.UtcNow },
84-
new Product { Name = "Camping Lantern", Description = "This lantern is perfect for lighting up your campsite", Price = 19.99m, ImageUrl = null, CreatedDate = DateTime.UtcNow, ModifiedDate = DateTime.UtcNow },
85-
new Product { Name = "Camping Tent", Description = "This tent is perfect for camping trips", Price = 99.99m, ImageUrl = null, CreatedDate = DateTime.UtcNow, ModifiedDate = DateTime.UtcNow }
78+
new Product { Name = "Solar Powered Flashlight", Description = "A fantastic product for outdoor enthusiasts", Price = 19.99m, ImageUrl = "images/product1.png", CreatedDate = DateTime.UtcNow, ModifiedDate = DateTime.UtcNow },
79+
new Product { Name = "Hiking Poles", Description = "Ideal for camping and hiking trips", Price = 24.99m, ImageUrl = "images/product2.png", CreatedDate = DateTime.UtcNow, ModifiedDate = DateTime.UtcNow },
80+
new Product { Name = "Outdoor Rain Jacket", Description = "This product will keep you warm and dry in all weathers", Price = 49.99m, ImageUrl = "images/product3.png", CreatedDate = DateTime.UtcNow, ModifiedDate = DateTime.UtcNow },
81+
new Product { Name = "Survival Kit", Description = "A must-have for any outdoor adventurer", Price = 99.99m, ImageUrl = "images/product4.png", CreatedDate = DateTime.UtcNow, ModifiedDate = DateTime.UtcNow },
82+
new Product { Name = "Outdoor Backpack", Description = "This backpack is perfect for carrying all your outdoor essentials", Price = 39.99m, ImageUrl = "images/product5.png", CreatedDate = DateTime.UtcNow, ModifiedDate = DateTime.UtcNow },
83+
new Product { Name = "Camping Cookware", Description = "This cookware set is ideal for cooking outdoors", Price = 29.99m, ImageUrl = "images/product6.png", CreatedDate = DateTime.UtcNow, ModifiedDate = DateTime.UtcNow },
84+
new Product { Name = "Camping Stove", Description = "This stove is perfect for cooking outdoors", Price = 49.99m, ImageUrl = "images/product7.png", CreatedDate = DateTime.UtcNow, ModifiedDate = DateTime.UtcNow },
85+
new Product { Name = "Camping Lantern", Description = "This lantern is perfect for lighting up your campsite", Price = 19.99m, ImageUrl = "images/product8.png", CreatedDate = DateTime.UtcNow, ModifiedDate = DateTime.UtcNow },
86+
new Product { Name = "Camping Tent", Description = "This tent is perfect for camping trips", Price = 99.99m, ImageUrl = "images/product9.png", CreatedDate = DateTime.UtcNow, ModifiedDate = DateTime.UtcNow }
8687
};
8788

8889
context.Product.AddRange(products);
8990
await context.SaveChangesAsync();
9091

92+
// Load images into ImageData
93+
await LoadImagesAsync(context, logger);
94+
9195
logger.LogInformation("Seeded {Count} products successfully.", products.Count);
92-
logger.LogInformation("Run LoadImages.ps1 script to load images into ImageData column.");
96+
logger.LogInformation("Images loaded into ImageData column.");
97+
}
98+
99+
private static async Task LoadImagesAsync(ProductDataContext context, ILogger logger)
100+
{
101+
try
102+
{
103+
string imagesPath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "images");
104+
105+
if (!Directory.Exists(imagesPath))
106+
{
107+
logger.LogWarning("Images directory not found: {Path}", imagesPath);
108+
return;
109+
}
110+
111+
var products = await context.Product.OrderBy(p => p.Id).ToListAsync();
112+
var imageLoadTasks = new List<Task>();
113+
114+
for (int i = 0; i < products.Count && i < 9; i++)
115+
{
116+
string imageFile = Path.Combine(imagesPath, $"product{i+1}.png");
117+
if (File.Exists(imageFile))
118+
{
119+
try
120+
{
121+
byte[] imageData = await File.ReadAllBytesAsync(imageFile);
122+
products[i].ImageData = imageData;
123+
logger.LogInformation("Loaded image for product {Id}: {File}", products[i].Id, imageFile);
124+
}
125+
catch (Exception ex)
126+
{
127+
logger.LogWarning(ex, "Failed to load image: {File}", imageFile);
128+
}
129+
}
130+
else
131+
{
132+
logger.LogWarning("Image file not found: {File}", imageFile);
133+
}
134+
}
135+
136+
await context.SaveChangesAsync();
137+
}
138+
catch (Exception ex)
139+
{
140+
logger.LogError(ex, "Error during image loading - continuing without images");
141+
}
93142
}
94143
}

src/Products/Data/TestDb_Products.sql

Lines changed: 58 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -7,29 +7,36 @@ IF OBJECT_ID('dbo.Products', 'U') IS NOT NULL
77
DROP TABLE dbo.Products;
88
GO
99

10-
Create Table Products
11-
(
12-
Id int primary key,
13-
name varchar(50),
14-
Description varchar(255),
15-
Price decimal(10,2),
16-
ImageUrl varchar(255),
17-
image varbinary(max)
10+
CREATE TABLE [dbo].[Products] (
11+
[Id] INT IDENTITY (1, 1) NOT NULL,
12+
[Name] NVARCHAR (200) NOT NULL,
13+
[Description] NVARCHAR (1000) NULL,
14+
[Price] DECIMAL (18, 2) NOT NULL,
15+
[ImageUrl] NVARCHAR (500) NULL,
16+
[ImageData] VARBINARY (MAX) NULL,
17+
[CreatedDate] DATETIME2 (7) DEFAULT (getutcdate()) NULL,
18+
[ModifiedDate] DATETIME2 (7) DEFAULT (getutcdate()) NULL,
19+
PRIMARY KEY CLUSTERED ([Id] ASC)
1820
);
21+
22+
1923
GO
24+
CREATE NONCLUSTERED INDEX [IX_Products_Name]
25+
ON [dbo].[Products]([Name] ASC);
26+
2027

2128
BEGIN TRANSACTION;
2229

23-
INSERT INTO Products (Id, name, Description, Price, ImageUrl, image) VALUES
24-
(1, 'Solar Powered Flashlight', 'A fantastic product for outdoor enthusiasts', 19.99, 'product1.png', NULL),
25-
(2, 'Hiking Poles', 'Ideal for camping and hiking trips', 24.99, 'product2.png', NULL),
26-
(3, 'Outdoor Rain Jacket', 'This product will keep you warm and dry in all weathers', 49.99, 'product3.png', NULL),
27-
(4, 'Survival Kit', 'A must-have for any outdoor adventurer', 99.99, 'product4.png', NULL),
28-
(5, 'Outdoor Backpack', 'This backpack is perfect for carrying all your outdoor essentials', 39.99, 'product5.png', NULL),
29-
(6, 'Camping Cookware', 'This cookware set is ideal for cooking outdoors', 29.99, 'product6.png', NULL),
30-
(7, 'Camping Stove', 'This stove is perfect for cooking outdoors', 49.99, 'product7.png', NULL),
31-
(8, 'Camping Lantern', 'This lantern is perfect for lighting up your campsite', 19.99, 'product8.png', NULL),
32-
(9, 'Camping Tent', 'This tent is perfect for camping trips', 99.99, 'product9.png', NULL);
30+
INSERT INTO Products (name, Description, Price, ImageUrl, ImageData) VALUES
31+
('Solar Powered Flashlight', 'A fantastic product for outdoor enthusiasts', 19.99, 'images/product1.png', NULL),
32+
('Hiking Poles', 'Ideal for camping and hiking trips', 24.99, 'images/product2.png', NULL),
33+
('Outdoor Rain Jacket', 'This product will keep you warm and dry in all weathers', 49.99, 'images/product3.png', NULL),
34+
('Survival Kit', 'A must-have for any outdoor adventurer', 99.99, 'images/product4.png', NULL),
35+
('Outdoor Backpack', 'This backpack is perfect for carrying all your outdoor essentials', 39.99, 'images/product5.png', NULL),
36+
('Camping Cookware', 'This cookware set is ideal for cooking outdoors', 29.99, 'images/product6.png', NULL),
37+
('Camping Stove', 'This stove is perfect for cooking outdoors', 49.99, 'images/product7.png', NULL),
38+
('Camping Lantern', 'This lantern is perfect for lighting up your campsite', 19.99, 'images/product8.png', NULL),
39+
('Camping Tent', 'This tent is perfect for camping trips', 99.99, 'images/product9.png', NULL);
3340

3441
COMMIT;
3542
GO
@@ -49,50 +56,53 @@ END CATCH
4956
GO
5057

5158
-- Base folder where images are stored on the SQL Server machine.
52-
DECLARE @BasePath nvarchar(4000) = N'D:\repros\VS2022-lab300\src\Products\wwwroot\images\';
59+
DECLARE @BasePath nvarchar(4000) = N'D:\repros\VS2022-lab300\src\Products\wwwroot\images\product';
60+
5361

5462
-- Use dynamic SQL per-row to pass a literal path into OPENROWSET(BULK ...)
5563
BEGIN TRANSACTION;
5664
BEGIN TRY
57-
DECLARE @Id int;
65+
DECLARE @ProductId INT;
5866
DECLARE @ImageUrl nvarchar(255);
59-
DECLARE @FullPath nvarchar(4000);
67+
DECLARE @ImagePath nvarchar(4000);
6068
DECLARE @sql nvarchar(max);
69+
DECLARE @ImageIndex INT = 1;
70+
DECLARE @MaxImages INT = 9;
6171

62-
DECLARE image_cursor CURSOR LOCAL FAST_FORWARD FOR
63-
SELECT Id, ImageUrl FROM Products WHERE ImageUrl IS NOT NULL;
72+
-- Cursor to iterate through products ordered by ID
73+
DECLARE product_cursor CURSOR FOR
74+
SELECT Id FROM dbo.Products ORDER BY Id;
6475

65-
OPEN image_cursor;
66-
FETCH NEXT FROM image_cursor INTO @Id, @ImageUrl;
76+
OPEN product_cursor;
77+
FETCH NEXT FROM product_cursor INTO @ProductId;
6778

68-
WHILE @@FETCH_STATUS = 0
79+
WHILE @@FETCH_STATUS = 0 AND @ImageIndex <= @MaxImages
6980
BEGIN
70-
-- Build full path and escape single quotes
71-
SET @FullPath = @BasePath + ISNULL(@ImageUrl, '');
72-
SET @FullPath = REPLACE(@FullPath, '''', '''''');
73-
74-
-- Build dynamic SQL using a string literal for OPENROWSET path
75-
SET @sql = N'
76-
UPDATE Products
77-
SET image = (
78-
SELECT BulkColumn
79-
FROM OPENROWSET(BULK N''' + @FullPath + ''', SINGLE_BLOB) AS img
80-
)
81-
WHERE Id = ' + CAST(@Id AS nvarchar(10)) + ';';
82-
83-
BEGIN TRY
84-
EXEC sp_executesql @sql;
81+
-- Construct the image file path based on sequential index
82+
SET @ImagePath = @BasePath + CAST(@ImageIndex AS NVARCHAR(10)) + N'.png';
83+
84+
-- Build dynamic SQL to load the image
85+
SET @SQL = N'UPDATE dbo.Products
86+
SET ImageData = (SELECT * FROM OPENROWSET(BULK ''' + @ImagePath + N''', SINGLE_BLOB) AS ImageData),
87+
ImageUrl = ''images/product' + CAST(@ImageIndex AS NVARCHAR(10)) + N'.png''
88+
WHERE Id = ' + CAST(@ProductId AS NVARCHAR(10)) + N';';
89+
90+
-- Execute the dynamic SQL
91+
BEGIN TRY
92+
EXEC sp_executesql @SQL;
93+
PRINT 'Successfully loaded image ' + CAST(@ImageIndex AS NVARCHAR(10)) + ' for Product ID: ' + CAST(@ProductId AS NVARCHAR(10)) + ' - ' + @ImagePath;
8594
END TRY
8695
BEGIN CATCH
87-
PRINT 'Failed to load ' + ISNULL(@ImageUrl, '<null>') + ': ' + ERROR_MESSAGE();
88-
-- continue to next file
89-
END CATCH
90-
91-
FETCH NEXT FROM image_cursor INTO @Id, @ImageUrl;
96+
PRINT 'Error loading image ' + CAST(@ImageIndex AS NVARCHAR(10)) + ' for Product ID: ' + CAST(@ProductId AS NVARCHAR(10)) + ' - ' + ERROR_MESSAGE();
97+
END CATCH
98+
99+
-- Move to next product and image
100+
SET @ImageIndex = @ImageIndex + 1;
101+
FETCH NEXT FROM product_cursor INTO @ProductId;
92102
END
93103

94-
CLOSE image_cursor;
95-
DEALLOCATE image_cursor;
104+
CLOSE product_cursor;
105+
DEALLOCATE product_cursor;
96106

97107
COMMIT TRANSACTION;
98108
END TRY

src/Products/Endpoints/ProductEndpoints.cs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,16 @@ public static void MapProductEndpoints(this IEndpointRouteBuilder routes)
2020
});
2121

2222
// GET all products
23-
group.MapGet("/", async (ProductDataContext db) =>
23+
group.MapGet("/", async (ProductDataContext db, int? page = null, int? size = null) =>
2424
{
25-
return await db.Product
26-
.OrderBy(p => p.Id)
27-
.ToListAsync();
25+
var query = db.Product.OrderBy(p => p.Id);
26+
27+
if (page.HasValue && size.HasValue && page.Value > 0 && size.Value > 0)
28+
{
29+
query = (IOrderedQueryable<Product>)query.Skip((page.Value - 1) * size.Value).Take(size.Value);
30+
}
31+
32+
return await query.ToListAsync();
2833
})
2934
.WithName("GetAllProducts")
3035
.Produces<List<Product>>(StatusCodes.Status200OK);

0 commit comments

Comments
 (0)