using MediatR; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Metadata.Builders; using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.BuyerAggregate; using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate; using Microsoft.eShopOnContainers.Services.Ordering.Domain.Seedwork; using Microsoft.eShopOnContainers.Services.Ordering.Infrastructure.Idempotency; using Ordering.Infrastructure; using System; using System.Threading; using System.Threading.Tasks; namespace Microsoft.eShopOnContainers.Services.Ordering.Infrastructure { public class OrderingContext : DbContext,IUnitOfWork { const string DEFAULT_SCHEMA = "ordering"; public DbSet Orders { get; set; } public DbSet OrderItems { get; set; } public DbSet Payments { get; set; } public DbSet Buyers { get; set; } public DbSet CardTypes { get; set; } public DbSet OrderStatus { get; set; } private readonly IMediator _mediator; public OrderingContext(DbContextOptions options, IMediator mediator) : base(options) { _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator)); } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity(ConfigureRequests); modelBuilder.Entity
(ConfigureAddress); modelBuilder.Entity(ConfigurePayment); modelBuilder.Entity(ConfigureOrder); modelBuilder.Entity(ConfigureOrderItems); modelBuilder.Entity(ConfigureCardTypes); modelBuilder.Entity(ConfigureOrderStatus); modelBuilder.Entity(ConfigureBuyer); } private void ConfigureRequests(EntityTypeBuilder requestConfiguration) { requestConfiguration.ToTable("requests", DEFAULT_SCHEMA); requestConfiguration.HasKey(cr => cr.Id); requestConfiguration.Property(cr => cr.Name).IsRequired(); requestConfiguration.Property(cr => cr.Time).IsRequired(); } void ConfigureAddress(EntityTypeBuilder
addressConfiguration) { addressConfiguration.ToTable("address", DEFAULT_SCHEMA); // DDD Pattern comment: Implementing the Address Id as "Shadow property" // becuase the Address is a Value-Object (VO) and an Id (Identity) is not desired for a VO // EF Core just needs the Id so it is capable to store it in a database table // See: https://docs.microsoft.com/en-us/ef/core/modeling/shadow-properties addressConfiguration.Property("Id") .IsRequired(); addressConfiguration.HasKey("Id"); } void ConfigureBuyer(EntityTypeBuilder buyerConfiguration) { buyerConfiguration.ToTable("buyers", DEFAULT_SCHEMA); buyerConfiguration.HasKey(b => b.Id); buyerConfiguration.Ignore(b => b.DomainEvents); buyerConfiguration.Property(b => b.Id) .ForSqlServerUseSequenceHiLo("buyerseq", DEFAULT_SCHEMA); buyerConfiguration.Property(b=>b.IdentityGuid) .HasMaxLength(200) .IsRequired(); buyerConfiguration.HasIndex("IdentityGuid") .IsUnique(true); buyerConfiguration.HasMany(b => b.PaymentMethods) .WithOne() .HasForeignKey("BuyerId") .OnDelete(DeleteBehavior.Cascade); var navigation = buyerConfiguration.Metadata.FindNavigation(nameof(Buyer.PaymentMethods)); navigation.SetPropertyAccessMode(PropertyAccessMode.Field); } void ConfigurePayment(EntityTypeBuilder paymentConfiguration) { paymentConfiguration.ToTable("paymentmethods", DEFAULT_SCHEMA); paymentConfiguration.HasKey(b => b.Id); paymentConfiguration.Ignore(b => b.DomainEvents); paymentConfiguration.Property(b => b.Id) .ForSqlServerUseSequenceHiLo("paymentseq", DEFAULT_SCHEMA); paymentConfiguration.Property("BuyerId") .IsRequired(); paymentConfiguration.Property("CardHolderName") .HasMaxLength(200) .IsRequired(); paymentConfiguration.Property("Alias") .HasMaxLength(200) .IsRequired(); paymentConfiguration.Property("CardNumber") .HasMaxLength(25) .IsRequired(); paymentConfiguration.Property("Expiration") .IsRequired(); paymentConfiguration.Property("CardTypeId") .IsRequired(); paymentConfiguration.HasOne(p => p.CardType) .WithMany() .HasForeignKey("CardTypeId"); } void ConfigureOrder(EntityTypeBuilder orderConfiguration) { orderConfiguration.ToTable("orders", DEFAULT_SCHEMA); orderConfiguration.HasKey(o => o.Id); orderConfiguration.Ignore(b => b.DomainEvents); orderConfiguration.Property(o => o.Id) .ForSqlServerUseSequenceHiLo("orderseq", DEFAULT_SCHEMA); orderConfiguration.Property("OrderDate").IsRequired(); orderConfiguration.Property("BuyerId").IsRequired(false); orderConfiguration.Property("OrderStatusId").IsRequired(); orderConfiguration.Property("PaymentMethodId").IsRequired(false); var navigation = orderConfiguration.Metadata.FindNavigation(nameof(Order.OrderItems)); // DDD Patterns comment: //Set as Field (New since EF 1.1) to access the OrderItem collection property through its field navigation.SetPropertyAccessMode(PropertyAccessMode.Field); orderConfiguration.HasOne() .WithMany() .HasForeignKey("PaymentMethodId") .IsRequired(false) .OnDelete(DeleteBehavior.Restrict); orderConfiguration.HasOne() .WithMany() .IsRequired(false) .HasForeignKey("BuyerId"); orderConfiguration.HasOne(o => o.OrderStatus) .WithMany() .HasForeignKey("OrderStatusId"); } void ConfigureOrderItems(EntityTypeBuilder orderItemConfiguration) { orderItemConfiguration.ToTable("orderItems", DEFAULT_SCHEMA); orderItemConfiguration.HasKey(o => o.Id); orderItemConfiguration.Ignore(b => b.DomainEvents); orderItemConfiguration.Property(o => o.Id) .ForSqlServerUseSequenceHiLo("orderitemseq"); orderItemConfiguration.Property("OrderId") .IsRequired(); orderItemConfiguration.Property("Discount") .IsRequired(); orderItemConfiguration.Property("ProductId") .IsRequired(); orderItemConfiguration.Property("ProductName") .IsRequired(); orderItemConfiguration.Property("UnitPrice") .IsRequired(); orderItemConfiguration.Property("Units") .IsRequired(); orderItemConfiguration.Property("PictureUrl") .IsRequired(false); } void ConfigureOrderStatus(EntityTypeBuilder orderStatusConfiguration) { orderStatusConfiguration.ToTable("orderstatus", DEFAULT_SCHEMA); orderStatusConfiguration.HasKey(o => o.Id); orderStatusConfiguration.Property(o => o.Id) .HasDefaultValue(1) .ValueGeneratedNever() .IsRequired(); orderStatusConfiguration.Property(o => o.Name) .HasMaxLength(200) .IsRequired(); } void ConfigureCardTypes(EntityTypeBuilder cardTypesConfiguration) { cardTypesConfiguration.ToTable("cardtypes", DEFAULT_SCHEMA); cardTypesConfiguration.HasKey(ct => ct.Id); cardTypesConfiguration.Property(ct => ct.Id) .HasDefaultValue(1) .ValueGeneratedNever() .IsRequired(); cardTypesConfiguration.Property(ct => ct.Name) .HasMaxLength(200) .IsRequired(); } public async Task SaveEntitiesAsync(CancellationToken cancellationToken = default(CancellationToken)) { // Dispatch Domain Events collection. // Choices: // A) Right BEFORE committing data (EF SaveChanges) into the DB will make a single transaction including // side effects from the domain event handlers which are using the same DbContext with "InstancePerLifetimeScope" or "scoped" lifetime // B) Right AFTER committing data (EF SaveChanges) into the DB will make multiple transactions. // You will need to handle eventual consistency and compensatory actions in case of failures in any of the Handlers. await _mediator.DispatchDomainEventsAsync(this); // After executing this line all the changes (from the Command Handler and Domain Event Handlers) // performed throught the DbContext will be commited var result = await base.SaveChangesAsync(); return true; } } }