文章目錄
- 項目地址
- 一、Infrastructure Layer
- 1.1 創建Application層需要的服務
- 1. Clock服務
- 2. Email 服務
- 3. 注冊服務
- 1.2 數據庫服務
- 1. 表配置Configurations
- 2. Respository實現
- 3. 數據庫鏈接Factory實現
- 4. Dapper的DataOnly服務實現
- 5. 所有數據庫服務注冊
- 1.3 基于RowVersion的樂觀鎖
- 1. 添加RowVersion字段
- 2. 修改ReserveBookingCommandHandler
項目地址
- 教程作者:
- 教程地址:
- 代碼倉庫地址:
- 所用到的框架和插件:
dbt
airflow
一、Infrastructure Layer
添加對Application層和Domain層的引用
1.1 創建Application層需要的服務
1. Clock服務
- 當前時間接口的實現
using Bookify.Application.Abstractions.Clock;
namespace Bookify.Infrastructure.Clock;
internal sealed class DateTimeProvider : IDateTimeProvider
{public DateTime UtcNow => DateTime.UtcNow;
}
2. Email 服務
- Email的服務,實現Appliaction的接口
using Bookify.Application.Abstractions.Email;
namespace Bookify.Infrastructure.Email;
internal sealed class EmailService : IEmailService
{public Task SendAsync(Domain.Users.Email recipient, string subject, string body){return Task.CompletedTask;}
}
3. 注冊服務
- 注冊上面的兩個服務
public static class DependencyInjection
{public static IServiceCollection AddInfrastructure(this IServiceCollection services,IConfiguration configuration){services.AddTransient<IDateTimeProvider, DateTimeProvider>();services.AddTransient<IEmailService, EmailService>();return services;}
}
1.2 數據庫服務
1. 表配置Configurations
BookingConfiguration.cs
internal sealed class BookingConfiguration : IEntityTypeConfiguration<Booking>
{public void Configure(EntityTypeBuilder<Booking> builder){//設置數據庫表名為 bookingsbuilder.ToTable("bookings");//設置主鍵為 Idbuilder.HasKey(booking => booking.Id);//OwnsOne處理值對象builder.OwnsOne(booking => booking.PriceForPeriod, priceBuilder =>{priceBuilder.Property(money => money.Currency).HasConversion(currency => currency.Code, code => Currency.FromCode(code));});builder.OwnsOne(booking => booking.CleaningFee, priceBuilder =>{priceBuilder.Property(money => money.Currency).HasConversion(currency => currency.Code, code => Currency.FromCode(code));});builder.OwnsOne(booking => booking.AmenitiesUpCharge, priceBuilder =>{priceBuilder.Property(money => money.Currency).HasConversion(currency => currency.Code, code => Currency.FromCode(code));});builder.OwnsOne(booking => booking.TotalPrice, priceBuilder =>{priceBuilder.Property(money => money.Currency).HasConversion(currency => currency.Code, code => Currency.FromCode(code));});builder.OwnsOne(booking => booking.Duration);builder.HasOne<Apartment>().WithMany().HasForeignKey(booking => booking.ApartmentId);builder.HasOne<User>().WithMany().HasForeignKey(booking => booking.UserId);}
}
ApartmentConfiguration.cs
internal sealed class ApartmentConfiguration : IEntityTypeConfiguration<Apartment>
{public void Configure(EntityTypeBuilder<Apartment> builder){builder.ToTable("apartments");builder.HasKey(apartment => apartment.Id);builder.OwnsOne(apartment => apartment.Address);builder.Property(apartment => apartment.Name).HasMaxLength(200).HasConversion(name => name.Value, value => new Name(value));builder.Property(apartment => apartment.Description).HasMaxLength(2000).HasConversion(description => description.Value, value => new Description(value));builder.OwnsOne(apartment => apartment.Price, priceBuilder =>{priceBuilder.Property(money => money.Currency).HasConversion(currency => currency.Code, code => Currency.FromCode(code));});builder.OwnsOne(apartment => apartment.CleaningFee, priceBuilder =>{priceBuilder.Property(money => money.Currency).HasConversion(currency => currency.Code, code => Currency.FromCode(code));});builder.Property<uint>("Version").IsRowVersion();}
}
2. Respository實現
Repository.cs
internal abstract class Repository<T>where T : Entity
{protected readonly ApplicationDbContext DbContext;protected Repository(ApplicationDbContext dbContext){DbContext = dbContext;}public async Task<T?> GetByIdAsync(Guid id,CancellationToken cancellationToken = default){return await DbContext.Set<T>().FirstOrDefaultAsync(user => user.Id == id, cancellationToken);}public void Add(T entity){DbContext.Add(entity);}
}
BookingRepository.cs
namespace Bookify.Infrastructure.Repositories;internal sealed class BookingRepository : Repository<Booking>, IBookingRepository
{private static readonly BookingStatus[] ActiveBookingStatuses ={BookingStatus.Reserved,BookingStatus.Confirmed,BookingStatus.Completed};public BookingRepository(ApplicationDbContext dbContext): base(dbContext){}public async Task<bool> IsOverlappingAsync(Apartment apartment,DateRange duration,CancellationToken cancellationToken = default){return await DbContext.Set<Booking>().AnyAsync(booking =>booking.ApartmentId == apartment.Id &&booking.Duration.Start <= duration.End &&booking.Duration.End >= duration.Start &&ActiveBookingStatuses.Contains(booking.Status),cancellationToken);}
}
3. 數據庫鏈接Factory實現
//創建Dapper鏈接數據庫的工廠
internal sealed class SqlConnectionFactory : ISqlConnectionFactory
{private readonly string _connectionString;public SqlConnectionFactory(string connectionString){_connectionString = connectionString;}public IDbConnection CreateConnection(){var connection = new NpgsqlConnection(_connectionString);connection.Open();return connection;}
}
4. Dapper的DataOnly服務實現
namespace Bookify.Infrastructure.Data;
/// <summary>
/// A Dapper type handler for <see cref="DateOnly"/>.
/// </summary>
internal sealed class DateOnlyTypeHandler : SqlMapper.TypeHandler<DateOnly>
{public override DateOnly Parse(object value) => DateOnly.FromDateTime((DateTime)value);public override void SetValue(IDbDataParameter parameter, DateOnly value){parameter.DbType = DbType.Date;parameter.Value = value;}
}
5. 所有數據庫服務注冊
- 注冊所有infrastructure的服務
public static class DependencyInjection
{public static IServiceCollection AddInfrastructure(this IServiceCollection services,IConfiguration configuration){services.AddTransient<IDateTimeProvider, DateTimeProvider>();services.AddTransient<IEmailService, EmailService>();string connectionString =configuration.GetConnectionString("Database") ??throw new ArgumentNullException(nameof(configuration));services.AddDbContext<ApplicationDbContext>(options =>{options.UseNpgsql(connectionString).UseSnakeCaseNamingConvention();});services.AddScoped<IUserRepository, UserRepository>();services.AddScoped<IApartmentRepository, ApartmentRepository>();services.AddScoped<IBookingRepository, BookingRepository>();services.AddScoped<IUnitOfWork>(sp => sp.GetRequiredService<ApplicationDbContext>());services.AddSingleton<ISqlConnectionFactory>(_ =>new SqlConnectionFactory(connectionString));SqlMapper.AddTypeHandler(new DateOnlyTypeHandler());return services;}
}
1.3 基于RowVersion的樂觀鎖
1. 添加RowVersion字段
- 這里我們預定apartment需要樂觀鎖,所以在他的數據庫configuration里添加
2. 修改ReserveBookingCommandHandler
- 發成沖突后,返回已經預定的錯誤