自动迁移和数据库初始化
This commit is contained in:
parent
ccf1e7672d
commit
8da853a351
@ -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");
|
||||
|
@ -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");
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
@ -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);
|
||||
|
@ -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();
|
||||
|
49
src/HelloShop.IdentityService/Services/MigrationService.cs
Normal file
49
src/HelloShop.IdentityService/Services/MigrationService.cs
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
89
src/HelloShop.IdentityService/Workers/DataSeeder.cs
Normal file
89
src/HelloShop.IdentityService/Workers/DataSeeder.cs
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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);
|
@ -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);
|
||||
|
@ -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();
|
||||
|
49
src/HelloShop.OrderingService/Services/MigrationService.cs
Normal file
49
src/HelloShop.OrderingService/Services/MigrationService.cs
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
14
src/HelloShop.OrderingService/Workers/DataSeeder.cs
Normal file
14
src/HelloShop.OrderingService/Workers/DataSeeder.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
@ -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);
|
||||
|
@ -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.
|
||||
|
49
src/HelloShop.ProductService/Services/MigrationService.cs
Normal file
49
src/HelloShop.ProductService/Services/MigrationService.cs
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
65
src/HelloShop.ProductService/Workers/DataSeeder.cs
Normal file
65
src/HelloShop.ProductService/Workers/DataSeeder.cs
Normal 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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user