自动迁移和数据库初始化

This commit is contained in:
hello 2025-03-25 21:17:54 +08:00
parent ccf1e7672d
commit 8da853a351
27 changed files with 345 additions and 221 deletions

View File

@ -8,8 +8,7 @@ var builder = DistributedApplication.CreateBuilder(args);
var postgreUser = builder.AddParameter("postgreUser", secret: true);
var postgrePassword = builder.AddParameter("postgrePassword", secret: true);
var postgres = builder.AddPostgres("postgres", postgreUser, postgrePassword, port: 5432).WithPgAdmin()
.WithLifetime(ContainerLifetime.Persistent);
var postgres = builder.AddPostgres("postgres", postgreUser, postgrePassword, port: 5432).WithPgAdmin();
var identitydb = postgres.AddDatabase("identitydb");
var productdb = postgres.AddDatabase("productdb");
var orderingdb = postgres.AddDatabase("orderingdb");

View File

@ -34,7 +34,6 @@ builder.Services.AddGrpc().AddJsonTranscoding();
builder.Services.AddGrpcSwagger();
builder.Services.AddOpenApi();
builder.Services.AddDataSeedingProviders();
builder.Services.AddCustomLocalization();
builder.Services.AddModelMapper().AddModelValidator();
builder.Services.AddLocalization().AddPermissionDefinitions();
@ -59,7 +58,6 @@ app.MapDaprEventBus();
app.UseAuthentication().UseAuthorization();
app.MapGrpcService<GreeterService>();
app.MapGrpcService<CustomerBasketService>();
app.UseDataSeedingProviders();
app.UseCustomLocalization();
app.UseOpenApi();
app.MapGroup("api/Permissions").MapPermissionDefinitions("Permissions");

View File

@ -1,76 +0,0 @@
// Copyright (c) HelloShop Corporation. All rights reserved.
// See the license file in the project root for more information.
using HelloShop.IdentityService.Entities;
using HelloShop.IdentityService.Infrastructure;
using HelloShop.ServiceDefaults.Infrastructure;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
namespace HelloShop.IdentityService.DataSeeding
{
public class UserDataSeedingProvider(UserManager<User> userManager, RoleManager<Role> roleManager) : IDataSeedingProvider
{
public async Task SeedingAsync(IServiceProvider serviceProvider, CancellationToken cancellationToken = default)
{
await serviceProvider.GetRequiredService<IdentityServiceDbContext>().Database.EnsureCreatedAsync(cancellationToken);
var adminRole = await roleManager.Roles.SingleOrDefaultAsync(x => x.Name == "AdminRole", cancellationToken);
if (adminRole == null)
{
adminRole = new Role { Name = "AdminRole", };
await roleManager.CreateAsync(adminRole);
}
var guestRole = await roleManager.Roles.SingleOrDefaultAsync(x => x.Name == "GuestRole", cancellationToken: cancellationToken);
if (guestRole == null)
{
guestRole = new Role { Name = "GuestRole", };
await roleManager.CreateAsync(guestRole);
}
var adminUser = await userManager.FindByNameAsync("admin");
if (adminUser == null)
{
adminUser = new User
{
UserName = "admin",
Email = "admin@test.com"
};
await userManager.CreateAsync(adminUser, adminUser.UserName);
}
await userManager.AddToRolesAsync(adminUser, ["AdminRole", "GuestRole"]);
var guestUser = await userManager.FindByNameAsync("guest");
if (guestUser == null)
{
guestUser = new User
{
UserName = "guest",
Email = "guest@test.com"
};
await userManager.CreateAsync(guestUser, guestUser.UserName);
}
await userManager.AddToRoleAsync(guestUser, "GuestRole");
if (userManager.Users.Count() < 30)
{
for (int i = 0; i < 30; i++)
{
var user = new User
{
UserName = $"user{i}",
Email = $"test{i}@test.com",
};
await userManager.CreateAsync(user, user.UserName);
}
}
}
}
}

View File

@ -12,7 +12,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace HelloShop.IdentityService.Infrastructure.Migrations
{
[DbContext(typeof(IdentityServiceDbContext))]
[Migration("20241123020852_InitialCreate")]
[Migration("20250325130953_InitialCreate")]
partial class InitialCreate
{
/// <inheritdoc />
@ -20,7 +20,7 @@ namespace HelloShop.IdentityService.Infrastructure.Migrations
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.10")
.HasAnnotation("ProductVersion", "9.0.3")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);

View File

@ -17,7 +17,7 @@ namespace HelloShop.IdentityService.Infrastructure.Migrations
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.10")
.HasAnnotation("ProductVersion", "9.0.3")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);

View File

@ -6,6 +6,8 @@ using HelloShop.IdentityService.Authorization;
using HelloShop.IdentityService.Constants;
using HelloShop.IdentityService.Entities;
using HelloShop.IdentityService.Infrastructure;
using HelloShop.IdentityService.Services;
using HelloShop.IdentityService.Workers;
using HelloShop.ServiceDefaults.Authorization;
using HelloShop.ServiceDefaults.Extensions;
using Microsoft.AspNetCore.Authentication.JwtBearer;
@ -28,6 +30,8 @@ builder.AddNpgsqlDbContext<IdentityServiceDbContext>(connectionName: DbConstants
options.UseSnakeCaseNamingConvention();
});
builder.Services.AddSingleton<MigrationService<IdentityServiceDbContext>>().AddHostedService<DataSeeder>();
builder.Services.AddIdentity<User, Role>(options =>
{
options.Password.RequireDigit = false;
@ -57,7 +61,6 @@ builder.Services.AddAuthentication(options =>
options.SecurityAlgorithm = SecurityAlgorithms.HmacSha256;
});
builder.Services.AddDataSeedingProviders();
builder.Services.AddOpenApi();
builder.Services.AddPermissionDefinitions();
builder.Services.AddAuthorization().AddDistributedMemoryCache().AddHttpClient().AddHttpContextAccessor().AddTransient<IPermissionChecker, LocalPermissionChecker>().AddCustomAuthorization();
@ -75,9 +78,10 @@ app.UseCors(options => options.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader(
app.MapControllers();
app.UseDataSeedingProviders();
app.UseOpenApi();
app.MapGroup("api/Permissions").MapPermissionDefinitions("Permissions");
app.UseCustomLocalization();
app.Run();
await app.Services.GetRequiredService<MigrationService<IdentityServiceDbContext>>().ExecuteAsync();
await app.RunAsync();

View File

@ -0,0 +1,49 @@
// Copyright (c) HelloShop Corporation. All rights reserved.
// See the license file in the project root for more information.
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage;
using System.Diagnostics;
namespace HelloShop.IdentityService.Services
{
public class MigrationService<TDbContext>(IServiceScopeFactory scopeFactory) where TDbContext : DbContext
{
public const string ActivitySourceName = "Migrations";
private static readonly ActivitySource s_activitySource = new(ActivitySourceName);
public async ValueTask ExecuteAsync(CancellationToken cancellationToken = default)
{
using var activity = s_activitySource.StartActivity("Migrating database", ActivityKind.Client);
try
{
using var scope = scopeFactory.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<TDbContext>();
await RunMigrationAsync(dbContext, cancellationToken);
}
catch (Exception ex)
{
activity?.AddException(ex);
}
}
private static async ValueTask RunMigrationAsync(TDbContext dbContext, CancellationToken cancellationToken)
{
var strategy = dbContext.Database.CreateExecutionStrategy();
var dbCreator = dbContext.GetService<IRelationalDatabaseCreator>();
var historyRepository = dbContext.GetService<IHistoryRepository>();
await strategy.ExecuteAsync(async () =>
{
if (!await dbCreator.ExistsAsync(cancellationToken))
{
await dbCreator.CreateAsync(cancellationToken);
}
await historyRepository.CreateIfNotExistsAsync();
await dbContext.Database.MigrateAsync(cancellationToken);
});
}
}
}

View File

@ -0,0 +1,89 @@
// Copyright (c) HelloShop Corporation. All rights reserved.
// See the license file in the project root for more information.
using HelloShop.IdentityService.Entities;
using HelloShop.IdentityService.Infrastructure;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
namespace HelloShop.IdentityService.Workers
{
public class DataSeeder(IServiceScopeFactory scopeFactory) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
using var scope = scopeFactory.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<IdentityServiceDbContext>();
var roleManager = scope.ServiceProvider.GetRequiredService<RoleManager<Role>>();
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<User>>();
TimeProvider timeProvider = scope.ServiceProvider.GetRequiredService<TimeProvider>();
await dbContext.Database.CreateExecutionStrategy().ExecuteAsync(async () =>
{
await using var transaction = await dbContext.Database.BeginTransactionAsync(stoppingToken);
var adminRole = await roleManager.Roles.SingleOrDefaultAsync(x => x.Name == "AdminRole", stoppingToken);
if (adminRole == null)
{
adminRole = new Role { Name = "AdminRole", CreationTime = timeProvider.GetUtcNow() };
await roleManager.CreateAsync(adminRole);
}
var guestRole = await roleManager.Roles.SingleOrDefaultAsync(x => x.Name == "GuestRole", stoppingToken);
if (guestRole == null)
{
guestRole = new Role { Name = "GuestRole", CreationTime = timeProvider.GetUtcNow() };
await roleManager.CreateAsync(guestRole);
}
var adminUser = await userManager.FindByNameAsync("admin");
if (adminUser == null)
{
adminUser = new User
{
UserName = "admin",
Email = "admin@test.com",
CreationTime = timeProvider.GetUtcNow()
};
await userManager.CreateAsync(adminUser, adminUser.UserName);
}
await userManager.AddToRolesAsync(adminUser, ["AdminRole", "GuestRole"]);
var guestUser = await userManager.FindByNameAsync("guest");
if (guestUser == null)
{
guestUser = new User
{
UserName = "guest",
Email = "guest@test.com",
CreationTime = timeProvider.GetUtcNow()
};
await userManager.CreateAsync(guestUser, guestUser.UserName);
}
await userManager.AddToRoleAsync(guestUser, "GuestRole");
if (!await dbContext.Set<PermissionGranted>().AnyAsync(cancellationToken: stoppingToken))
{
dbContext.AddRange(
new PermissionGranted { PermissionName = "Catalog.Products", RoleId = 1 },
new PermissionGranted { PermissionName = "Catalog.Products.Create", RoleId = 1 },
new PermissionGranted { PermissionName = "Catalog.Products.Update", RoleId = 1 },
new PermissionGranted { PermissionName = "Catalog.Products.Delete", RoleId = 1 },
new PermissionGranted { PermissionName = "Catalog.Products.Details", RoleId = 1 }
);
await dbContext.SaveChangesAsync(stoppingToken);
}
await transaction.CommitAsync(stoppingToken);
});
}
}
}

View File

@ -1,16 +0,0 @@
// Copyright (c) HelloShop Corporation. All rights reserved.
// See the license file in the project root for more information.
using HelloShop.OrderingService.Infrastructure;
using HelloShop.ServiceDefaults.Infrastructure;
namespace HelloShop.OrderingService.DataSeeding
{
public class OrderingDataSeedingProvider(OrderingServiceDbContext dbContext) : IDataSeedingProvider
{
public async Task SeedingAsync(IServiceProvider serviceProvider, CancellationToken cancellationToken = default)
{
await dbContext.Database.EnsureCreatedAsync(cancellationToken);
}
}
}

View File

@ -33,14 +33,14 @@ namespace HelloShop.OrderingService.Extensions
builder.Services.AddHttpContextAccessor();
builder.Services.AddDataSeedingProviders();
builder.AddNpgsqlDbContext<OrderingServiceDbContext>(connectionName: DbConstants.MasterConnectionStringName, configureDbContextOptions: options =>
{
new NpgsqlDbContextOptionsBuilder(options).MigrationsHistoryTable(DbConstants.MigrationsHistoryTableName);
options.UseSnakeCaseNamingConvention();
});
builder.Services.AddSingleton<MigrationService<OrderingServiceDbContext>>().AddHostedService<DataSeeder>();
builder.Services.AddScoped<IClientRequestManager, ClientRequestManager>();
builder.Services.AddMediatR(options =>
@ -78,7 +78,6 @@ namespace HelloShop.OrderingService.Extensions
{
app.UseCors(options => options.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader());
app.UseOpenApi();
app.UseDataSeedingProviders();
app.MapDaprEventBus();
return app;

View File

@ -12,7 +12,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace HelloShop.OrderingService.Infrastructure.Migrations
{
[DbContext(typeof(OrderingServiceDbContext))]
[Migration("20241123020913_InitialCreate")]
[Migration("20250325131032_InitialCreate")]
partial class InitialCreate
{
/// <inheritdoc />
@ -20,7 +20,7 @@ namespace HelloShop.OrderingService.Infrastructure.Migrations
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.10")
.HasAnnotation("ProductVersion", "9.0.3")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);

View File

@ -17,7 +17,7 @@ namespace HelloShop.OrderingService.Infrastructure.Migrations
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.10")
.HasAnnotation("ProductVersion", "9.0.3")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);

View File

@ -2,6 +2,8 @@
// See the license file in the project root for more information.
using HelloShop.OrderingService.Extensions;
using HelloShop.OrderingService.Infrastructure;
using HelloShop.OrderingService.Services;
var builder = WebApplication.CreateBuilder(args);
@ -19,4 +21,6 @@ app.MapApplicationEndpoints();
app.MapControllers();
app.Run();
await app.Services.GetRequiredService<MigrationService<OrderingServiceDbContext>>().ExecuteAsync();
await app.RunAsync();

View File

@ -0,0 +1,49 @@
// Copyright (c) HelloShop Corporation. All rights reserved.
// See the license file in the project root for more information.
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage;
using System.Diagnostics;
namespace HelloShop.OrderingService.Services
{
public class MigrationService<TDbContext>(IServiceScopeFactory scopeFactory) where TDbContext : DbContext
{
public const string ActivitySourceName = "Migrations";
private static readonly ActivitySource s_activitySource = new(ActivitySourceName);
public async ValueTask ExecuteAsync(CancellationToken cancellationToken = default)
{
using var activity = s_activitySource.StartActivity("Migrating database", ActivityKind.Client);
try
{
using var scope = scopeFactory.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<TDbContext>();
await RunMigrationAsync(dbContext, cancellationToken);
}
catch (Exception ex)
{
activity?.AddException(ex);
}
}
private static async ValueTask RunMigrationAsync(TDbContext dbContext, CancellationToken cancellationToken)
{
var strategy = dbContext.Database.CreateExecutionStrategy();
var dbCreator = dbContext.GetService<IRelationalDatabaseCreator>();
var historyRepository = dbContext.GetService<IHistoryRepository>();
await strategy.ExecuteAsync(async () =>
{
if (!await dbCreator.ExistsAsync(cancellationToken))
{
await dbCreator.CreateAsync(cancellationToken);
}
await historyRepository.CreateIfNotExistsAsync();
await dbContext.Database.MigrateAsync(cancellationToken);
});
}
}
}

View File

@ -0,0 +1,14 @@
// Copyright (c) HelloShop Corporation. All rights reserved.
// See the license file in the project root for more information.
namespace HelloShop.OrderingService.Workers
{
public class DataSeeder : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await Task.CompletedTask;
}
}
}

View File

@ -1,56 +0,0 @@
// Copyright (c) HelloShop Corporation. All rights reserved.
// See the license file in the project root for more information.
using HelloShop.ProductService.Entities.Products;
using HelloShop.ProductService.Infrastructure;
using HelloShop.ServiceDefaults.Infrastructure;
using Microsoft.EntityFrameworkCore;
using System.Text.Json;
namespace HelloShop.ProductService.DataSeeding
{
public class ProductDataSeedingProvider(ProductServiceDbContext dbContext, IWebHostEnvironment env, ILogger<ProductDataSeedingProvider> logger) : IDataSeedingProvider
{
public record CatalogSourceEntry(int Id, string Name, string Type, string Brand, string Description, decimal Price);
public async Task SeedingAsync(IServiceProvider ServiceProvider, CancellationToken cancellationToken = default)
{
await dbContext.Database.EnsureCreatedAsync(cancellationToken);
if (!dbContext.Set<Product>().Any())
{
string sourcePath = Path.Combine(env.ContentRootPath, "DataSeeding", "SetupCatalogSamples.json");
string sourceJson = await File.ReadAllTextAsync(sourcePath, cancellationToken);
var catalogItems = JsonSerializer.Deserialize<IEnumerable<CatalogSourceEntry>>(sourceJson);
if (catalogItems != null && catalogItems.Any())
{
dbContext.RemoveRange(dbContext.Set<Brand>());
await dbContext.Set<Brand>().AddRangeAsync(catalogItems.DistinctBy(x => x.Type).Select(x => new Brand { Name = x.Type }), cancellationToken);
await dbContext.SaveChangesAsync(cancellationToken);
var brandIdsByName = await dbContext.Set<Brand>().ToDictionaryAsync(x => x.Name, x => x.Id, cancellationToken: cancellationToken);
logger.LogInformation("Seeded catalog with {NumBrands} brands", brandIdsByName.Count);
await dbContext.Set<Product>().AddRangeAsync(catalogItems.Select(x => new Product
{
Id = x.Id,
Name = x.Name,
Description = x.Description,
Price = x.Price,
BrandId = brandIdsByName[x.Type],
ImageUrl = $"https://oss.xcode.me/notes/helloshop/products/{x.Id}.webp",
AvailableStock = 100
}), cancellationToken);
int result = await dbContext.SaveChangesAsync(cancellationToken);
logger.LogInformation("Seeded catalog with {NumItems} items", result);
}
}
}
}
}

View File

@ -12,7 +12,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace HelloShop.ProductService.Infrastructure.Migrations
{
[DbContext(typeof(ProductServiceDbContext))]
[Migration("20241123020927_InitialCreate")]
[Migration("20250325131047_InitialCreate")]
partial class InitialCreate
{
/// <inheritdoc />
@ -20,7 +20,7 @@ namespace HelloShop.ProductService.Infrastructure.Migrations
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.10")
.HasAnnotation("ProductVersion", "9.0.3")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);

View File

@ -17,7 +17,7 @@ namespace HelloShop.ProductService.Infrastructure.Migrations
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.10")
.HasAnnotation("ProductVersion", "9.0.3")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);

View File

@ -6,6 +6,8 @@ using HelloShop.EventBus.Abstractions;
using HelloShop.EventBus.Dapr;
using HelloShop.ProductService.Constants;
using HelloShop.ProductService.Infrastructure;
using HelloShop.ProductService.Services;
using HelloShop.ProductService.Workers;
using HelloShop.ServiceDefaults.Extensions;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
@ -35,8 +37,8 @@ builder.AddNpgsqlDbContext<ProductServiceDbContext>(connectionName: DbConstants.
new NpgsqlDbContextOptionsBuilder(options).MigrationsHistoryTable(DbConstants.MigrationsHistoryTableName);
options.UseSnakeCaseNamingConvention();
});
builder.Services.AddSingleton<MigrationService<ProductServiceDbContext>>().AddHostedService<DataSeeder>();
builder.Services.AddHttpClient().AddHttpContextAccessor().AddDistributedMemoryCache();
builder.Services.AddDataSeedingProviders();
builder.Services.AddCustomLocalization();
builder.Services.AddOpenApi();
builder.Services.AddModelMapper().AddModelValidator();
@ -58,14 +60,15 @@ app.UseCors(options => options.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader(
app.MapControllers();
// Configure extensions request pipeline.
app.UseDataSeedingProviders();
app.UseCustomLocalization();
app.UseOpenApi();
app.MapGroup("api/Permissions").MapPermissionDefinitions("Permissions");
app.MapDaprEventBus();
// End configure extensions request pipeline.
app.Run();
await app.Services.GetRequiredService<MigrationService<ProductServiceDbContext>>().ExecuteAsync();
await app.RunAsync();
/// <summary>
/// The test project requires a public Program type.

View File

@ -0,0 +1,49 @@
// Copyright (c) HelloShop Corporation. All rights reserved.
// See the license file in the project root for more information.
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage;
using System.Diagnostics;
namespace HelloShop.ProductService.Services
{
public class MigrationService<TDbContext>(IServiceScopeFactory scopeFactory) where TDbContext : DbContext
{
public const string ActivitySourceName = "Migrations";
private static readonly ActivitySource s_activitySource = new(ActivitySourceName);
public async ValueTask ExecuteAsync(CancellationToken cancellationToken = default)
{
using var activity = s_activitySource.StartActivity("Migrating database", ActivityKind.Client);
try
{
using var scope = scopeFactory.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<TDbContext>();
await RunMigrationAsync(dbContext, cancellationToken);
}
catch (Exception ex)
{
activity?.AddException(ex);
}
}
private static async ValueTask RunMigrationAsync(TDbContext dbContext, CancellationToken cancellationToken)
{
var strategy = dbContext.Database.CreateExecutionStrategy();
var dbCreator = dbContext.GetService<IRelationalDatabaseCreator>();
var historyRepository = dbContext.GetService<IHistoryRepository>();
await strategy.ExecuteAsync(async () =>
{
if (!await dbCreator.ExistsAsync(cancellationToken))
{
await dbCreator.CreateAsync(cancellationToken);
}
await historyRepository.CreateIfNotExistsAsync();
await dbContext.Database.MigrateAsync(cancellationToken);
});
}
}
}

View File

@ -0,0 +1,65 @@
// Copyright (c) HelloShop Corporation. All rights reserved.
// See the license file in the project root for more information.
using HelloShop.ProductService.Entities.Products;
using HelloShop.ProductService.Infrastructure;
using Microsoft.EntityFrameworkCore;
using System.Text.Json;
namespace HelloShop.ProductService.Workers
{
public class DataSeeder(IServiceScopeFactory scopeFactory) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
using var scope = scopeFactory.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<ProductServiceDbContext>();
var strategy = dbContext.Database.CreateExecutionStrategy();
var env = scope.ServiceProvider.GetRequiredService<IWebHostEnvironment>();
var timeProvider = scope.ServiceProvider.GetRequiredService<TimeProvider>();
var logger = scope.ServiceProvider.GetRequiredService<ILogger<DataSeeder>>();
await strategy.ExecuteAsync(async () =>
{
await using var transaction = await dbContext.Database.BeginTransactionAsync(stoppingToken);
if (!dbContext.Set<Product>().Any())
{
string sourcePath = Path.Combine(env.ContentRootPath, "Workers", "SetupCatalogSamples.json");
using var stream = new FileStream(sourcePath, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 4096, useAsync: true);
var catalogItems = await JsonSerializer.DeserializeAsync<IEnumerable<CatalogSourceEntry>>(stream, cancellationToken: stoppingToken);
if (catalogItems != null && catalogItems.Any())
{
dbContext.RemoveRange(dbContext.Set<Brand>());
await dbContext.Set<Brand>().AddRangeAsync(catalogItems.DistinctBy(x => x.Type).Select(x => new Brand { Name = x.Type }), stoppingToken);
await dbContext.SaveChangesAsync(stoppingToken);
var brandIdsByName = await dbContext.Set<Brand>().ToDictionaryAsync(x => x.Name, x => x.Id, cancellationToken: stoppingToken);
logger.LogInformation("Seeded catalog with {NumBrands} brands.", brandIdsByName.Count);
await dbContext.Set<Product>().AddRangeAsync(catalogItems.Select(x => new Product
{
Id = x.Id,
Name = x.Name,
Description = x.Description,
Price = x.Price,
BrandId = brandIdsByName[x.Type],
ImageUrl = $"https://oss.xcode.me/notes/helloshop/products/{x.Id}.webp",
AvailableStock = 100,
CreationTime = timeProvider.GetUtcNow()
}), stoppingToken);
int result = await dbContext.SaveChangesAsync(stoppingToken);
logger.LogInformation("Seeded catalog with {NumItems} items.", result);
}
}
await transaction.CommitAsync(stoppingToken);
});
}
private record CatalogSourceEntry(int Id, string Name, string Type, string Brand, string Description, decimal Price);
}
}

View File

@ -1,38 +0,0 @@
// Copyright (c) HelloShop Corporation. All rights reserved.
// See the license file in the project root for more information.
using HelloShop.ServiceDefaults.Infrastructure;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using System.Reflection;
namespace HelloShop.ServiceDefaults.Extensions
{
public static class DataSeedingExtensions
{
public static IServiceCollection AddDataSeedingProviders(this IServiceCollection services, Assembly? assembly = null)
{
assembly ??= Assembly.GetCallingAssembly();
var dataSeedProviders = assembly.ExportedTypes.Where(t => t.IsAssignableTo(typeof(IDataSeedingProvider)) && t.IsClass);
dataSeedProviders.ToList().ForEach(t => services.AddTransient(typeof(IDataSeedingProvider), t));
return services;
}
public static IApplicationBuilder UseDataSeedingProviders(this IApplicationBuilder app)
{
using var serviceScope = app.ApplicationServices.CreateScope();
var dataSeedingProviders = serviceScope.ServiceProvider.GetServices<IDataSeedingProvider>().OrderBy(x => x.Order);
foreach (var dataSeedingProvider in dataSeedingProviders)
{
dataSeedingProvider.SeedingAsync(serviceScope.ServiceProvider).Wait();
}
return app;
}
}
}

View File

@ -1,12 +0,0 @@
// Copyright (c) HelloShop Corporation. All rights reserved.
// See the license file in the project root for more information.
namespace HelloShop.ServiceDefaults.Infrastructure
{
public interface IDataSeedingProvider
{
int Order => default;
Task SeedingAsync(IServiceProvider serviceProvider, CancellationToken cancellationToken = default);
}
}