文章目录
- 项目地址
- 一、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)
{
//设置数据库表名为 bookings
builder.ToTable("bookings");
//设置主键为 Id
builder.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
- 发成冲突后,返回已经预定的错误