From aafe246cd399c8fce0747d12e16bee077449d7a4 Mon Sep 17 00:00:00 2001 From: Umer Iftikhar Date: Thu, 17 Sep 2020 18:14:09 +0300 Subject: [PATCH 1/8] Basic Example Added for handling the saga with EF core --- src/Chronicle.sln | 20 ++++- src/EFCoreTestApp/Commands/CreateOrder.cs | 22 ++++++ .../Controllers/OrderController.cs | 49 ++++++++++++ .../Controllers/SagaController.cs | 52 +++++++++++++ src/EFCoreTestApp/DTO/CreateOrderDTO.cs | 17 +++++ src/EFCoreTestApp/DTO/OrderCreatedDTO.cs | 11 +++ src/EFCoreTestApp/EFCoreTestApp.csproj | 19 +++++ .../SagaLogDataEntityTypeConfiguration.cs | 22 ++++++ .../SagaStateEntityTypeConfiguration.cs | 21 ++++++ src/EFCoreTestApp/Events/OrderCreated.cs | 18 +++++ .../Extensions/ChronicBuilder.cs | 17 +++++ .../Handlers/OrderSagaHandler.cs | 44 +++++++++++ .../Persistence/ISagaLogRepository.cs | 19 +++++ .../Persistence/ISagaStateDBRepository.cs | 18 +++++ .../Persistence/ISagaUnitOfWork.cs | 28 +++++++ .../Persistence/SagaDbContext.cs | 30 ++++++++ .../Persistence/SagaLogRepository.cs | 44 +++++++++++ .../Persistence/SagaStateRepository.cs | 52 +++++++++++++ .../Persistence/SagaUnitOfWork.cs | 28 +++++++ src/EFCoreTestApp/Program.cs | 21 ++++++ .../SagaRepository/EFCoreSagaLog.cs | 30 ++++++++ .../SagaRepository/EFCoreSagaLogData.cs | 34 +++++++++ .../SagaRepository/EFCoreSagaState.cs | 36 +++++++++ .../SagaRepository/EFCoreSagaStateData.cs | 63 ++++++++++++++++ src/EFCoreTestApp/Sagas/CreatingOrderData.cs | 11 +++ src/EFCoreTestApp/Sagas/OrderSaga.cs | 72 ++++++++++++++++++ src/EFCoreTestApp/Startup.cs | 75 +++++++++++++++++++ .../appsettings.Development.json | 9 +++ src/EFCoreTestApp/appsettings.json | 13 ++++ 29 files changed, 892 insertions(+), 3 deletions(-) create mode 100644 src/EFCoreTestApp/Commands/CreateOrder.cs create mode 100644 src/EFCoreTestApp/Controllers/OrderController.cs create mode 100644 src/EFCoreTestApp/Controllers/SagaController.cs create mode 100644 src/EFCoreTestApp/DTO/CreateOrderDTO.cs create mode 100644 src/EFCoreTestApp/DTO/OrderCreatedDTO.cs create mode 100644 src/EFCoreTestApp/EFCoreTestApp.csproj create mode 100644 src/EFCoreTestApp/EntityConfigurations/SagaLogDataEntityTypeConfiguration.cs create mode 100644 src/EFCoreTestApp/EntityConfigurations/SagaStateEntityTypeConfiguration.cs create mode 100644 src/EFCoreTestApp/Events/OrderCreated.cs create mode 100644 src/EFCoreTestApp/Extensions/ChronicBuilder.cs create mode 100644 src/EFCoreTestApp/Handlers/OrderSagaHandler.cs create mode 100644 src/EFCoreTestApp/Persistence/ISagaLogRepository.cs create mode 100644 src/EFCoreTestApp/Persistence/ISagaStateDBRepository.cs create mode 100644 src/EFCoreTestApp/Persistence/ISagaUnitOfWork.cs create mode 100644 src/EFCoreTestApp/Persistence/SagaDbContext.cs create mode 100644 src/EFCoreTestApp/Persistence/SagaLogRepository.cs create mode 100644 src/EFCoreTestApp/Persistence/SagaStateRepository.cs create mode 100644 src/EFCoreTestApp/Persistence/SagaUnitOfWork.cs create mode 100644 src/EFCoreTestApp/Program.cs create mode 100644 src/EFCoreTestApp/SagaRepository/EFCoreSagaLog.cs create mode 100644 src/EFCoreTestApp/SagaRepository/EFCoreSagaLogData.cs create mode 100644 src/EFCoreTestApp/SagaRepository/EFCoreSagaState.cs create mode 100644 src/EFCoreTestApp/SagaRepository/EFCoreSagaStateData.cs create mode 100644 src/EFCoreTestApp/Sagas/CreatingOrderData.cs create mode 100644 src/EFCoreTestApp/Sagas/OrderSaga.cs create mode 100644 src/EFCoreTestApp/Startup.cs create mode 100644 src/EFCoreTestApp/appsettings.Development.json create mode 100644 src/EFCoreTestApp/appsettings.json diff --git a/src/Chronicle.sln b/src/Chronicle.sln index a10f270..c428715 100644 --- a/src/Chronicle.sln +++ b/src/Chronicle.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.26124.0 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30309.148 MinimumVisualStudioVersion = 15.0.26124.0 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Chronicle", "Chronicle\Chronicle.csproj", "{04B4EFDC-54CE-4ACD-9295-9029D9556958}" EndProject @@ -9,7 +9,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestApp", "TestApp\TestApp. EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Chronicle.Tests", "Chronicle.Tests\Chronicle.Tests.csproj", "{0B686327-6650-4589-8DC9-6FCCFDCB24DB}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Chronicle.Integrations.MongoDB", "Chronicle.Integrations.MongoDB\src\Chronicle.Integrations.MongoDB.csproj", "{DFB344D1-3121-43F9-9E66-20BC6B0DD9CC}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Chronicle.Integrations.MongoDB", "Chronicle.Integrations.MongoDB\src\Chronicle.Integrations.MongoDB.csproj", "{DFB344D1-3121-43F9-9E66-20BC6B0DD9CC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EFCoreTestApp", "EFCoreTestApp\EFCoreTestApp.csproj", "{E47A600F-6802-4A9E-91E3-E8D213EC9195}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -69,6 +71,18 @@ Global {DFB344D1-3121-43F9-9E66-20BC6B0DD9CC}.Release|x64.Build.0 = Release|Any CPU {DFB344D1-3121-43F9-9E66-20BC6B0DD9CC}.Release|x86.ActiveCfg = Release|Any CPU {DFB344D1-3121-43F9-9E66-20BC6B0DD9CC}.Release|x86.Build.0 = Release|Any CPU + {E47A600F-6802-4A9E-91E3-E8D213EC9195}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E47A600F-6802-4A9E-91E3-E8D213EC9195}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E47A600F-6802-4A9E-91E3-E8D213EC9195}.Debug|x64.ActiveCfg = Debug|Any CPU + {E47A600F-6802-4A9E-91E3-E8D213EC9195}.Debug|x64.Build.0 = Debug|Any CPU + {E47A600F-6802-4A9E-91E3-E8D213EC9195}.Debug|x86.ActiveCfg = Debug|Any CPU + {E47A600F-6802-4A9E-91E3-E8D213EC9195}.Debug|x86.Build.0 = Debug|Any CPU + {E47A600F-6802-4A9E-91E3-E8D213EC9195}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E47A600F-6802-4A9E-91E3-E8D213EC9195}.Release|Any CPU.Build.0 = Release|Any CPU + {E47A600F-6802-4A9E-91E3-E8D213EC9195}.Release|x64.ActiveCfg = Release|Any CPU + {E47A600F-6802-4A9E-91E3-E8D213EC9195}.Release|x64.Build.0 = Release|Any CPU + {E47A600F-6802-4A9E-91E3-E8D213EC9195}.Release|x86.ActiveCfg = Release|Any CPU + {E47A600F-6802-4A9E-91E3-E8D213EC9195}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/EFCoreTestApp/Commands/CreateOrder.cs b/src/EFCoreTestApp/Commands/CreateOrder.cs new file mode 100644 index 0000000..8e0e4a9 --- /dev/null +++ b/src/EFCoreTestApp/Commands/CreateOrder.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using MediatR; + +namespace EFCoreTestApp.Commands +{ + public class CreateOrder: IRequest + { + public Guid OrderId { get; } + public Guid CustomerId { get; } + public Guid ParcelId { get; } + + public CreateOrder(Guid orderId, Guid customerId, Guid parcelId) + { + OrderId = orderId; + CustomerId = customerId; + ParcelId = parcelId; + } + } +} diff --git a/src/EFCoreTestApp/Controllers/OrderController.cs b/src/EFCoreTestApp/Controllers/OrderController.cs new file mode 100644 index 0000000..5a9d6f1 --- /dev/null +++ b/src/EFCoreTestApp/Controllers/OrderController.cs @@ -0,0 +1,49 @@ +using System; +using System.Threading.Tasks; +using Chronicle; +using MediatR; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using EFCoreTestApp.Commands; +using EFCoreTestApp.Events; +using EFCoreTestApp.Persistence; +using EFCoreTestApp.DTO; + +namespace EFCoreTestApp.Controllers +{ + [ApiController] + [Route("api/[controller]")] + public class OrderController : ControllerBase + { + private readonly IMediator _mediator; + + public OrderController(IMediator mediator) + { + _mediator = mediator; + } + + // [HttpPost, Route("{id:guid}")] + [HttpPost] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public async Task CreateOrder([FromBody] CreateOrderDTO dto) + { + // [FromRoute] Guid id + var request = new CreateOrder(dto.OrderId, dto.CustomerId, dto.ParcelId); + var result = await _mediator.Send(request); + return Ok(); + } + + [HttpPost("created")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public async Task OrderCreated([FromBody] OrderCreatedDTO dto) + { + // [FromRoute] Guid id + var notification = new OrderCreated(dto.OrderId); + await _mediator.Publish(notification); + return Ok(); + } + + } +} diff --git a/src/EFCoreTestApp/Controllers/SagaController.cs b/src/EFCoreTestApp/Controllers/SagaController.cs new file mode 100644 index 0000000..951b598 --- /dev/null +++ b/src/EFCoreTestApp/Controllers/SagaController.cs @@ -0,0 +1,52 @@ +using System; +using System.Threading.Tasks; +using Chronicle; +using MediatR; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using EFCoreTestApp.Commands; +using EFCoreTestApp.Events; +using EFCoreTestApp.Persistence; +using EFCoreTestApp.DTO; + +namespace EFCoreTestApp.Controllers +{ + /* + In PRODUCTION environment, this whole controller should be protected and only authorized users should + be able to access it. + */ + [ApiController] + [Route("api/[controller]")] + public class SagaController : ControllerBase + { + private readonly ISagaStateDBRepository _sagaStateDBRepository; + private readonly ISagaLogRepository _sagaLogRepository; + + public SagaController(ISagaStateDBRepository sagaStateDBRepository, ISagaLogRepository sagaLogRepository) + { + _sagaStateDBRepository = sagaStateDBRepository; + _sagaLogRepository = sagaLogRepository; + } + + [HttpGet, Route("{id:guid}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public async Task GetSagaById([FromRoute] Guid id) + { + // [FromRoute] Guid id + var sagaState = await _sagaStateDBRepository.GetByIdAsync((SagaId)id.ToString()); + return Ok(sagaState); + } + + [HttpGet, Route("{id:guid}/logs")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public async Task GetSagaLogsById([FromRoute] Guid id) + { + // [FromRoute] Guid id + var sagaState = await _sagaLogRepository.ReadByIdAsync((SagaId)id.ToString()); + return Ok(sagaState); + } + + } +} diff --git a/src/EFCoreTestApp/DTO/CreateOrderDTO.cs b/src/EFCoreTestApp/DTO/CreateOrderDTO.cs new file mode 100644 index 0000000..017d566 --- /dev/null +++ b/src/EFCoreTestApp/DTO/CreateOrderDTO.cs @@ -0,0 +1,17 @@ +using System; +using System.ComponentModel.DataAnnotations; + +namespace EFCoreTestApp.DTO +{ + public class CreateOrderDTO + { + [Required] + public Guid OrderId { get; set; } + + [Required] + public Guid CustomerId { get; set; } + + [Required] + public Guid ParcelId { get; set; } + } +} diff --git a/src/EFCoreTestApp/DTO/OrderCreatedDTO.cs b/src/EFCoreTestApp/DTO/OrderCreatedDTO.cs new file mode 100644 index 0000000..c502d0c --- /dev/null +++ b/src/EFCoreTestApp/DTO/OrderCreatedDTO.cs @@ -0,0 +1,11 @@ +using System; +using System.ComponentModel.DataAnnotations; + +namespace EFCoreTestApp.DTO +{ + public class OrderCreatedDTO + { + [Required] + public Guid OrderId { get; set; } + } +} diff --git a/src/EFCoreTestApp/EFCoreTestApp.csproj b/src/EFCoreTestApp/EFCoreTestApp.csproj new file mode 100644 index 0000000..1c57d2b --- /dev/null +++ b/src/EFCoreTestApp/EFCoreTestApp.csproj @@ -0,0 +1,19 @@ + + + + netcoreapp3.1 + + + + + + + + + + + + + + + diff --git a/src/EFCoreTestApp/EntityConfigurations/SagaLogDataEntityTypeConfiguration.cs b/src/EFCoreTestApp/EntityConfigurations/SagaLogDataEntityTypeConfiguration.cs new file mode 100644 index 0000000..e75ff0d --- /dev/null +++ b/src/EFCoreTestApp/EntityConfigurations/SagaLogDataEntityTypeConfiguration.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using EFCoreTestApp.SagaRepository; + +namespace EFCoreTestApp.EntityConfigurations +{ + public class SagaLogDataEntityTypeConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("SagaLog", "dbo"); + builder.HasKey(c => c.logId); + builder.Property(c => c.logId).ValueGeneratedOnAdd(); + builder.Ignore(c => c.Id); + builder.Ignore(c => c.Message); + } + } +} diff --git a/src/EFCoreTestApp/EntityConfigurations/SagaStateEntityTypeConfiguration.cs b/src/EFCoreTestApp/EntityConfigurations/SagaStateEntityTypeConfiguration.cs new file mode 100644 index 0000000..7246091 --- /dev/null +++ b/src/EFCoreTestApp/EntityConfigurations/SagaStateEntityTypeConfiguration.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using EFCoreTestApp.SagaRepository; + +namespace EFCoreTestApp.EntityConfigurations +{ + public class SagaStateEntityTypeConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("SagaState", "dbo"); + builder.HasKey(r => r.SagaId); + builder.Ignore(c => c.Id); + builder.Ignore(c => c.Data); + } + } +} diff --git a/src/EFCoreTestApp/Events/OrderCreated.cs b/src/EFCoreTestApp/Events/OrderCreated.cs new file mode 100644 index 0000000..dcd8cc7 --- /dev/null +++ b/src/EFCoreTestApp/Events/OrderCreated.cs @@ -0,0 +1,18 @@ +using MediatR; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace EFCoreTestApp.Events +{ + public class OrderCreated: INotification + { + public Guid OrderId { get; } + + public OrderCreated(Guid orderId) + { + OrderId = orderId; + } + } +} diff --git a/src/EFCoreTestApp/Extensions/ChronicBuilder.cs b/src/EFCoreTestApp/Extensions/ChronicBuilder.cs new file mode 100644 index 0000000..1d5de5a --- /dev/null +++ b/src/EFCoreTestApp/Extensions/ChronicBuilder.cs @@ -0,0 +1,17 @@ +using System; +using Chronicle; +using Microsoft.Extensions.DependencyInjection; +using EFCoreTestApp.SagaRepository; + +namespace EFCoreTestApp.Extensions +{ + public static class ChronicBuilder + { + public static IServiceCollection UserEfCoreForSaga(this IServiceCollection serviceCollection) + { + serviceCollection.AddTransient(typeof(ISagaStateRepository), typeof(EFCoreSagaState)); + serviceCollection.AddTransient(typeof(ISagaLog), typeof(EFCoreSagaLog)); + return serviceCollection; + } + } +} diff --git a/src/EFCoreTestApp/Handlers/OrderSagaHandler.cs b/src/EFCoreTestApp/Handlers/OrderSagaHandler.cs new file mode 100644 index 0000000..d28e956 --- /dev/null +++ b/src/EFCoreTestApp/Handlers/OrderSagaHandler.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Chronicle; +using MediatR; +using EFCoreTestApp.Commands; +using EFCoreTestApp.Events; +using EFCoreTestApp.Persistence; + +namespace EFCoreTestApp.Handlers +{ + public class OrderSagaHandler: IRequestHandler, INotificationHandler + { + private readonly ISagaCoordinator _coordinator; + private ISagaUnitOfWork SagaUnitOfWork { get; } + + public OrderSagaHandler(ISagaCoordinator coordinator, ISagaUnitOfWork _sagaUnitOfWork) + { + _coordinator = coordinator; + SagaUnitOfWork = _sagaUnitOfWork; + } + + public async Task Handle(CreateOrder command, CancellationToken cancellationToken) + { + await _coordinator.ProcessAsync(command, SagaContext.Empty); + // once every thing is processed in the handler, commit changes to DB. + // This can be moved else where depending on the user needs. + await SagaUnitOfWork.CommitAsync(); + return Unit.Value; + } + + public async Task Handle(OrderCreated notification, CancellationToken cancellationToken) + { + IEnumerable metadata = new List(); + await _coordinator.ProcessAsync(notification, + SagaContext.Create((SagaId)notification.OrderId.ToString(), notification.GetType().Name, metadata)); + // once every thing is processed in the handler, commit changes to DB. + // This can be moved else where depending on the user needs. + await SagaUnitOfWork.CommitAsync(); + } + } +} diff --git a/src/EFCoreTestApp/Persistence/ISagaLogRepository.cs b/src/EFCoreTestApp/Persistence/ISagaLogRepository.cs new file mode 100644 index 0000000..1a31016 --- /dev/null +++ b/src/EFCoreTestApp/Persistence/ISagaLogRepository.cs @@ -0,0 +1,19 @@ +using Chronicle; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using EFCoreTestApp.SagaRepository; + +namespace EFCoreTestApp.Persistence +{ + public interface ISagaLogRepository + { + Task> ReadAsync(SagaId id, Type type); + + Task> ReadByIdAsync(SagaId id); + + Task WriteAsync(EFCoreSagaLogData message); + } +} diff --git a/src/EFCoreTestApp/Persistence/ISagaStateDBRepository.cs b/src/EFCoreTestApp/Persistence/ISagaStateDBRepository.cs new file mode 100644 index 0000000..96b2a91 --- /dev/null +++ b/src/EFCoreTestApp/Persistence/ISagaStateDBRepository.cs @@ -0,0 +1,18 @@ +using Chronicle; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using EFCoreTestApp.SagaRepository; + +namespace EFCoreTestApp.Persistence +{ + public interface ISagaStateDBRepository + { + Task ReadAsync(SagaId id, Type type); + Task WriteAsync(EFCoreSagaStateData message); + + Task GetByIdAsync(SagaId id); + } +} diff --git a/src/EFCoreTestApp/Persistence/ISagaUnitOfWork.cs b/src/EFCoreTestApp/Persistence/ISagaUnitOfWork.cs new file mode 100644 index 0000000..3e9c74b --- /dev/null +++ b/src/EFCoreTestApp/Persistence/ISagaUnitOfWork.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace EFCoreTestApp.Persistence +{ + public interface IUnitOfWork + { + Task CommitAsync(CancellationToken cancellationToken = default(CancellationToken)); + + // Task BeginTransactionAsync(CancellationToken cancellationToken = default(CancellationToken)); + } + + public interface ITransaction : IDisposable + { + void Commit(); + + void Rollback(); + } + + public interface ISagaUnitOfWork : IUnitOfWork + { + ISagaLogRepository SagaLogRepository { get; } + ISagaStateDBRepository SagaStateDBRepository { get; } + } +} diff --git a/src/EFCoreTestApp/Persistence/SagaDbContext.cs b/src/EFCoreTestApp/Persistence/SagaDbContext.cs new file mode 100644 index 0000000..7f205b4 --- /dev/null +++ b/src/EFCoreTestApp/Persistence/SagaDbContext.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Chronicle; +using Microsoft.EntityFrameworkCore; +using EFCoreTestApp.EntityConfigurations; +using EFCoreTestApp.SagaRepository; + +namespace EFCoreTestApp.Persistence +{ + public class SagaDbContext : DbContext + { + public SagaDbContext(DbContextOptions options) + : base(options) + { + Database.EnsureCreated(); + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.ApplyConfiguration(new SagaLogDataEntityTypeConfiguration()); + modelBuilder.ApplyConfiguration(new SagaStateEntityTypeConfiguration()); + } + + public DbSet SagaLog { get; set; } + + public DbSet SagaState { get; set; } + } +} diff --git a/src/EFCoreTestApp/Persistence/SagaLogRepository.cs b/src/EFCoreTestApp/Persistence/SagaLogRepository.cs new file mode 100644 index 0000000..c3602f4 --- /dev/null +++ b/src/EFCoreTestApp/Persistence/SagaLogRepository.cs @@ -0,0 +1,44 @@ +using Chronicle; +using System; +using System.Collections.Generic; +using System.Threading; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using System.Collections.Immutable; +using EFCoreTestApp.SagaRepository; + +namespace EFCoreTestApp.Persistence +{ + public class SagaLogRepository : ISagaLogRepository + { + private readonly SagaDbContext _dbContext; + + public SagaLogRepository(SagaDbContext dbContext) + { + _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext)); + } + + public async Task> ReadAsync(SagaId id, Type type) + { + return await _dbContext.SagaLog + .Where(sld => sld.SagaId == id.Id && sld.SagaType == type.FullName) + .ToArrayAsync(); + } + + public async Task> ReadByIdAsync(SagaId id) + { + return await _dbContext.SagaLog + .Where(sld => sld.SagaId == id.Id) + .ToArrayAsync(); + } + + public async Task WriteAsync(EFCoreSagaLogData message) + { + if (null == message) + throw new ArgumentNullException(nameof(message)); + await _dbContext.SagaLog.AddAsync(message); + // await _dbContext.SaveChangesAsync(); + } + } +} diff --git a/src/EFCoreTestApp/Persistence/SagaStateRepository.cs b/src/EFCoreTestApp/Persistence/SagaStateRepository.cs new file mode 100644 index 0000000..a532603 --- /dev/null +++ b/src/EFCoreTestApp/Persistence/SagaStateRepository.cs @@ -0,0 +1,52 @@ +using Chronicle; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using EFCoreTestApp.SagaRepository; +using Newtonsoft.Json; + +namespace EFCoreTestApp.Persistence +{ + public class SagaStateRepository : ISagaStateDBRepository + { + private readonly SagaDbContext _dbContext; + + public SagaStateRepository(SagaDbContext dbContext) + { + _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext)); + } + + + public async Task GetByIdAsync(SagaId sagaId) + { + return await _dbContext.SagaState + .FirstOrDefaultAsync(sld => sld.SagaId == sagaId.Id); + } + + public async Task ReadAsync(SagaId id, Type type) + { + return await _dbContext.SagaState + .FirstOrDefaultAsync(sld => sld.SagaId == id.Id && sld.SagaType == type.FullName); + } + + public async Task WriteAsync(EFCoreSagaStateData sagaState) + { + var entity = await _dbContext + .SagaState + .FirstOrDefaultAsync(sld => sld.SagaId == sagaState.Id.Id && sld.SagaType == sagaState.SagaType); + if(entity != null) + { + _dbContext.SagaState.Remove(entity); + } + + await _dbContext.SagaState.AddAsync( + new EFCoreSagaStateData(sagaState.Id.Id, sagaState.SagaType, sagaState.State, JsonConvert.SerializeObject(sagaState.Data)) + ); + // await _dbContext.SaveChangesAsync(); + } + } +} diff --git a/src/EFCoreTestApp/Persistence/SagaUnitOfWork.cs b/src/EFCoreTestApp/Persistence/SagaUnitOfWork.cs new file mode 100644 index 0000000..d9f28c1 --- /dev/null +++ b/src/EFCoreTestApp/Persistence/SagaUnitOfWork.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace EFCoreTestApp.Persistence +{ + public class SagaUnitOfWork: ISagaUnitOfWork, IUnitOfWork + { + protected readonly SagaDbContext DbContext; + public ISagaLogRepository SagaLogRepository { get; } + public ISagaStateDBRepository SagaStateDBRepository { get; } + + public SagaUnitOfWork(SagaDbContext dbContext, ISagaLogRepository _sagaLogRepository, ISagaStateDBRepository _sagaStateDBRepository) + { + DbContext = dbContext; + SagaLogRepository = _sagaLogRepository; + SagaStateDBRepository = _sagaStateDBRepository; + } + + public async Task CommitAsync(CancellationToken cancellationToken) + { + await DbContext.SaveChangesAsync(cancellationToken); + } + + } +} diff --git a/src/EFCoreTestApp/Program.cs b/src/EFCoreTestApp/Program.cs new file mode 100644 index 0000000..335e07f --- /dev/null +++ b/src/EFCoreTestApp/Program.cs @@ -0,0 +1,21 @@ +using System; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace EFCoreTestApp +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/src/EFCoreTestApp/SagaRepository/EFCoreSagaLog.cs b/src/EFCoreTestApp/SagaRepository/EFCoreSagaLog.cs new file mode 100644 index 0000000..20bba81 --- /dev/null +++ b/src/EFCoreTestApp/SagaRepository/EFCoreSagaLog.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Chronicle; +using Newtonsoft.Json; +using EFCoreTestApp.Persistence; + +namespace EFCoreTestApp.SagaRepository +{ + public class EFCoreSagaLog: ISagaLog + { + public ISagaUnitOfWork SagaUnitOfWork { get; } + + public EFCoreSagaLog(ISagaUnitOfWork _sagaUnitOfWork) + { + SagaUnitOfWork = _sagaUnitOfWork ?? throw new ArgumentNullException(nameof(_sagaUnitOfWork)); + } + + public async Task> ReadAsync(SagaId id, Type type) + => await SagaUnitOfWork.SagaLogRepository + .ReadAsync(id, type); + + public async Task WriteAsync(ISagaLogData message) + { + await SagaUnitOfWork.SagaLogRepository + .WriteAsync(new EFCoreSagaLogData(message.Id.Id, message.Type.ToString(), message.CreatedAt, JsonConvert.SerializeObject(message.Message))); + } + } +} diff --git a/src/EFCoreTestApp/SagaRepository/EFCoreSagaLogData.cs b/src/EFCoreTestApp/SagaRepository/EFCoreSagaLogData.cs new file mode 100644 index 0000000..52eda9a --- /dev/null +++ b/src/EFCoreTestApp/SagaRepository/EFCoreSagaLogData.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using Chronicle; +using Newtonsoft.Json; + +namespace EFCoreTestApp.SagaRepository +{ + public class EFCoreSagaLogData : ISagaLogData + { + public int logId { get; set; } + public string SagaId { get; set; } + [NotMapped] + public SagaId Id => SagaId; + public string SagaType { get; set; } + public long CreatedAt { get; set; } + [NotMapped] + public object Message => JsonConvert.DeserializeObject(MessagePayload); + public string MessagePayload { get; set; } + + Type ISagaLogData.Type => Assembly.GetEntryAssembly()?.GetType(SagaType); + + public EFCoreSagaLogData(string sagaId, string sagaType, long createdAt, string messagePayload) + { + SagaId = sagaId; + SagaType = sagaType; + CreatedAt = createdAt; + MessagePayload = messagePayload; + } + } +} diff --git a/src/EFCoreTestApp/SagaRepository/EFCoreSagaState.cs b/src/EFCoreTestApp/SagaRepository/EFCoreSagaState.cs new file mode 100644 index 0000000..a7214cc --- /dev/null +++ b/src/EFCoreTestApp/SagaRepository/EFCoreSagaState.cs @@ -0,0 +1,36 @@ +using Chronicle; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using EFCoreTestApp.Persistence; + +namespace EFCoreTestApp.SagaRepository +{ + public class EFCoreSagaState : ISagaStateRepository + { + + public ISagaUnitOfWork SagaUnitOfWork { get; } + + public EFCoreSagaState(ISagaUnitOfWork _sagaUnitOfWork) + { + SagaUnitOfWork = _sagaUnitOfWork ?? throw new ArgumentNullException(nameof(_sagaUnitOfWork)); + } + + public async Task ReadAsync(SagaId id, Type type) + { + var _currSagaState = await SagaUnitOfWork + .SagaStateDBRepository + .ReadAsync(id, type); + return _currSagaState; + } + + public async Task WriteAsync(ISagaState state) + { + await SagaUnitOfWork + .SagaStateDBRepository + .WriteAsync(new EFCoreSagaStateData(state.Id.Id, state.Type.ToString(), state.State, JsonConvert.SerializeObject(state.Data))); + } + } +} diff --git a/src/EFCoreTestApp/SagaRepository/EFCoreSagaStateData.cs b/src/EFCoreTestApp/SagaRepository/EFCoreSagaStateData.cs new file mode 100644 index 0000000..7e12e53 --- /dev/null +++ b/src/EFCoreTestApp/SagaRepository/EFCoreSagaStateData.cs @@ -0,0 +1,63 @@ +using System; +using Chronicle; +using Newtonsoft.Json; +using System.ComponentModel.DataAnnotations.Schema; +using System.Reflection; + +namespace EFCoreTestApp.SagaRepository +{ + public class EFCoreSagaStateData : ISagaState + { + public string SagaId { get; set; } + [NotMapped] + public SagaId Id + { + get { + var currId = (SagaId)SagaId.ToString(); + return currId; + } + } + + public string SagaType { get; set; } + [NotMapped] + // Type ISagaState.Type => Assembly.GetEntryAssembly()?.GetType(SagaType); + Type ISagaState.Type + { + get + { + var currType = Assembly.GetEntryAssembly()?.GetType(SagaType); + return currType; + } + } + + public SagaStates State { get; private set; } + + // public object Data => JsonConvert.DeserializeObject(MessagePayload); + public object Data + { + get { + var currPayLoad = JsonConvert.DeserializeObject(MessagePayload); + return currPayLoad; + } + } + + public string MessagePayload { get; set; } + + public Type DataType { get; } + + public EFCoreSagaStateData(string sagaId, string sagaType, SagaStates state, string messagePayload) + { + SagaId = sagaId; + SagaType = sagaType; + State = state; + MessagePayload = messagePayload; + } + + public void Update(SagaStates state, object data = null) + { + State = state; + MessagePayload = JsonConvert.SerializeObject(data); + } + + } +} diff --git a/src/EFCoreTestApp/Sagas/CreatingOrderData.cs b/src/EFCoreTestApp/Sagas/CreatingOrderData.cs new file mode 100644 index 0000000..16e9e94 --- /dev/null +++ b/src/EFCoreTestApp/Sagas/CreatingOrderData.cs @@ -0,0 +1,11 @@ +using System; + +namespace EFCoreTestApp.Sagas +{ + public class CreatingOrderData + { + public Guid OrderId { get; set; } + public Guid CustomerId { get; set; } + public Guid ParcelId { get; set; } + } +} diff --git a/src/EFCoreTestApp/Sagas/OrderSaga.cs b/src/EFCoreTestApp/Sagas/OrderSaga.cs new file mode 100644 index 0000000..4a77c85 --- /dev/null +++ b/src/EFCoreTestApp/Sagas/OrderSaga.cs @@ -0,0 +1,72 @@ +using System; +using Chronicle; +using Microsoft.Extensions.Logging; +using System.Threading.Tasks; +using EFCoreTestApp.Commands; +using EFCoreTestApp.Events; +using Newtonsoft.Json.Linq; + +namespace EFCoreTestApp.Sagas +{ + public class OrderSaga : Saga, + ISagaStartAction, ISagaAction + { + private const string SagaHeader = "Saga"; + private readonly ILogger _logger; + + public OrderSaga(ILogger logger) + { + _logger = logger; + } + + /*public override void Initialize(SagaId id, SagaStates state, CreatingOrderData data) + { + base.Initialize(id, state); + Data = data; + }*/ + + + /* + This extra function is added to make the Saga & SagaLog repositroies to be ignorant of any data type. + and since Newtonsoft is being used to cast Data as JObject, therefore a method must be present that + matches the given parameter and Initialize the data. + See this line: public object Data => JsonConvert.DeserializeObject(MessagePayload); + in EFCoreSagaStateData file. + */ + + public void Initialize(SagaId id, SagaStates state, JObject data) + { + base.Initialize(id, state); + Data = data.ToObject(); + } + + public override SagaId ResolveId(object message, ISagaContext context) + => message switch + { + CreateOrder m => (SagaId)m.OrderId.ToString(), + OrderCreated m => (SagaId)m.OrderId.ToString(), + _ => base.ResolveId(message, context) + }; + + public async Task HandleAsync(CreateOrder message, ISagaContext context) + { + _logger.LogInformation($"[CreateOrder] Started a saga for order: '{message.OrderId}'."); + Data.ParcelId = message.ParcelId; + Data.OrderId = message.OrderId; + Data.CustomerId = message.CustomerId; + } + + public Task CompensateAsync(CreateOrder message, ISagaContext context) + => Task.CompletedTask; + + public async Task HandleAsync(OrderCreated message, ISagaContext context) + { + _logger.LogInformation($"[OrderCreated] Event for order: '{message.OrderId}'."); + Data.OrderId = message.OrderId; + await CompleteAsync(); + } + + public Task CompensateAsync(OrderCreated message, ISagaContext context) + => Task.CompletedTask; + } +} diff --git a/src/EFCoreTestApp/Startup.cs b/src/EFCoreTestApp/Startup.cs new file mode 100644 index 0000000..ed6e6dc --- /dev/null +++ b/src/EFCoreTestApp/Startup.cs @@ -0,0 +1,75 @@ +using System; +using MediatR; +using Chronicle; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Configuration; +using EFCoreTestApp.SagaRepository; +using EFCoreTestApp.Persistence; +using EFCoreTestApp.Extensions; + +namespace EFCoreTestApp +{ + public class Startup + { + private readonly IWebHostEnvironment _env; + public IConfiguration Configuration { get; } + + public Startup(IConfiguration configuration, IWebHostEnvironment env) + { + _env = env; + Configuration = configuration; + } + + public void ConfigureServices(IServiceCollection services) + { + services.AddControllers(); + services.AddDbContextPool(builder => + { + var connStr = this.Configuration.GetConnectionString("db"); + builder.UseSqlServer(connStr); + }); + + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + services.UserEfCoreForSaga(); + + services.AddMediatR(new[]{ + typeof(Startup).Assembly + }); + + services.AddMvc().AddNewtonsoftJson(); + + static void TestChronicleBuilder(IChronicleBuilder cb) + { + cb.UseSagaLog(); + cb.UseSagaStateRepository(); + } + services.AddChronicle(TestChronicleBuilder); + + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseRouting(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); + + } + } +} diff --git a/src/EFCoreTestApp/appsettings.Development.json b/src/EFCoreTestApp/appsettings.Development.json new file mode 100644 index 0000000..8983e0f --- /dev/null +++ b/src/EFCoreTestApp/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/src/EFCoreTestApp/appsettings.json b/src/EFCoreTestApp/appsettings.json new file mode 100644 index 0000000..8ccbb07 --- /dev/null +++ b/src/EFCoreTestApp/appsettings.json @@ -0,0 +1,13 @@ +{ + "ConnectionStrings": { + "db": "Data Source=(localdb)\\mssqllocaldb;Initial Catalog=EFCore_Saga_Demo;Trusted_Connection=True;MultipleActiveResultSets=true" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} \ No newline at end of file From 43524164bc617c994a238d4dbc0a7ca4a107b099 Mon Sep 17 00:00:00 2001 From: Umer Iftikhar Date: Thu, 17 Sep 2020 21:55:09 +0300 Subject: [PATCH 2/8] Added Implementation for EFCore for Chronicle --- .../Chronicle.Integrations.EFCore.csproj | 28 +++++++++++ .../SagaLogDataEntityTypeConfiguration.cs | 18 +++++++ .../SagaStateEntityTypeConfiguration.cs | 17 +++++++ .../Extensions.cs | 26 ++++++++++ .../Persistence/EFCoreSagaLog.cs | 28 +++++++++++ .../Persistence/EFCoreSagaLogData.cs | 30 ++++++++++++ .../Persistence/EFCoreSagaState.cs | 31 ++++++++++++ .../Persistence/EFCoreSagaStateData.cs | 48 +++++++++++++++++++ .../Repositories/ISagaLogRepository.cs | 16 +++++++ .../Repositories/ISagaStateDBRepository.cs | 15 ++++++ .../Repositories/SagaDbContext.cs | 27 +++++++++++ .../Repositories/SagaLogRepository.cs | 34 +++++++++++++ .../Repositories/SagaStateRepository.cs | 47 ++++++++++++++++++ src/Chronicle.sln | 14 ++++++ src/EFCoreTestApp/EFCoreTestApp.csproj | 3 +- .../Extensions/ChronicBuilder.cs | 17 ------- .../Handlers/OrderSagaHandler.cs | 5 ++ src/EFCoreTestApp/Sagas/OrderSaga.cs | 5 ++ src/EFCoreTestApp/Startup.cs | 44 ++++++++++++----- 19 files changed, 422 insertions(+), 31 deletions(-) create mode 100644 src/Chronicle.Integrations.EFCore/Chronicle.Integrations.EFCore.csproj create mode 100644 src/Chronicle.Integrations.EFCore/EntityConfigurations/SagaLogDataEntityTypeConfiguration.cs create mode 100644 src/Chronicle.Integrations.EFCore/EntityConfigurations/SagaStateEntityTypeConfiguration.cs create mode 100644 src/Chronicle.Integrations.EFCore/Extensions.cs create mode 100644 src/Chronicle.Integrations.EFCore/Persistence/EFCoreSagaLog.cs create mode 100644 src/Chronicle.Integrations.EFCore/Persistence/EFCoreSagaLogData.cs create mode 100644 src/Chronicle.Integrations.EFCore/Persistence/EFCoreSagaState.cs create mode 100644 src/Chronicle.Integrations.EFCore/Persistence/EFCoreSagaStateData.cs create mode 100644 src/Chronicle.Integrations.EFCore/Repositories/ISagaLogRepository.cs create mode 100644 src/Chronicle.Integrations.EFCore/Repositories/ISagaStateDBRepository.cs create mode 100644 src/Chronicle.Integrations.EFCore/Repositories/SagaDbContext.cs create mode 100644 src/Chronicle.Integrations.EFCore/Repositories/SagaLogRepository.cs create mode 100644 src/Chronicle.Integrations.EFCore/Repositories/SagaStateRepository.cs delete mode 100644 src/EFCoreTestApp/Extensions/ChronicBuilder.cs diff --git a/src/Chronicle.Integrations.EFCore/Chronicle.Integrations.EFCore.csproj b/src/Chronicle.Integrations.EFCore/Chronicle.Integrations.EFCore.csproj new file mode 100644 index 0000000..dcf17b1 --- /dev/null +++ b/src/Chronicle.Integrations.EFCore/Chronicle.Integrations.EFCore.csproj @@ -0,0 +1,28 @@ + + + + netcoreapp3.1 + Chronicle.Integrations.EFCore + Chronicle.Integrations.EFCore + Chronicle_.Integrations.EFCore + chronicle;saga;efcore + Implementation of saga pattern for .NET Core + Umer Iftikhar + 3.1.1 + 3.1.1 + 3.1.1 + 3.1.1 + + + + + + + + + + + + + + diff --git a/src/Chronicle.Integrations.EFCore/EntityConfigurations/SagaLogDataEntityTypeConfiguration.cs b/src/Chronicle.Integrations.EFCore/EntityConfigurations/SagaLogDataEntityTypeConfiguration.cs new file mode 100644 index 0000000..9d033cf --- /dev/null +++ b/src/Chronicle.Integrations.EFCore/EntityConfigurations/SagaLogDataEntityTypeConfiguration.cs @@ -0,0 +1,18 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Chronicle.Integrations.EFCore.Persistence; + +namespace Chronicle.Integrations.EFCore.EntityConfigurations +{ + public class SagaLogDataEntityTypeConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("SagaLog", "dbo"); + builder.HasKey(c => c.logId); + builder.Property(c => c.logId).ValueGeneratedOnAdd(); + builder.Ignore(c => c.Id); + builder.Ignore(c => c.Message); + } + } +} diff --git a/src/Chronicle.Integrations.EFCore/EntityConfigurations/SagaStateEntityTypeConfiguration.cs b/src/Chronicle.Integrations.EFCore/EntityConfigurations/SagaStateEntityTypeConfiguration.cs new file mode 100644 index 0000000..a6f2982 --- /dev/null +++ b/src/Chronicle.Integrations.EFCore/EntityConfigurations/SagaStateEntityTypeConfiguration.cs @@ -0,0 +1,17 @@ +using Chronicle.Integrations.EFCore.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Chronicle.Integrations.EFCore.EntityConfigurations +{ + internal class SagaStateEntityTypeConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("SagaState", "dbo"); + builder.HasKey(r => r.SagaId); + builder.Ignore(c => c.Id); + builder.Ignore(c => c.Data); + } + } +} diff --git a/src/Chronicle.Integrations.EFCore/Extensions.cs b/src/Chronicle.Integrations.EFCore/Extensions.cs new file mode 100644 index 0000000..1ac7676 --- /dev/null +++ b/src/Chronicle.Integrations.EFCore/Extensions.cs @@ -0,0 +1,26 @@ +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Configuration; +using Chronicle.Integrations.EFCore.Repositories; +using Chronicle.Integrations.EFCore.Persistence; + +namespace Chronicle.Integrations.EFCore +{ + public static class Extensions + { + public static IChronicleBuilder UseEFCorePersistence(this IChronicleBuilder builder, string dbConnectionString) + { + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddDbContext(builder => + { + builder.UseSqlServer(dbConnectionString); + }, ServiceLifetime.Transient); + builder.UseSagaLog(); + builder.UseSagaStateRepository(); + + return builder; + } + } +} diff --git a/src/Chronicle.Integrations.EFCore/Persistence/EFCoreSagaLog.cs b/src/Chronicle.Integrations.EFCore/Persistence/EFCoreSagaLog.cs new file mode 100644 index 0000000..8a4fe2c --- /dev/null +++ b/src/Chronicle.Integrations.EFCore/Persistence/EFCoreSagaLog.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Chronicle.Integrations.EFCore.Repositories; + +namespace Chronicle.Integrations.EFCore.Persistence +{ + internal class EFCoreSagaLog : ISagaLog + { + private readonly ISagaLogRepository SagaLogRepository; + + public EFCoreSagaLog(ISagaLogRepository _sagaLogRepository) + { + SagaLogRepository = _sagaLogRepository ?? throw new ArgumentNullException(nameof(_sagaLogRepository)); + } + + public async Task> ReadAsync(SagaId id, Type type) + => await SagaLogRepository.ReadAsync(id, type); + + public async Task WriteAsync(ISagaLogData message) + { + await SagaLogRepository + .WriteAsync(new EFCoreSagaLogData(message.Id.Id, message.Type.ToString(), message.CreatedAt, JsonConvert.SerializeObject(message.Message))); + } + } + +} diff --git a/src/Chronicle.Integrations.EFCore/Persistence/EFCoreSagaLogData.cs b/src/Chronicle.Integrations.EFCore/Persistence/EFCoreSagaLogData.cs new file mode 100644 index 0000000..e8acd31 --- /dev/null +++ b/src/Chronicle.Integrations.EFCore/Persistence/EFCoreSagaLogData.cs @@ -0,0 +1,30 @@ +using System; +using System.ComponentModel.DataAnnotations.Schema; +using System.Reflection; +using Newtonsoft.Json; + +namespace Chronicle.Integrations.EFCore.Persistence +{ + public class EFCoreSagaLogData : ISagaLogData + { + public int logId { get; set; } + public string SagaId { get; set; } + [NotMapped] + public SagaId Id => SagaId; + public string SagaType { get; set; } + public long CreatedAt { get; set; } + [NotMapped] + public object Message => JsonConvert.DeserializeObject(MessagePayload); + public string MessagePayload { get; set; } + + Type ISagaLogData.Type => Assembly.GetEntryAssembly()?.GetType(SagaType); + + public EFCoreSagaLogData(string sagaId, string sagaType, long createdAt, string messagePayload) + { + SagaId = sagaId; + SagaType = sagaType; + CreatedAt = createdAt; + MessagePayload = messagePayload; + } + } +} diff --git a/src/Chronicle.Integrations.EFCore/Persistence/EFCoreSagaState.cs b/src/Chronicle.Integrations.EFCore/Persistence/EFCoreSagaState.cs new file mode 100644 index 0000000..1ca8324 --- /dev/null +++ b/src/Chronicle.Integrations.EFCore/Persistence/EFCoreSagaState.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Chronicle.Integrations.EFCore.Repositories; + +namespace Chronicle.Integrations.EFCore.Persistence +{ + internal class EFCoreSagaState : ISagaStateRepository + { + public ISagaStateDBRepository SagaStateDBRepository { get; } + + public EFCoreSagaState(ISagaStateDBRepository _sagaStateDBRepository) + { + SagaStateDBRepository = _sagaStateDBRepository ?? throw new ArgumentNullException(nameof(_sagaStateDBRepository)); + } + + public async Task ReadAsync(SagaId id, Type type) + { + var _currSagaState = await SagaStateDBRepository.ReadAsync(id, type); + return _currSagaState; + } + + public async Task WriteAsync(ISagaState state) + { + await SagaStateDBRepository + .WriteAsync(new EFCoreSagaStateData(state.Id.Id, state.Type.ToString(), state.State, JsonConvert.SerializeObject(state.Data))); + } + } +} diff --git a/src/Chronicle.Integrations.EFCore/Persistence/EFCoreSagaStateData.cs b/src/Chronicle.Integrations.EFCore/Persistence/EFCoreSagaStateData.cs new file mode 100644 index 0000000..e128a37 --- /dev/null +++ b/src/Chronicle.Integrations.EFCore/Persistence/EFCoreSagaStateData.cs @@ -0,0 +1,48 @@ +using System; +using Newtonsoft.Json; +using System.ComponentModel.DataAnnotations.Schema; +using System.Reflection; + +namespace Chronicle.Integrations.EFCore.Persistence +{ + internal class EFCoreSagaStateData : ISagaState + { + public string SagaId { get; set; } + [NotMapped] + public SagaId Id + { + get + { + var currId = (SagaId)SagaId.ToString(); + return currId; + } + } + + public string SagaType { get; set; } + [NotMapped] + Type ISagaState.Type => Assembly.GetEntryAssembly()?.GetType(SagaType); + + public SagaStates State { get; private set; } + + public object Data => JsonConvert.DeserializeObject(MessagePayload); + + public string MessagePayload { get; set; } + + public Type DataType { get; } + + public EFCoreSagaStateData(string sagaId, string sagaType, SagaStates state, string messagePayload) + { + SagaId = sagaId; + SagaType = sagaType; + State = state; + MessagePayload = messagePayload; + } + + public void Update(SagaStates state, object data = null) + { + State = state; + MessagePayload = JsonConvert.SerializeObject(data); + } + + } +} diff --git a/src/Chronicle.Integrations.EFCore/Repositories/ISagaLogRepository.cs b/src/Chronicle.Integrations.EFCore/Repositories/ISagaLogRepository.cs new file mode 100644 index 0000000..1242078 --- /dev/null +++ b/src/Chronicle.Integrations.EFCore/Repositories/ISagaLogRepository.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Chronicle.Integrations.EFCore.Persistence; + +namespace Chronicle.Integrations.EFCore.Repositories +{ + internal interface ISagaLogRepository + { + Task> ReadAsync(SagaId id, Type type); + + Task WriteAsync(EFCoreSagaLogData message); + } +} diff --git a/src/Chronicle.Integrations.EFCore/Repositories/ISagaStateDBRepository.cs b/src/Chronicle.Integrations.EFCore/Repositories/ISagaStateDBRepository.cs new file mode 100644 index 0000000..3142863 --- /dev/null +++ b/src/Chronicle.Integrations.EFCore/Repositories/ISagaStateDBRepository.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Chronicle.Integrations.EFCore.Persistence; + +namespace Chronicle.Integrations.EFCore.Repositories +{ + internal interface ISagaStateDBRepository + { + Task ReadAsync(SagaId id, Type type); + Task WriteAsync(EFCoreSagaStateData message); + } +} diff --git a/src/Chronicle.Integrations.EFCore/Repositories/SagaDbContext.cs b/src/Chronicle.Integrations.EFCore/Repositories/SagaDbContext.cs new file mode 100644 index 0000000..ed3f006 --- /dev/null +++ b/src/Chronicle.Integrations.EFCore/Repositories/SagaDbContext.cs @@ -0,0 +1,27 @@ +using Microsoft.EntityFrameworkCore; +using Chronicle.Integrations.EFCore.EntityConfigurations; +using Chronicle.Integrations.EFCore.Persistence; + + +namespace Chronicle.Integrations.EFCore.Repositories +{ + internal class SagaDbContext : DbContext + { + public SagaDbContext(DbContextOptions options) + : base(options) + { + // This can be removed and tables can be created manually as part of an Initialization Script + Database.EnsureCreated(); + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.ApplyConfiguration(new SagaLogDataEntityTypeConfiguration()); + modelBuilder.ApplyConfiguration(new SagaStateEntityTypeConfiguration()); + } + + public DbSet SagaLog { get; set; } + + public DbSet SagaState { get; set; } + } +} diff --git a/src/Chronicle.Integrations.EFCore/Repositories/SagaLogRepository.cs b/src/Chronicle.Integrations.EFCore/Repositories/SagaLogRepository.cs new file mode 100644 index 0000000..d0c01f8 --- /dev/null +++ b/src/Chronicle.Integrations.EFCore/Repositories/SagaLogRepository.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Chronicle.Integrations.EFCore.Persistence; + +namespace Chronicle.Integrations.EFCore.Repositories +{ + internal class SagaLogRepository : ISagaLogRepository + { + private readonly SagaDbContext _dbContext; + + public SagaLogRepository(SagaDbContext dbContext) + { + _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext)); + } + + public async Task> ReadAsync(SagaId id, Type type) + { + return await _dbContext.SagaLog + .Where(sld => sld.SagaId == id.Id && sld.SagaType == type.FullName) + .ToArrayAsync(); + } + + public async Task WriteAsync(EFCoreSagaLogData message) + { + if (null == message) + throw new ArgumentNullException(nameof(message)); + await _dbContext.SagaLog.AddAsync(message); + await _dbContext.SaveChangesAsync(); + } + } +} diff --git a/src/Chronicle.Integrations.EFCore/Repositories/SagaStateRepository.cs b/src/Chronicle.Integrations.EFCore/Repositories/SagaStateRepository.cs new file mode 100644 index 0000000..48184b5 --- /dev/null +++ b/src/Chronicle.Integrations.EFCore/Repositories/SagaStateRepository.cs @@ -0,0 +1,47 @@ +using System; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Newtonsoft.Json; +using Chronicle.Integrations.EFCore.Persistence; + +namespace Chronicle.Integrations.EFCore.Repositories +{ + internal class SagaStateRepository : ISagaStateDBRepository + { + private readonly SagaDbContext _dbContext; + + public SagaStateRepository(SagaDbContext dbContext) + { + _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext)); + } + + + public async Task GetByIdAsync(SagaId sagaId) + { + return await _dbContext.SagaState + .FirstOrDefaultAsync(sld => sld.SagaId == sagaId.Id); + } + + public async Task ReadAsync(SagaId id, Type type) + { + return await _dbContext.SagaState + .FirstOrDefaultAsync(sld => sld.SagaId == id.Id && sld.SagaType == type.FullName); + } + + public async Task WriteAsync(EFCoreSagaStateData sagaState) + { + var entity = await _dbContext + .SagaState + .FirstOrDefaultAsync(sld => sld.SagaId == sagaState.Id.Id && sld.SagaType == sagaState.SagaType); + if (entity != null) + { + _dbContext.SagaState.Remove(entity); + } + + await _dbContext.SagaState.AddAsync( + new EFCoreSagaStateData(sagaState.Id.Id, sagaState.SagaType, sagaState.State, JsonConvert.SerializeObject(sagaState.Data)) + ); + await _dbContext.SaveChangesAsync(); + } + } +} diff --git a/src/Chronicle.sln b/src/Chronicle.sln index c428715..883ebb1 100644 --- a/src/Chronicle.sln +++ b/src/Chronicle.sln @@ -13,6 +13,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Chronicle.Integrations.Mong EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EFCoreTestApp", "EFCoreTestApp\EFCoreTestApp.csproj", "{E47A600F-6802-4A9E-91E3-E8D213EC9195}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Chronicle.Integrations.EFCore", "Chronicle.Integrations.EFCore\Chronicle.Integrations.EFCore.csproj", "{F0B818F0-BDC2-48C6-9E53-3F4526558701}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -83,6 +85,18 @@ Global {E47A600F-6802-4A9E-91E3-E8D213EC9195}.Release|x64.Build.0 = Release|Any CPU {E47A600F-6802-4A9E-91E3-E8D213EC9195}.Release|x86.ActiveCfg = Release|Any CPU {E47A600F-6802-4A9E-91E3-E8D213EC9195}.Release|x86.Build.0 = Release|Any CPU + {F0B818F0-BDC2-48C6-9E53-3F4526558701}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F0B818F0-BDC2-48C6-9E53-3F4526558701}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F0B818F0-BDC2-48C6-9E53-3F4526558701}.Debug|x64.ActiveCfg = Debug|Any CPU + {F0B818F0-BDC2-48C6-9E53-3F4526558701}.Debug|x64.Build.0 = Debug|Any CPU + {F0B818F0-BDC2-48C6-9E53-3F4526558701}.Debug|x86.ActiveCfg = Debug|Any CPU + {F0B818F0-BDC2-48C6-9E53-3F4526558701}.Debug|x86.Build.0 = Debug|Any CPU + {F0B818F0-BDC2-48C6-9E53-3F4526558701}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F0B818F0-BDC2-48C6-9E53-3F4526558701}.Release|Any CPU.Build.0 = Release|Any CPU + {F0B818F0-BDC2-48C6-9E53-3F4526558701}.Release|x64.ActiveCfg = Release|Any CPU + {F0B818F0-BDC2-48C6-9E53-3F4526558701}.Release|x64.Build.0 = Release|Any CPU + {F0B818F0-BDC2-48C6-9E53-3F4526558701}.Release|x86.ActiveCfg = Release|Any CPU + {F0B818F0-BDC2-48C6-9E53-3F4526558701}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/EFCoreTestApp/EFCoreTestApp.csproj b/src/EFCoreTestApp/EFCoreTestApp.csproj index 1c57d2b..8f6f31d 100644 --- a/src/EFCoreTestApp/EFCoreTestApp.csproj +++ b/src/EFCoreTestApp/EFCoreTestApp.csproj @@ -14,6 +14,5 @@ - - + diff --git a/src/EFCoreTestApp/Extensions/ChronicBuilder.cs b/src/EFCoreTestApp/Extensions/ChronicBuilder.cs deleted file mode 100644 index 1d5de5a..0000000 --- a/src/EFCoreTestApp/Extensions/ChronicBuilder.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; -using Chronicle; -using Microsoft.Extensions.DependencyInjection; -using EFCoreTestApp.SagaRepository; - -namespace EFCoreTestApp.Extensions -{ - public static class ChronicBuilder - { - public static IServiceCollection UserEfCoreForSaga(this IServiceCollection serviceCollection) - { - serviceCollection.AddTransient(typeof(ISagaStateRepository), typeof(EFCoreSagaState)); - serviceCollection.AddTransient(typeof(ISagaLog), typeof(EFCoreSagaLog)); - return serviceCollection; - } - } -} diff --git a/src/EFCoreTestApp/Handlers/OrderSagaHandler.cs b/src/EFCoreTestApp/Handlers/OrderSagaHandler.cs index d28e956..e6f5cfe 100644 --- a/src/EFCoreTestApp/Handlers/OrderSagaHandler.cs +++ b/src/EFCoreTestApp/Handlers/OrderSagaHandler.cs @@ -13,6 +13,11 @@ namespace EFCoreTestApp.Handlers { public class OrderSagaHandler: IRequestHandler, INotificationHandler { + /* + NOTE: Remove ISagaUnitOfWork if the Chronicles Internal EFCore/SQL Server implementation is being used. + If implementing with a custom Solution enable it. + */ + private readonly ISagaCoordinator _coordinator; private ISagaUnitOfWork SagaUnitOfWork { get; } diff --git a/src/EFCoreTestApp/Sagas/OrderSaga.cs b/src/EFCoreTestApp/Sagas/OrderSaga.cs index 4a77c85..014a0dd 100644 --- a/src/EFCoreTestApp/Sagas/OrderSaga.cs +++ b/src/EFCoreTestApp/Sagas/OrderSaga.cs @@ -32,6 +32,11 @@ This extra function is added to make the Saga & SagaLog repositroies to be igno matches the given parameter and Initialize the data. See this line: public object Data => JsonConvert.DeserializeObject(MessagePayload); in EFCoreSagaStateData file. + + See this line in Chronicle repositry: + file name: src/Chronicle/Utils/SagaExtensions.cs + line number: 16 + saga.GetType().GetMethod(method, args.Select(arg => arg.GetType()).ToArray())?.Invoke(saga, args); */ public void Initialize(SagaId id, SagaStates state, JObject data) diff --git a/src/EFCoreTestApp/Startup.cs b/src/EFCoreTestApp/Startup.cs index ed6e6dc..ccbd97f 100644 --- a/src/EFCoreTestApp/Startup.cs +++ b/src/EFCoreTestApp/Startup.cs @@ -9,7 +9,8 @@ using Microsoft.Extensions.Configuration; using EFCoreTestApp.SagaRepository; using EFCoreTestApp.Persistence; -using EFCoreTestApp.Extensions; +// Inorder to use Chronicles internal EFCore Implementation +// using Chronicle.Integrations.EFCore; namespace EFCoreTestApp { @@ -17,41 +18,60 @@ public class Startup { private readonly IWebHostEnvironment _env; public IConfiguration Configuration { get; } + private static string _connectionString; public Startup(IConfiguration configuration, IWebHostEnvironment env) { _env = env; Configuration = configuration; + _connectionString = configuration.GetConnectionString("db"); } public void ConfigureServices(IServiceCollection services) { + services.AddControllers(); - services.AddDbContextPool(builder => + /* + NOTE: Remove AddDbContext if the Chronicles Internal EFCore/SQL Server implementation is being used. + */ + services.AddDbContext(builder => { var connStr = this.Configuration.GetConnectionString("db"); builder.UseSqlServer(connStr); }); - + + /* + NOTE: Enable only if Chroniel Internal Implementation needs to be used. + */ + /*static void TestChronicleBuilder(IChronicleBuilder cb) + { + cb.UseEFCorePersistence(_connectionString); + }*/ + + /* + NOTE: Remove this if the Chronicles Internal EFCore/SQL Server implementation is being used. + */ + static void TestChronicleBuilder(IChronicleBuilder cb) + { + cb.UseSagaLog(); + cb.UseSagaStateRepository(); + } + + services.AddChronicle(TestChronicleBuilder); + + /* + NOTE: Remove this if the Chronicles Internal EFCore/SQL Server implementation is being used. + */ services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.UserEfCoreForSaga(); - services.AddMediatR(new[]{ typeof(Startup).Assembly }); services.AddMvc().AddNewtonsoftJson(); - static void TestChronicleBuilder(IChronicleBuilder cb) - { - cb.UseSagaLog(); - cb.UseSagaStateRepository(); - } - services.AddChronicle(TestChronicleBuilder); - } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) From 951ef264a63f3e1a2aade4c336bbb3e48c406135 Mon Sep 17 00:00:00 2001 From: Umer Iftikhar Date: Fri, 18 Sep 2020 12:33:14 +0300 Subject: [PATCH 3/8] Updated the Readme for Custom Saga Implementation via EFCore --- README.md | 36 ++++++++++++++++++++++++++++ src/EFCoreTestApp/Sagas/OrderSaga.cs | 7 ------ 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 8bf7360..ca55978 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,42 @@ The result looks as follows: ![Result](https://user-images.githubusercontent.com/7096476/53180548-0c885900-35f6-11e9-864b-6b6d13641f2a.png) +# Sample Application with EFCore +DB connection string is in **appsettings.json** file under property **ConnectionStrings->db**. +Persistence has been added under folder: EFCoreTestApp-> Persistence +Custom Implementation for `ISagaLog` & `ISagaStateRepository` has been added under folder: EFCoreTestApp-> SagaRepository +Custom Saga Persistence has been applied in Starup file using the following code: +``` +static void TestChronicleBuilder(IChronicleBuilder cb) +{ + cb.UseSagaLog(); + cb.UseSagaStateRepository(); +} +services.AddChronicle(TestChronicleBuilder); +``` +Example Saga has been created under folder: **EFCoreTestApp-> Sagas** +filename: **OrderSaga.cs** + +**Initialize unique Saga:** +POST http://localhost:{PORT_NUMBER}/api/order/ +```json +{ + "orderId": "1045dbed-1520-47e3-bdfd-b6de59abcfb2", + "customerId": "1045dbed-1520-47e3-bdfd-b6de59abcfb2", + "parcelId": "1045dbed-1520-47e3-bdfd-b6de59abcfb2" +} +``` +**Update the existing Saga:** +POST http://localhost:{PORT_NUMBER}/api/order/created +```json +{ + "orderId": "1045dbed-1520-47e3-bdfd-b6de59abcfb2" +} +``` +Similary the Saga Data and logs can be retrieved using the following endpoints: +GET http://localhost:{PORT_NUMBER}/api/saga/{SAGA_ID} +GET http://localhost:{PORT_NUMBER}/api/saga/{SAGA_ID}/logs + # Documentation If you're looking for documentation, you can find it [here](https://chronicle.readthedocs.io/en/latest/). diff --git a/src/EFCoreTestApp/Sagas/OrderSaga.cs b/src/EFCoreTestApp/Sagas/OrderSaga.cs index 014a0dd..32142fa 100644 --- a/src/EFCoreTestApp/Sagas/OrderSaga.cs +++ b/src/EFCoreTestApp/Sagas/OrderSaga.cs @@ -19,13 +19,6 @@ public OrderSaga(ILogger logger) _logger = logger; } - /*public override void Initialize(SagaId id, SagaStates state, CreatingOrderData data) - { - base.Initialize(id, state); - Data = data; - }*/ - - /* This extra function is added to make the Saga & SagaLog repositroies to be ignorant of any data type. and since Newtonsoft is being used to cast Data as JObject, therefore a method must be present that From 875ef5cfeb275c967bed944980f0e6050f3a02f6 Mon Sep 17 00:00:00 2001 From: Umer Iftikhar Date: Fri, 18 Sep 2020 20:50:39 +0300 Subject: [PATCH 4/8] JSON Deserializer generic method used while converting the saga Data --- .../Persistence/EFCoreSagaStateData.cs | 15 ++++++++++++++- .../SagaRepository/EFCoreSagaStateData.cs | 9 ++++++--- src/EFCoreTestApp/Sagas/OrderSaga.cs | 4 ++-- 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/src/Chronicle.Integrations.EFCore/Persistence/EFCoreSagaStateData.cs b/src/Chronicle.Integrations.EFCore/Persistence/EFCoreSagaStateData.cs index e128a37..5d1a6d7 100644 --- a/src/Chronicle.Integrations.EFCore/Persistence/EFCoreSagaStateData.cs +++ b/src/Chronicle.Integrations.EFCore/Persistence/EFCoreSagaStateData.cs @@ -2,6 +2,7 @@ using Newtonsoft.Json; using System.ComponentModel.DataAnnotations.Schema; using System.Reflection; +using System.Linq; namespace Chronicle.Integrations.EFCore.Persistence { @@ -24,7 +25,19 @@ public SagaId Id public SagaStates State { get; private set; } - public object Data => JsonConvert.DeserializeObject(MessagePayload); + // public object Data => JsonConvert.DeserializeObject(MessagePayload); + public object Data + { + get + { + var currType = Assembly.GetEntryAssembly()?.GetType(SagaType); + var sagaInterface = currType.GetInterfaces() + .FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(ISaga<>)); + var sagaGenericDataType = sagaInterface.GetGenericArguments().FirstOrDefault(); + var currPayLoad = JsonConvert.DeserializeObject(MessagePayload, sagaGenericDataType); + return currPayLoad; + } + } public string MessagePayload { get; set; } diff --git a/src/EFCoreTestApp/SagaRepository/EFCoreSagaStateData.cs b/src/EFCoreTestApp/SagaRepository/EFCoreSagaStateData.cs index 7e12e53..7b5a226 100644 --- a/src/EFCoreTestApp/SagaRepository/EFCoreSagaStateData.cs +++ b/src/EFCoreTestApp/SagaRepository/EFCoreSagaStateData.cs @@ -3,6 +3,7 @@ using Newtonsoft.Json; using System.ComponentModel.DataAnnotations.Schema; using System.Reflection; +using System.Linq; namespace EFCoreTestApp.SagaRepository { @@ -20,7 +21,6 @@ public SagaId Id public string SagaType { get; set; } [NotMapped] - // Type ISagaState.Type => Assembly.GetEntryAssembly()?.GetType(SagaType); Type ISagaState.Type { get @@ -32,11 +32,14 @@ Type ISagaState.Type public SagaStates State { get; private set; } - // public object Data => JsonConvert.DeserializeObject(MessagePayload); public object Data { get { - var currPayLoad = JsonConvert.DeserializeObject(MessagePayload); + var currType = Assembly.GetEntryAssembly()?.GetType(SagaType); + var sagaInterface = currType.GetInterfaces() + .FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(ISaga<>)); + var sagaGenericDataType = sagaInterface.GetGenericArguments().FirstOrDefault(); + var currPayLoad = JsonConvert.DeserializeObject(MessagePayload, sagaGenericDataType); return currPayLoad; } } diff --git a/src/EFCoreTestApp/Sagas/OrderSaga.cs b/src/EFCoreTestApp/Sagas/OrderSaga.cs index 32142fa..b462929 100644 --- a/src/EFCoreTestApp/Sagas/OrderSaga.cs +++ b/src/EFCoreTestApp/Sagas/OrderSaga.cs @@ -32,11 +32,11 @@ in EFCoreSagaStateData file. saga.GetType().GetMethod(method, args.Select(arg => arg.GetType()).ToArray())?.Invoke(saga, args); */ - public void Initialize(SagaId id, SagaStates state, JObject data) + /*public void Initialize(SagaId id, SagaStates state, JObject data) { base.Initialize(id, state); Data = data.ToObject(); - } + }*/ public override SagaId ResolveId(object message, ISagaContext context) => message switch From 370d7bf27501ce64500aa8fe729e79af4a631e9c Mon Sep 17 00:00:00 2001 From: Umer Iftikhar Date: Fri, 18 Sep 2020 21:58:20 +0300 Subject: [PATCH 5/8] fixed Data return type for Saga without TData --- .../Persistence/EFCoreSagaStateData.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Chronicle.Integrations.EFCore/Persistence/EFCoreSagaStateData.cs b/src/Chronicle.Integrations.EFCore/Persistence/EFCoreSagaStateData.cs index 5d1a6d7..0a41079 100644 --- a/src/Chronicle.Integrations.EFCore/Persistence/EFCoreSagaStateData.cs +++ b/src/Chronicle.Integrations.EFCore/Persistence/EFCoreSagaStateData.cs @@ -33,7 +33,7 @@ public object Data var currType = Assembly.GetEntryAssembly()?.GetType(SagaType); var sagaInterface = currType.GetInterfaces() .FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(ISaga<>)); - var sagaGenericDataType = sagaInterface.GetGenericArguments().FirstOrDefault(); + var sagaGenericDataType = sagaInterface?.GetGenericArguments()?.FirstOrDefault(); var currPayLoad = JsonConvert.DeserializeObject(MessagePayload, sagaGenericDataType); return currPayLoad; } From 7a067e5ecfea4d5d8b97139ad6e25d5af3d052c9 Mon Sep 17 00:00:00 2001 From: Umer Iftikhar Date: Sat, 19 Sep 2020 09:57:45 +0300 Subject: [PATCH 6/8] renamed the efcore test project --- src/Chronicle.sln | 4 ++-- .../Commands/CreateOrder.cs | 0 .../Controllers/OrderController.cs | 0 .../Controllers/SagaController.cs | 0 .../DTO/CreateOrderDTO.cs | 0 .../DTO/OrderCreatedDTO.cs | 0 .../EFCoreTestAppWithCutomSaga.csproj} | 0 .../SagaLogDataEntityTypeConfiguration.cs | 0 .../EntityConfigurations/SagaStateEntityTypeConfiguration.cs | 0 .../Events/OrderCreated.cs | 0 .../Handlers/OrderSagaHandler.cs | 0 .../Persistence/ISagaLogRepository.cs | 0 .../Persistence/ISagaStateDBRepository.cs | 0 .../Persistence/ISagaUnitOfWork.cs | 0 .../Persistence/SagaDbContext.cs | 0 .../Persistence/SagaLogRepository.cs | 0 .../Persistence/SagaStateRepository.cs | 0 .../Persistence/SagaUnitOfWork.cs | 0 src/{EFCoreTestApp => EFCoreTestAppWithCutomSaga}/Program.cs | 0 .../SagaRepository/EFCoreSagaLog.cs | 0 .../SagaRepository/EFCoreSagaLogData.cs | 0 .../SagaRepository/EFCoreSagaState.cs | 0 .../SagaRepository/EFCoreSagaStateData.cs | 0 .../Sagas/CreatingOrderData.cs | 0 .../Sagas/OrderSaga.cs | 0 src/{EFCoreTestApp => EFCoreTestAppWithCutomSaga}/Startup.cs | 0 .../appsettings.Development.json | 0 .../appsettings.json | 0 28 files changed, 2 insertions(+), 2 deletions(-) rename src/{EFCoreTestApp => EFCoreTestAppWithCutomSaga}/Commands/CreateOrder.cs (100%) rename src/{EFCoreTestApp => EFCoreTestAppWithCutomSaga}/Controllers/OrderController.cs (100%) rename src/{EFCoreTestApp => EFCoreTestAppWithCutomSaga}/Controllers/SagaController.cs (100%) rename src/{EFCoreTestApp => EFCoreTestAppWithCutomSaga}/DTO/CreateOrderDTO.cs (100%) rename src/{EFCoreTestApp => EFCoreTestAppWithCutomSaga}/DTO/OrderCreatedDTO.cs (100%) rename src/{EFCoreTestApp/EFCoreTestApp.csproj => EFCoreTestAppWithCutomSaga/EFCoreTestAppWithCutomSaga.csproj} (100%) rename src/{EFCoreTestApp => EFCoreTestAppWithCutomSaga}/EntityConfigurations/SagaLogDataEntityTypeConfiguration.cs (100%) rename src/{EFCoreTestApp => EFCoreTestAppWithCutomSaga}/EntityConfigurations/SagaStateEntityTypeConfiguration.cs (100%) rename src/{EFCoreTestApp => EFCoreTestAppWithCutomSaga}/Events/OrderCreated.cs (100%) rename src/{EFCoreTestApp => EFCoreTestAppWithCutomSaga}/Handlers/OrderSagaHandler.cs (100%) rename src/{EFCoreTestApp => EFCoreTestAppWithCutomSaga}/Persistence/ISagaLogRepository.cs (100%) rename src/{EFCoreTestApp => EFCoreTestAppWithCutomSaga}/Persistence/ISagaStateDBRepository.cs (100%) rename src/{EFCoreTestApp => EFCoreTestAppWithCutomSaga}/Persistence/ISagaUnitOfWork.cs (100%) rename src/{EFCoreTestApp => EFCoreTestAppWithCutomSaga}/Persistence/SagaDbContext.cs (100%) rename src/{EFCoreTestApp => EFCoreTestAppWithCutomSaga}/Persistence/SagaLogRepository.cs (100%) rename src/{EFCoreTestApp => EFCoreTestAppWithCutomSaga}/Persistence/SagaStateRepository.cs (100%) rename src/{EFCoreTestApp => EFCoreTestAppWithCutomSaga}/Persistence/SagaUnitOfWork.cs (100%) rename src/{EFCoreTestApp => EFCoreTestAppWithCutomSaga}/Program.cs (100%) rename src/{EFCoreTestApp => EFCoreTestAppWithCutomSaga}/SagaRepository/EFCoreSagaLog.cs (100%) rename src/{EFCoreTestApp => EFCoreTestAppWithCutomSaga}/SagaRepository/EFCoreSagaLogData.cs (100%) rename src/{EFCoreTestApp => EFCoreTestAppWithCutomSaga}/SagaRepository/EFCoreSagaState.cs (100%) rename src/{EFCoreTestApp => EFCoreTestAppWithCutomSaga}/SagaRepository/EFCoreSagaStateData.cs (100%) rename src/{EFCoreTestApp => EFCoreTestAppWithCutomSaga}/Sagas/CreatingOrderData.cs (100%) rename src/{EFCoreTestApp => EFCoreTestAppWithCutomSaga}/Sagas/OrderSaga.cs (100%) rename src/{EFCoreTestApp => EFCoreTestAppWithCutomSaga}/Startup.cs (100%) rename src/{EFCoreTestApp => EFCoreTestAppWithCutomSaga}/appsettings.Development.json (100%) rename src/{EFCoreTestApp => EFCoreTestAppWithCutomSaga}/appsettings.json (100%) diff --git a/src/Chronicle.sln b/src/Chronicle.sln index 883ebb1..e7c01f7 100644 --- a/src/Chronicle.sln +++ b/src/Chronicle.sln @@ -11,9 +11,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Chronicle.Tests", "Chronicl EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Chronicle.Integrations.MongoDB", "Chronicle.Integrations.MongoDB\src\Chronicle.Integrations.MongoDB.csproj", "{DFB344D1-3121-43F9-9E66-20BC6B0DD9CC}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EFCoreTestApp", "EFCoreTestApp\EFCoreTestApp.csproj", "{E47A600F-6802-4A9E-91E3-E8D213EC9195}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EFCoreTestAppWithCutomSaga", "EFCoreTestAppWithCutomSaga\EFCoreTestAppWithCutomSaga.csproj", "{E47A600F-6802-4A9E-91E3-E8D213EC9195}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Chronicle.Integrations.EFCore", "Chronicle.Integrations.EFCore\Chronicle.Integrations.EFCore.csproj", "{F0B818F0-BDC2-48C6-9E53-3F4526558701}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Chronicle.Integrations.EFCore", "Chronicle.Integrations.EFCore\Chronicle.Integrations.EFCore.csproj", "{F0B818F0-BDC2-48C6-9E53-3F4526558701}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/src/EFCoreTestApp/Commands/CreateOrder.cs b/src/EFCoreTestAppWithCutomSaga/Commands/CreateOrder.cs similarity index 100% rename from src/EFCoreTestApp/Commands/CreateOrder.cs rename to src/EFCoreTestAppWithCutomSaga/Commands/CreateOrder.cs diff --git a/src/EFCoreTestApp/Controllers/OrderController.cs b/src/EFCoreTestAppWithCutomSaga/Controllers/OrderController.cs similarity index 100% rename from src/EFCoreTestApp/Controllers/OrderController.cs rename to src/EFCoreTestAppWithCutomSaga/Controllers/OrderController.cs diff --git a/src/EFCoreTestApp/Controllers/SagaController.cs b/src/EFCoreTestAppWithCutomSaga/Controllers/SagaController.cs similarity index 100% rename from src/EFCoreTestApp/Controllers/SagaController.cs rename to src/EFCoreTestAppWithCutomSaga/Controllers/SagaController.cs diff --git a/src/EFCoreTestApp/DTO/CreateOrderDTO.cs b/src/EFCoreTestAppWithCutomSaga/DTO/CreateOrderDTO.cs similarity index 100% rename from src/EFCoreTestApp/DTO/CreateOrderDTO.cs rename to src/EFCoreTestAppWithCutomSaga/DTO/CreateOrderDTO.cs diff --git a/src/EFCoreTestApp/DTO/OrderCreatedDTO.cs b/src/EFCoreTestAppWithCutomSaga/DTO/OrderCreatedDTO.cs similarity index 100% rename from src/EFCoreTestApp/DTO/OrderCreatedDTO.cs rename to src/EFCoreTestAppWithCutomSaga/DTO/OrderCreatedDTO.cs diff --git a/src/EFCoreTestApp/EFCoreTestApp.csproj b/src/EFCoreTestAppWithCutomSaga/EFCoreTestAppWithCutomSaga.csproj similarity index 100% rename from src/EFCoreTestApp/EFCoreTestApp.csproj rename to src/EFCoreTestAppWithCutomSaga/EFCoreTestAppWithCutomSaga.csproj diff --git a/src/EFCoreTestApp/EntityConfigurations/SagaLogDataEntityTypeConfiguration.cs b/src/EFCoreTestAppWithCutomSaga/EntityConfigurations/SagaLogDataEntityTypeConfiguration.cs similarity index 100% rename from src/EFCoreTestApp/EntityConfigurations/SagaLogDataEntityTypeConfiguration.cs rename to src/EFCoreTestAppWithCutomSaga/EntityConfigurations/SagaLogDataEntityTypeConfiguration.cs diff --git a/src/EFCoreTestApp/EntityConfigurations/SagaStateEntityTypeConfiguration.cs b/src/EFCoreTestAppWithCutomSaga/EntityConfigurations/SagaStateEntityTypeConfiguration.cs similarity index 100% rename from src/EFCoreTestApp/EntityConfigurations/SagaStateEntityTypeConfiguration.cs rename to src/EFCoreTestAppWithCutomSaga/EntityConfigurations/SagaStateEntityTypeConfiguration.cs diff --git a/src/EFCoreTestApp/Events/OrderCreated.cs b/src/EFCoreTestAppWithCutomSaga/Events/OrderCreated.cs similarity index 100% rename from src/EFCoreTestApp/Events/OrderCreated.cs rename to src/EFCoreTestAppWithCutomSaga/Events/OrderCreated.cs diff --git a/src/EFCoreTestApp/Handlers/OrderSagaHandler.cs b/src/EFCoreTestAppWithCutomSaga/Handlers/OrderSagaHandler.cs similarity index 100% rename from src/EFCoreTestApp/Handlers/OrderSagaHandler.cs rename to src/EFCoreTestAppWithCutomSaga/Handlers/OrderSagaHandler.cs diff --git a/src/EFCoreTestApp/Persistence/ISagaLogRepository.cs b/src/EFCoreTestAppWithCutomSaga/Persistence/ISagaLogRepository.cs similarity index 100% rename from src/EFCoreTestApp/Persistence/ISagaLogRepository.cs rename to src/EFCoreTestAppWithCutomSaga/Persistence/ISagaLogRepository.cs diff --git a/src/EFCoreTestApp/Persistence/ISagaStateDBRepository.cs b/src/EFCoreTestAppWithCutomSaga/Persistence/ISagaStateDBRepository.cs similarity index 100% rename from src/EFCoreTestApp/Persistence/ISagaStateDBRepository.cs rename to src/EFCoreTestAppWithCutomSaga/Persistence/ISagaStateDBRepository.cs diff --git a/src/EFCoreTestApp/Persistence/ISagaUnitOfWork.cs b/src/EFCoreTestAppWithCutomSaga/Persistence/ISagaUnitOfWork.cs similarity index 100% rename from src/EFCoreTestApp/Persistence/ISagaUnitOfWork.cs rename to src/EFCoreTestAppWithCutomSaga/Persistence/ISagaUnitOfWork.cs diff --git a/src/EFCoreTestApp/Persistence/SagaDbContext.cs b/src/EFCoreTestAppWithCutomSaga/Persistence/SagaDbContext.cs similarity index 100% rename from src/EFCoreTestApp/Persistence/SagaDbContext.cs rename to src/EFCoreTestAppWithCutomSaga/Persistence/SagaDbContext.cs diff --git a/src/EFCoreTestApp/Persistence/SagaLogRepository.cs b/src/EFCoreTestAppWithCutomSaga/Persistence/SagaLogRepository.cs similarity index 100% rename from src/EFCoreTestApp/Persistence/SagaLogRepository.cs rename to src/EFCoreTestAppWithCutomSaga/Persistence/SagaLogRepository.cs diff --git a/src/EFCoreTestApp/Persistence/SagaStateRepository.cs b/src/EFCoreTestAppWithCutomSaga/Persistence/SagaStateRepository.cs similarity index 100% rename from src/EFCoreTestApp/Persistence/SagaStateRepository.cs rename to src/EFCoreTestAppWithCutomSaga/Persistence/SagaStateRepository.cs diff --git a/src/EFCoreTestApp/Persistence/SagaUnitOfWork.cs b/src/EFCoreTestAppWithCutomSaga/Persistence/SagaUnitOfWork.cs similarity index 100% rename from src/EFCoreTestApp/Persistence/SagaUnitOfWork.cs rename to src/EFCoreTestAppWithCutomSaga/Persistence/SagaUnitOfWork.cs diff --git a/src/EFCoreTestApp/Program.cs b/src/EFCoreTestAppWithCutomSaga/Program.cs similarity index 100% rename from src/EFCoreTestApp/Program.cs rename to src/EFCoreTestAppWithCutomSaga/Program.cs diff --git a/src/EFCoreTestApp/SagaRepository/EFCoreSagaLog.cs b/src/EFCoreTestAppWithCutomSaga/SagaRepository/EFCoreSagaLog.cs similarity index 100% rename from src/EFCoreTestApp/SagaRepository/EFCoreSagaLog.cs rename to src/EFCoreTestAppWithCutomSaga/SagaRepository/EFCoreSagaLog.cs diff --git a/src/EFCoreTestApp/SagaRepository/EFCoreSagaLogData.cs b/src/EFCoreTestAppWithCutomSaga/SagaRepository/EFCoreSagaLogData.cs similarity index 100% rename from src/EFCoreTestApp/SagaRepository/EFCoreSagaLogData.cs rename to src/EFCoreTestAppWithCutomSaga/SagaRepository/EFCoreSagaLogData.cs diff --git a/src/EFCoreTestApp/SagaRepository/EFCoreSagaState.cs b/src/EFCoreTestAppWithCutomSaga/SagaRepository/EFCoreSagaState.cs similarity index 100% rename from src/EFCoreTestApp/SagaRepository/EFCoreSagaState.cs rename to src/EFCoreTestAppWithCutomSaga/SagaRepository/EFCoreSagaState.cs diff --git a/src/EFCoreTestApp/SagaRepository/EFCoreSagaStateData.cs b/src/EFCoreTestAppWithCutomSaga/SagaRepository/EFCoreSagaStateData.cs similarity index 100% rename from src/EFCoreTestApp/SagaRepository/EFCoreSagaStateData.cs rename to src/EFCoreTestAppWithCutomSaga/SagaRepository/EFCoreSagaStateData.cs diff --git a/src/EFCoreTestApp/Sagas/CreatingOrderData.cs b/src/EFCoreTestAppWithCutomSaga/Sagas/CreatingOrderData.cs similarity index 100% rename from src/EFCoreTestApp/Sagas/CreatingOrderData.cs rename to src/EFCoreTestAppWithCutomSaga/Sagas/CreatingOrderData.cs diff --git a/src/EFCoreTestApp/Sagas/OrderSaga.cs b/src/EFCoreTestAppWithCutomSaga/Sagas/OrderSaga.cs similarity index 100% rename from src/EFCoreTestApp/Sagas/OrderSaga.cs rename to src/EFCoreTestAppWithCutomSaga/Sagas/OrderSaga.cs diff --git a/src/EFCoreTestApp/Startup.cs b/src/EFCoreTestAppWithCutomSaga/Startup.cs similarity index 100% rename from src/EFCoreTestApp/Startup.cs rename to src/EFCoreTestAppWithCutomSaga/Startup.cs diff --git a/src/EFCoreTestApp/appsettings.Development.json b/src/EFCoreTestAppWithCutomSaga/appsettings.Development.json similarity index 100% rename from src/EFCoreTestApp/appsettings.Development.json rename to src/EFCoreTestAppWithCutomSaga/appsettings.Development.json diff --git a/src/EFCoreTestApp/appsettings.json b/src/EFCoreTestAppWithCutomSaga/appsettings.json similarity index 100% rename from src/EFCoreTestApp/appsettings.json rename to src/EFCoreTestAppWithCutomSaga/appsettings.json From ea59c32cb097cf23fe9c3f50b4b962971760aeee Mon Sep 17 00:00:00 2001 From: Umer Iftikhar Date: Sat, 19 Sep 2020 12:57:43 +0300 Subject: [PATCH 7/8] Internal Implementaion of EFCore changed --- README.md | 86 ++++++++++++++++++- .../ConfigureSagaTables.cs | 16 ++++ .../SagaLogDataEntityTypeConfiguration.cs | 2 +- .../Extensions.cs | 13 +-- .../Persistence/EFCoreSagaLogData.cs | 2 +- .../Persistence/EFCoreSagaState.cs | 2 - .../Repositories/ISagaUnitOfWork.cs | 12 +++ .../Repositories/SagaDbContext.cs | 27 ------ .../Repositories/SagaLogRepository.cs | 11 ++- .../Repositories/SagaStateRepository.cs | 22 ++--- src/Chronicle.sln | 14 +++ .../Commands/CreateOrder.cs | 22 +++++ .../Controllers/OrderController.cs | 49 +++++++++++ .../DTO/CreateOrderDTO.cs | 17 ++++ .../DTO/OrderCreatedDTO.cs | 11 +++ .../EFCoreTestAppWithChronicleSaga.csproj | 23 +++++ .../Events/OrderCreated.cs | 18 ++++ .../Handlers/OrderSagaHandler.cs | 49 +++++++++++ .../Persistence/ICustomUnitOfWork.cs | 11 +++ .../Persistence/SagaDbContext.cs | 22 +++++ .../Persistence/SagaUnitOfWork.cs | 25 ++++++ src/EFCoreTestAppWithChronicleSaga/Program.cs | 26 ++++++ .../Sagas/CreatingOrderData.cs | 11 +++ .../Sagas/OrderSaga.cs | 51 +++++++++++ src/EFCoreTestAppWithChronicleSaga/Startup.cs | 78 +++++++++++++++++ .../appsettings.Development.json | 9 ++ .../appsettings.json | 13 +++ .../Sagas/OrderSaga.cs | 19 ---- src/EFCoreTestAppWithCutomSaga/Startup.cs | 16 ---- 29 files changed, 580 insertions(+), 97 deletions(-) create mode 100644 src/Chronicle.Integrations.EFCore/EntityConfigurations/ConfigureSagaTables.cs create mode 100644 src/Chronicle.Integrations.EFCore/Repositories/ISagaUnitOfWork.cs delete mode 100644 src/Chronicle.Integrations.EFCore/Repositories/SagaDbContext.cs create mode 100644 src/EFCoreTestAppWithChronicleSaga/Commands/CreateOrder.cs create mode 100644 src/EFCoreTestAppWithChronicleSaga/Controllers/OrderController.cs create mode 100644 src/EFCoreTestAppWithChronicleSaga/DTO/CreateOrderDTO.cs create mode 100644 src/EFCoreTestAppWithChronicleSaga/DTO/OrderCreatedDTO.cs create mode 100644 src/EFCoreTestAppWithChronicleSaga/EFCoreTestAppWithChronicleSaga.csproj create mode 100644 src/EFCoreTestAppWithChronicleSaga/Events/OrderCreated.cs create mode 100644 src/EFCoreTestAppWithChronicleSaga/Handlers/OrderSagaHandler.cs create mode 100644 src/EFCoreTestAppWithChronicleSaga/Persistence/ICustomUnitOfWork.cs create mode 100644 src/EFCoreTestAppWithChronicleSaga/Persistence/SagaDbContext.cs create mode 100644 src/EFCoreTestAppWithChronicleSaga/Persistence/SagaUnitOfWork.cs create mode 100644 src/EFCoreTestAppWithChronicleSaga/Program.cs create mode 100644 src/EFCoreTestAppWithChronicleSaga/Sagas/CreatingOrderData.cs create mode 100644 src/EFCoreTestAppWithChronicleSaga/Sagas/OrderSaga.cs create mode 100644 src/EFCoreTestAppWithChronicleSaga/Startup.cs create mode 100644 src/EFCoreTestAppWithChronicleSaga/appsettings.Development.json create mode 100644 src/EFCoreTestAppWithChronicleSaga/appsettings.json diff --git a/README.md b/README.md index ca55978..2c243eb 100644 --- a/README.md +++ b/README.md @@ -103,7 +103,91 @@ The result looks as follows: ![Result](https://user-images.githubusercontent.com/7096476/53180548-0c885900-35f6-11e9-864b-6b6d13641f2a.png) -# Sample Application with EFCore +# Sample Application using Chronicle Implementation of EFCore +##### Application Name: `EFCoreTestAppWithChronicleSaga` +There are certain prerequisites to fulfill inorder for the internal implementation to work: +1. Extend `ISagaUnitOfWork` from `Chronicle.Integrations.EFCore.Repositories` as shown below: +```cs +using Microsoft.EntityFrameworkCore; +using Chronicle.Integrations.EFCore.Repositories; +namespace EFCoreTestApp.Persistence +{ + public interface ICustomUnitOfWork : ISagaUnitOfWork + where TContext : DbContext + { + } + + public class SagaUnitOfWork : ICustomUnitOfWork + where TContext : DbContext + { + protected readonly TContext DbContext; + + public SagaUnitOfWork(TContext dbContext) + { + DbContext = dbContext; + } + + public async Task CommitAsync(CancellationToken cancellationToken) + { + await DbContext.SaveChangesAsync(cancellationToken); + } + + } +} +``` +2. While creating the DBContext make sure that the Saga related Tables have been initialized, by invoking the `Create` Method on `ConfigureSagaTables`: +```cs +using Microsoft.EntityFrameworkCore; +using Chronicle.Integrations.EFCore.EntityConfigurations; + +namespace EFCoreTestApp.Persistence +{ + public class SagaDbContext : DbContext + { + public SagaDbContext(DbContextOptions options) + : base(options) + { + Database.EnsureCreated(); + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + // NOTE: MAKE SURE SAGA MODELS ARE CREATED IN DB + ConfigureSagaTables.Create(modelBuilder); + // Configure other models for the Application. + } + } +} +``` +3. Configure the following in the Startup.cs file: +```cs +services.AddDbContext(builder => +{ + var connStr = this.Configuration.GetConnectionString("db"); + builder.UseSqlServer(connStr); +}); +services.AddScoped, SagaUnitOfWork>(); +static void TestChronicleBuilder(IChronicleBuilder cb) +{ + cb.UseEFCorePersistence(); +} +services.AddChronicle(TestChronicleBuilder); +``` +4. Since unit of work is being used and is passed by the Application, therefore, do not forget to call `CommitAsync` mentod after all the work has been completed. Example can bee seen in the this file: Handlers -> OrderSagaHandler.cs +```cs +public async Task Handle(CreateOrder command, CancellationToken cancellationToken) +{ + await _coordinator.ProcessAsync(command, SagaContext.Empty); + // once every thing is processed in the handler, commit changes to DB. + // This can be moved else where depending on the user needs. + await SagaUnitOfWork.CommitAsync(); + return Unit.Value; +} +``` + +# Sample Application using Cutom Implementaion of `ISagaLog` & `ISagaStateRepository` with EFCore +##### Application Name: `EFCoreTestAppWithCutomSaga` + DB connection string is in **appsettings.json** file under property **ConnectionStrings->db**. Persistence has been added under folder: EFCoreTestApp-> Persistence Custom Implementation for `ISagaLog` & `ISagaStateRepository` has been added under folder: EFCoreTestApp-> SagaRepository diff --git a/src/Chronicle.Integrations.EFCore/EntityConfigurations/ConfigureSagaTables.cs b/src/Chronicle.Integrations.EFCore/EntityConfigurations/ConfigureSagaTables.cs new file mode 100644 index 0000000..8b78dcf --- /dev/null +++ b/src/Chronicle.Integrations.EFCore/EntityConfigurations/ConfigureSagaTables.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.EntityFrameworkCore; + +namespace Chronicle.Integrations.EFCore.EntityConfigurations +{ + public static class ConfigureSagaTables + { + public static void Create(ModelBuilder modelBuilder) + { + modelBuilder.ApplyConfiguration(new SagaLogDataEntityTypeConfiguration()); + modelBuilder.ApplyConfiguration(new SagaStateEntityTypeConfiguration()); + } + } +} diff --git a/src/Chronicle.Integrations.EFCore/EntityConfigurations/SagaLogDataEntityTypeConfiguration.cs b/src/Chronicle.Integrations.EFCore/EntityConfigurations/SagaLogDataEntityTypeConfiguration.cs index 9d033cf..83cce89 100644 --- a/src/Chronicle.Integrations.EFCore/EntityConfigurations/SagaLogDataEntityTypeConfiguration.cs +++ b/src/Chronicle.Integrations.EFCore/EntityConfigurations/SagaLogDataEntityTypeConfiguration.cs @@ -4,7 +4,7 @@ namespace Chronicle.Integrations.EFCore.EntityConfigurations { - public class SagaLogDataEntityTypeConfiguration : IEntityTypeConfiguration + internal class SagaLogDataEntityTypeConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { diff --git a/src/Chronicle.Integrations.EFCore/Extensions.cs b/src/Chronicle.Integrations.EFCore/Extensions.cs index 1ac7676..f5c2274 100644 --- a/src/Chronicle.Integrations.EFCore/Extensions.cs +++ b/src/Chronicle.Integrations.EFCore/Extensions.cs @@ -1,7 +1,5 @@ -using System; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Configuration; using Chronicle.Integrations.EFCore.Repositories; using Chronicle.Integrations.EFCore.Persistence; @@ -9,14 +7,11 @@ namespace Chronicle.Integrations.EFCore { public static class Extensions { - public static IChronicleBuilder UseEFCorePersistence(this IChronicleBuilder builder, string dbConnectionString) + public static IChronicleBuilder UseEFCorePersistence(this IChronicleBuilder builder) + where TContext: DbContext { - builder.Services.AddTransient(); - builder.Services.AddTransient(); - builder.Services.AddDbContext(builder => - { - builder.UseSqlServer(dbConnectionString); - }, ServiceLifetime.Transient); + builder.Services.AddTransient>(); + builder.Services.AddTransient>(); builder.UseSagaLog(); builder.UseSagaStateRepository(); diff --git a/src/Chronicle.Integrations.EFCore/Persistence/EFCoreSagaLogData.cs b/src/Chronicle.Integrations.EFCore/Persistence/EFCoreSagaLogData.cs index e8acd31..4afd79b 100644 --- a/src/Chronicle.Integrations.EFCore/Persistence/EFCoreSagaLogData.cs +++ b/src/Chronicle.Integrations.EFCore/Persistence/EFCoreSagaLogData.cs @@ -5,7 +5,7 @@ namespace Chronicle.Integrations.EFCore.Persistence { - public class EFCoreSagaLogData : ISagaLogData + internal class EFCoreSagaLogData : ISagaLogData { public int logId { get; set; } public string SagaId { get; set; } diff --git a/src/Chronicle.Integrations.EFCore/Persistence/EFCoreSagaState.cs b/src/Chronicle.Integrations.EFCore/Persistence/EFCoreSagaState.cs index 1ca8324..e640302 100644 --- a/src/Chronicle.Integrations.EFCore/Persistence/EFCoreSagaState.cs +++ b/src/Chronicle.Integrations.EFCore/Persistence/EFCoreSagaState.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; using Newtonsoft.Json; using Chronicle.Integrations.EFCore.Repositories; diff --git a/src/Chronicle.Integrations.EFCore/Repositories/ISagaUnitOfWork.cs b/src/Chronicle.Integrations.EFCore/Repositories/ISagaUnitOfWork.cs new file mode 100644 index 0000000..39f2da4 --- /dev/null +++ b/src/Chronicle.Integrations.EFCore/Repositories/ISagaUnitOfWork.cs @@ -0,0 +1,12 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; + +namespace Chronicle.Integrations.EFCore.Repositories +{ + public interface ISagaUnitOfWork where TContext: DbContext + { + Task CommitAsync(CancellationToken cancellationToken = default(CancellationToken)); + } +} diff --git a/src/Chronicle.Integrations.EFCore/Repositories/SagaDbContext.cs b/src/Chronicle.Integrations.EFCore/Repositories/SagaDbContext.cs deleted file mode 100644 index ed3f006..0000000 --- a/src/Chronicle.Integrations.EFCore/Repositories/SagaDbContext.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Chronicle.Integrations.EFCore.EntityConfigurations; -using Chronicle.Integrations.EFCore.Persistence; - - -namespace Chronicle.Integrations.EFCore.Repositories -{ - internal class SagaDbContext : DbContext - { - public SagaDbContext(DbContextOptions options) - : base(options) - { - // This can be removed and tables can be created manually as part of an Initialization Script - Database.EnsureCreated(); - } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - modelBuilder.ApplyConfiguration(new SagaLogDataEntityTypeConfiguration()); - modelBuilder.ApplyConfiguration(new SagaStateEntityTypeConfiguration()); - } - - public DbSet SagaLog { get; set; } - - public DbSet SagaState { get; set; } - } -} diff --git a/src/Chronicle.Integrations.EFCore/Repositories/SagaLogRepository.cs b/src/Chronicle.Integrations.EFCore/Repositories/SagaLogRepository.cs index d0c01f8..5a5f885 100644 --- a/src/Chronicle.Integrations.EFCore/Repositories/SagaLogRepository.cs +++ b/src/Chronicle.Integrations.EFCore/Repositories/SagaLogRepository.cs @@ -7,18 +7,18 @@ namespace Chronicle.Integrations.EFCore.Repositories { - internal class SagaLogRepository : ISagaLogRepository + internal class SagaLogRepository : ISagaLogRepository where TContext : DbContext { - private readonly SagaDbContext _dbContext; + private readonly TContext _dbContext; - public SagaLogRepository(SagaDbContext dbContext) + public SagaLogRepository(TContext dbContext) { _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext)); } public async Task> ReadAsync(SagaId id, Type type) { - return await _dbContext.SagaLog + return await _dbContext.Set() .Where(sld => sld.SagaId == id.Id && sld.SagaType == type.FullName) .ToArrayAsync(); } @@ -27,8 +27,7 @@ public async Task WriteAsync(EFCoreSagaLogData message) { if (null == message) throw new ArgumentNullException(nameof(message)); - await _dbContext.SagaLog.AddAsync(message); - await _dbContext.SaveChangesAsync(); + await _dbContext.Set().AddAsync(message); } } } diff --git a/src/Chronicle.Integrations.EFCore/Repositories/SagaStateRepository.cs b/src/Chronicle.Integrations.EFCore/Repositories/SagaStateRepository.cs index 48184b5..4274b9b 100644 --- a/src/Chronicle.Integrations.EFCore/Repositories/SagaStateRepository.cs +++ b/src/Chronicle.Integrations.EFCore/Repositories/SagaStateRepository.cs @@ -6,42 +6,34 @@ namespace Chronicle.Integrations.EFCore.Repositories { - internal class SagaStateRepository : ISagaStateDBRepository + internal class SagaStateRepository : ISagaStateDBRepository where TContext : DbContext { - private readonly SagaDbContext _dbContext; + private readonly TContext _dbContext; - public SagaStateRepository(SagaDbContext dbContext) + public SagaStateRepository(TContext dbContext) { _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext)); } - - public async Task GetByIdAsync(SagaId sagaId) - { - return await _dbContext.SagaState - .FirstOrDefaultAsync(sld => sld.SagaId == sagaId.Id); - } - public async Task ReadAsync(SagaId id, Type type) { - return await _dbContext.SagaState + return await _dbContext.Set() .FirstOrDefaultAsync(sld => sld.SagaId == id.Id && sld.SagaType == type.FullName); } public async Task WriteAsync(EFCoreSagaStateData sagaState) { var entity = await _dbContext - .SagaState + .Set() .FirstOrDefaultAsync(sld => sld.SagaId == sagaState.Id.Id && sld.SagaType == sagaState.SagaType); if (entity != null) { - _dbContext.SagaState.Remove(entity); + _dbContext.Set().Remove(entity); } - await _dbContext.SagaState.AddAsync( + await _dbContext.Set().AddAsync( new EFCoreSagaStateData(sagaState.Id.Id, sagaState.SagaType, sagaState.State, JsonConvert.SerializeObject(sagaState.Data)) ); - await _dbContext.SaveChangesAsync(); } } } diff --git a/src/Chronicle.sln b/src/Chronicle.sln index e7c01f7..db88052 100644 --- a/src/Chronicle.sln +++ b/src/Chronicle.sln @@ -15,6 +15,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EFCoreTestAppWithCutomSaga" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Chronicle.Integrations.EFCore", "Chronicle.Integrations.EFCore\Chronicle.Integrations.EFCore.csproj", "{F0B818F0-BDC2-48C6-9E53-3F4526558701}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EFCoreTestAppWithChronicleSaga", "EFCoreTestAppWithChronicleSaga\EFCoreTestAppWithChronicleSaga.csproj", "{A9849344-1C00-45A4-A74E-DDC1A39C59D7}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -97,6 +99,18 @@ Global {F0B818F0-BDC2-48C6-9E53-3F4526558701}.Release|x64.Build.0 = Release|Any CPU {F0B818F0-BDC2-48C6-9E53-3F4526558701}.Release|x86.ActiveCfg = Release|Any CPU {F0B818F0-BDC2-48C6-9E53-3F4526558701}.Release|x86.Build.0 = Release|Any CPU + {A9849344-1C00-45A4-A74E-DDC1A39C59D7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A9849344-1C00-45A4-A74E-DDC1A39C59D7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A9849344-1C00-45A4-A74E-DDC1A39C59D7}.Debug|x64.ActiveCfg = Debug|Any CPU + {A9849344-1C00-45A4-A74E-DDC1A39C59D7}.Debug|x64.Build.0 = Debug|Any CPU + {A9849344-1C00-45A4-A74E-DDC1A39C59D7}.Debug|x86.ActiveCfg = Debug|Any CPU + {A9849344-1C00-45A4-A74E-DDC1A39C59D7}.Debug|x86.Build.0 = Debug|Any CPU + {A9849344-1C00-45A4-A74E-DDC1A39C59D7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A9849344-1C00-45A4-A74E-DDC1A39C59D7}.Release|Any CPU.Build.0 = Release|Any CPU + {A9849344-1C00-45A4-A74E-DDC1A39C59D7}.Release|x64.ActiveCfg = Release|Any CPU + {A9849344-1C00-45A4-A74E-DDC1A39C59D7}.Release|x64.Build.0 = Release|Any CPU + {A9849344-1C00-45A4-A74E-DDC1A39C59D7}.Release|x86.ActiveCfg = Release|Any CPU + {A9849344-1C00-45A4-A74E-DDC1A39C59D7}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/EFCoreTestAppWithChronicleSaga/Commands/CreateOrder.cs b/src/EFCoreTestAppWithChronicleSaga/Commands/CreateOrder.cs new file mode 100644 index 0000000..8e0e4a9 --- /dev/null +++ b/src/EFCoreTestAppWithChronicleSaga/Commands/CreateOrder.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using MediatR; + +namespace EFCoreTestApp.Commands +{ + public class CreateOrder: IRequest + { + public Guid OrderId { get; } + public Guid CustomerId { get; } + public Guid ParcelId { get; } + + public CreateOrder(Guid orderId, Guid customerId, Guid parcelId) + { + OrderId = orderId; + CustomerId = customerId; + ParcelId = parcelId; + } + } +} diff --git a/src/EFCoreTestAppWithChronicleSaga/Controllers/OrderController.cs b/src/EFCoreTestAppWithChronicleSaga/Controllers/OrderController.cs new file mode 100644 index 0000000..5a9d6f1 --- /dev/null +++ b/src/EFCoreTestAppWithChronicleSaga/Controllers/OrderController.cs @@ -0,0 +1,49 @@ +using System; +using System.Threading.Tasks; +using Chronicle; +using MediatR; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using EFCoreTestApp.Commands; +using EFCoreTestApp.Events; +using EFCoreTestApp.Persistence; +using EFCoreTestApp.DTO; + +namespace EFCoreTestApp.Controllers +{ + [ApiController] + [Route("api/[controller]")] + public class OrderController : ControllerBase + { + private readonly IMediator _mediator; + + public OrderController(IMediator mediator) + { + _mediator = mediator; + } + + // [HttpPost, Route("{id:guid}")] + [HttpPost] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public async Task CreateOrder([FromBody] CreateOrderDTO dto) + { + // [FromRoute] Guid id + var request = new CreateOrder(dto.OrderId, dto.CustomerId, dto.ParcelId); + var result = await _mediator.Send(request); + return Ok(); + } + + [HttpPost("created")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public async Task OrderCreated([FromBody] OrderCreatedDTO dto) + { + // [FromRoute] Guid id + var notification = new OrderCreated(dto.OrderId); + await _mediator.Publish(notification); + return Ok(); + } + + } +} diff --git a/src/EFCoreTestAppWithChronicleSaga/DTO/CreateOrderDTO.cs b/src/EFCoreTestAppWithChronicleSaga/DTO/CreateOrderDTO.cs new file mode 100644 index 0000000..017d566 --- /dev/null +++ b/src/EFCoreTestAppWithChronicleSaga/DTO/CreateOrderDTO.cs @@ -0,0 +1,17 @@ +using System; +using System.ComponentModel.DataAnnotations; + +namespace EFCoreTestApp.DTO +{ + public class CreateOrderDTO + { + [Required] + public Guid OrderId { get; set; } + + [Required] + public Guid CustomerId { get; set; } + + [Required] + public Guid ParcelId { get; set; } + } +} diff --git a/src/EFCoreTestAppWithChronicleSaga/DTO/OrderCreatedDTO.cs b/src/EFCoreTestAppWithChronicleSaga/DTO/OrderCreatedDTO.cs new file mode 100644 index 0000000..c502d0c --- /dev/null +++ b/src/EFCoreTestAppWithChronicleSaga/DTO/OrderCreatedDTO.cs @@ -0,0 +1,11 @@ +using System; +using System.ComponentModel.DataAnnotations; + +namespace EFCoreTestApp.DTO +{ + public class OrderCreatedDTO + { + [Required] + public Guid OrderId { get; set; } + } +} diff --git a/src/EFCoreTestAppWithChronicleSaga/EFCoreTestAppWithChronicleSaga.csproj b/src/EFCoreTestAppWithChronicleSaga/EFCoreTestAppWithChronicleSaga.csproj new file mode 100644 index 0000000..b2d7a49 --- /dev/null +++ b/src/EFCoreTestAppWithChronicleSaga/EFCoreTestAppWithChronicleSaga.csproj @@ -0,0 +1,23 @@ + + + + netcoreapp3.1 + + + + + + + + + + + + + + + + + + + diff --git a/src/EFCoreTestAppWithChronicleSaga/Events/OrderCreated.cs b/src/EFCoreTestAppWithChronicleSaga/Events/OrderCreated.cs new file mode 100644 index 0000000..dcd8cc7 --- /dev/null +++ b/src/EFCoreTestAppWithChronicleSaga/Events/OrderCreated.cs @@ -0,0 +1,18 @@ +using MediatR; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace EFCoreTestApp.Events +{ + public class OrderCreated: INotification + { + public Guid OrderId { get; } + + public OrderCreated(Guid orderId) + { + OrderId = orderId; + } + } +} diff --git a/src/EFCoreTestAppWithChronicleSaga/Handlers/OrderSagaHandler.cs b/src/EFCoreTestAppWithChronicleSaga/Handlers/OrderSagaHandler.cs new file mode 100644 index 0000000..f2247e5 --- /dev/null +++ b/src/EFCoreTestAppWithChronicleSaga/Handlers/OrderSagaHandler.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Chronicle; +using MediatR; +using EFCoreTestApp.Commands; +using EFCoreTestApp.Events; +using EFCoreTestApp.Persistence; + +namespace EFCoreTestApp.Handlers +{ + public class OrderSagaHandler: IRequestHandler, INotificationHandler + { + /* + NOTE: Remove ISagaUnitOfWork if the Chronicles Internal EFCore/SQL Server implementation is being used. + If implementing with a custom Solution enable it. + */ + + private readonly ISagaCoordinator _coordinator; + private ICustomUnitOfWork SagaUnitOfWork { get; } + + public OrderSagaHandler(ISagaCoordinator coordinator, ICustomUnitOfWork _sagaUnitOfWork) + { + _coordinator = coordinator; + SagaUnitOfWork = _sagaUnitOfWork; + } + + public async Task Handle(CreateOrder command, CancellationToken cancellationToken) + { + await _coordinator.ProcessAsync(command, SagaContext.Empty); + // once every thing is processed in the handler, commit changes to DB. + // This can be moved else where depending on the user needs. + await SagaUnitOfWork.CommitAsync(); + return Unit.Value; + } + + public async Task Handle(OrderCreated notification, CancellationToken cancellationToken) + { + IEnumerable metadata = new List(); + await _coordinator.ProcessAsync(notification, + SagaContext.Create((SagaId)notification.OrderId.ToString(), notification.GetType().Name, metadata)); + // once every thing is processed in the handler, commit changes to DB. + // This can be moved else where depending on the user needs. + await SagaUnitOfWork.CommitAsync(); + } + } +} diff --git a/src/EFCoreTestAppWithChronicleSaga/Persistence/ICustomUnitOfWork.cs b/src/EFCoreTestAppWithChronicleSaga/Persistence/ICustomUnitOfWork.cs new file mode 100644 index 0000000..c277e35 --- /dev/null +++ b/src/EFCoreTestAppWithChronicleSaga/Persistence/ICustomUnitOfWork.cs @@ -0,0 +1,11 @@ +using System; +using Microsoft.EntityFrameworkCore; +using Chronicle.Integrations.EFCore.Repositories; + +namespace EFCoreTestApp.Persistence +{ + // Inhert from ISagaUnitOfWork from Chronicle.Integrations.EFCore.Repositories + public interface ICustomUnitOfWork : ISagaUnitOfWork where TContext : DbContext + { + } +} diff --git a/src/EFCoreTestAppWithChronicleSaga/Persistence/SagaDbContext.cs b/src/EFCoreTestAppWithChronicleSaga/Persistence/SagaDbContext.cs new file mode 100644 index 0000000..880c953 --- /dev/null +++ b/src/EFCoreTestAppWithChronicleSaga/Persistence/SagaDbContext.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore; +using Chronicle.Integrations.EFCore.EntityConfigurations; + +namespace EFCoreTestApp.Persistence +{ + public class SagaDbContext : DbContext + { + public SagaDbContext(DbContextOptions options) + : base(options) + { + Database.EnsureCreated(); + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + // NOTE: MAKE SURE SAGA MODELS ARE CREATED IN DB + ConfigureSagaTables.Create(modelBuilder); + + // Configure other models for the Application. + } + } +} diff --git a/src/EFCoreTestAppWithChronicleSaga/Persistence/SagaUnitOfWork.cs b/src/EFCoreTestAppWithChronicleSaga/Persistence/SagaUnitOfWork.cs new file mode 100644 index 0000000..30d8660 --- /dev/null +++ b/src/EFCoreTestAppWithChronicleSaga/Persistence/SagaUnitOfWork.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; + +namespace EFCoreTestApp.Persistence +{ + public class SagaUnitOfWork : ICustomUnitOfWork where TContext : DbContext + { + protected readonly TContext DbContext; + + public SagaUnitOfWork(TContext dbContext) + { + DbContext = dbContext; + } + + public async Task CommitAsync(CancellationToken cancellationToken) + { + await DbContext.SaveChangesAsync(cancellationToken); + } + + } +} diff --git a/src/EFCoreTestAppWithChronicleSaga/Program.cs b/src/EFCoreTestAppWithChronicleSaga/Program.cs new file mode 100644 index 0000000..e3b0c0d --- /dev/null +++ b/src/EFCoreTestAppWithChronicleSaga/Program.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace EFCoreTestAppWithChronicleSaga +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/src/EFCoreTestAppWithChronicleSaga/Sagas/CreatingOrderData.cs b/src/EFCoreTestAppWithChronicleSaga/Sagas/CreatingOrderData.cs new file mode 100644 index 0000000..16e9e94 --- /dev/null +++ b/src/EFCoreTestAppWithChronicleSaga/Sagas/CreatingOrderData.cs @@ -0,0 +1,11 @@ +using System; + +namespace EFCoreTestApp.Sagas +{ + public class CreatingOrderData + { + public Guid OrderId { get; set; } + public Guid CustomerId { get; set; } + public Guid ParcelId { get; set; } + } +} diff --git a/src/EFCoreTestAppWithChronicleSaga/Sagas/OrderSaga.cs b/src/EFCoreTestAppWithChronicleSaga/Sagas/OrderSaga.cs new file mode 100644 index 0000000..479f08c --- /dev/null +++ b/src/EFCoreTestAppWithChronicleSaga/Sagas/OrderSaga.cs @@ -0,0 +1,51 @@ +using System; +using Chronicle; +using Microsoft.Extensions.Logging; +using System.Threading.Tasks; +using EFCoreTestApp.Commands; +using EFCoreTestApp.Events; +using Newtonsoft.Json.Linq; + +namespace EFCoreTestApp.Sagas +{ + public class OrderSaga : Saga, + ISagaStartAction, ISagaAction + { + private const string SagaHeader = "Saga"; + private readonly ILogger _logger; + + public OrderSaga(ILogger logger) + { + _logger = logger; + } + + public override SagaId ResolveId(object message, ISagaContext context) + => message switch + { + CreateOrder m => (SagaId)m.OrderId.ToString(), + OrderCreated m => (SagaId)m.OrderId.ToString(), + _ => base.ResolveId(message, context) + }; + + public async Task HandleAsync(CreateOrder message, ISagaContext context) + { + _logger.LogInformation($"[CreateOrder] Started a saga for order: '{message.OrderId}'."); + Data.ParcelId = message.ParcelId; + Data.OrderId = message.OrderId; + Data.CustomerId = message.CustomerId; + } + + public Task CompensateAsync(CreateOrder message, ISagaContext context) + => Task.CompletedTask; + + public async Task HandleAsync(OrderCreated message, ISagaContext context) + { + _logger.LogInformation($"[OrderCreated] Event for order: '{message.OrderId}'."); + Data.OrderId = message.OrderId; + await CompleteAsync(); + } + + public Task CompensateAsync(OrderCreated message, ISagaContext context) + => Task.CompletedTask; + } +} diff --git a/src/EFCoreTestAppWithChronicleSaga/Startup.cs b/src/EFCoreTestAppWithChronicleSaga/Startup.cs new file mode 100644 index 0000000..736c7c6 --- /dev/null +++ b/src/EFCoreTestAppWithChronicleSaga/Startup.cs @@ -0,0 +1,78 @@ +using System; +using MediatR; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Chronicle; +using Chronicle.Integrations.EFCore; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using EFCoreTestApp.Persistence; + +namespace EFCoreTestAppWithChronicleSaga +{ + public class Startup + { + private readonly IWebHostEnvironment _env; + public IConfiguration Configuration { get; } + + public Startup(IConfiguration configuration, IWebHostEnvironment env) + { + _env = env; + Configuration = configuration; + } + + // This method gets called by the runtime. Use this method to add services to the container. + // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 + public void ConfigureServices(IServiceCollection services) + { + services.AddControllers(); + /* + MAKE Sure that both SagaDbContext and ICustomUnitOfWork are registered as Scoped. + Since Unit of Work is being used, therefore the Chronicle EFCore doesnt save changes to DB, + infact it gives control back to the application to commit the changes as per requirement. + */ + services.AddDbContext(builder => + { + var connStr = this.Configuration.GetConnectionString("db"); + builder.UseSqlServer(connStr); + }); + services.AddScoped, SagaUnitOfWork>(); + + services.AddMediatR(new[]{ + typeof(Startup).Assembly + }); + + services.AddMvc().AddNewtonsoftJson(); + + static void TestChronicleBuilder(IChronicleBuilder cb) + { + cb.UseEFCorePersistence(); + } + services.AddChronicle(TestChronicleBuilder); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseRouting(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); + } + } +} diff --git a/src/EFCoreTestAppWithChronicleSaga/appsettings.Development.json b/src/EFCoreTestAppWithChronicleSaga/appsettings.Development.json new file mode 100644 index 0000000..8983e0f --- /dev/null +++ b/src/EFCoreTestAppWithChronicleSaga/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/src/EFCoreTestAppWithChronicleSaga/appsettings.json b/src/EFCoreTestAppWithChronicleSaga/appsettings.json new file mode 100644 index 0000000..8ccbb07 --- /dev/null +++ b/src/EFCoreTestAppWithChronicleSaga/appsettings.json @@ -0,0 +1,13 @@ +{ + "ConnectionStrings": { + "db": "Data Source=(localdb)\\mssqllocaldb;Initial Catalog=EFCore_Saga_Demo;Trusted_Connection=True;MultipleActiveResultSets=true" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} \ No newline at end of file diff --git a/src/EFCoreTestAppWithCutomSaga/Sagas/OrderSaga.cs b/src/EFCoreTestAppWithCutomSaga/Sagas/OrderSaga.cs index b462929..479f08c 100644 --- a/src/EFCoreTestAppWithCutomSaga/Sagas/OrderSaga.cs +++ b/src/EFCoreTestAppWithCutomSaga/Sagas/OrderSaga.cs @@ -19,25 +19,6 @@ public OrderSaga(ILogger logger) _logger = logger; } - /* - This extra function is added to make the Saga & SagaLog repositroies to be ignorant of any data type. - and since Newtonsoft is being used to cast Data as JObject, therefore a method must be present that - matches the given parameter and Initialize the data. - See this line: public object Data => JsonConvert.DeserializeObject(MessagePayload); - in EFCoreSagaStateData file. - - See this line in Chronicle repositry: - file name: src/Chronicle/Utils/SagaExtensions.cs - line number: 16 - saga.GetType().GetMethod(method, args.Select(arg => arg.GetType()).ToArray())?.Invoke(saga, args); - */ - - /*public void Initialize(SagaId id, SagaStates state, JObject data) - { - base.Initialize(id, state); - Data = data.ToObject(); - }*/ - public override SagaId ResolveId(object message, ISagaContext context) => message switch { diff --git a/src/EFCoreTestAppWithCutomSaga/Startup.cs b/src/EFCoreTestAppWithCutomSaga/Startup.cs index ccbd97f..06cc02a 100644 --- a/src/EFCoreTestAppWithCutomSaga/Startup.cs +++ b/src/EFCoreTestAppWithCutomSaga/Startup.cs @@ -9,8 +9,6 @@ using Microsoft.Extensions.Configuration; using EFCoreTestApp.SagaRepository; using EFCoreTestApp.Persistence; -// Inorder to use Chronicles internal EFCore Implementation -// using Chronicle.Integrations.EFCore; namespace EFCoreTestApp { @@ -40,17 +38,6 @@ public void ConfigureServices(IServiceCollection services) builder.UseSqlServer(connStr); }); - /* - NOTE: Enable only if Chroniel Internal Implementation needs to be used. - */ - /*static void TestChronicleBuilder(IChronicleBuilder cb) - { - cb.UseEFCorePersistence(_connectionString); - }*/ - - /* - NOTE: Remove this if the Chronicles Internal EFCore/SQL Server implementation is being used. - */ static void TestChronicleBuilder(IChronicleBuilder cb) { cb.UseSagaLog(); @@ -59,9 +46,6 @@ static void TestChronicleBuilder(IChronicleBuilder cb) services.AddChronicle(TestChronicleBuilder); - /* - NOTE: Remove this if the Chronicles Internal EFCore/SQL Server implementation is being used. - */ services.AddScoped(); services.AddScoped(); services.AddScoped(); From 789e3748d6129d2e1ab5cb55b2f41fd5f43618ee Mon Sep 17 00:00:00 2001 From: Umer Iftikhar Date: Sun, 8 Nov 2020 16:39:31 +0200 Subject: [PATCH 8/8] removed UNIT OF WORK --- README.md | 46 ++----------------- .../SagaLogDataEntityTypeConfiguration.cs | 4 +- .../Extensions.cs | 8 +--- .../Persistence/EFCoreSagaLog.cs | 21 +++++---- .../Persistence/EFCoreSagaLogData.cs | 2 +- .../Persistence/EFCoreSagaState.cs | 30 +++++++----- .../Persistence/EFCoreSagaStateData.cs | 9 +--- .../Repositories/ISagaLogRepository.cs | 16 ------- .../Repositories/ISagaStateDBRepository.cs | 15 ------ .../Repositories/ISagaUnitOfWork.cs | 12 ----- .../Repositories/SagaLogRepository.cs | 33 ------------- .../Repositories/SagaStateRepository.cs | 39 ---------------- .../Handlers/OrderSagaHandler.cs | 19 ++------ .../Persistence/ICustomUnitOfWork.cs | 11 ----- .../Persistence/SagaUnitOfWork.cs | 25 ---------- src/EFCoreTestAppWithChronicleSaga/Startup.cs | 8 +--- 16 files changed, 47 insertions(+), 251 deletions(-) delete mode 100644 src/Chronicle.Integrations.EFCore/Repositories/ISagaLogRepository.cs delete mode 100644 src/Chronicle.Integrations.EFCore/Repositories/ISagaStateDBRepository.cs delete mode 100644 src/Chronicle.Integrations.EFCore/Repositories/ISagaUnitOfWork.cs delete mode 100644 src/Chronicle.Integrations.EFCore/Repositories/SagaLogRepository.cs delete mode 100644 src/Chronicle.Integrations.EFCore/Repositories/SagaStateRepository.cs delete mode 100644 src/EFCoreTestAppWithChronicleSaga/Persistence/ICustomUnitOfWork.cs delete mode 100644 src/EFCoreTestAppWithChronicleSaga/Persistence/SagaUnitOfWork.cs diff --git a/README.md b/README.md index 2c243eb..0b01b89 100644 --- a/README.md +++ b/README.md @@ -106,36 +106,7 @@ The result looks as follows: # Sample Application using Chronicle Implementation of EFCore ##### Application Name: `EFCoreTestAppWithChronicleSaga` There are certain prerequisites to fulfill inorder for the internal implementation to work: -1. Extend `ISagaUnitOfWork` from `Chronicle.Integrations.EFCore.Repositories` as shown below: -```cs -using Microsoft.EntityFrameworkCore; -using Chronicle.Integrations.EFCore.Repositories; -namespace EFCoreTestApp.Persistence -{ - public interface ICustomUnitOfWork : ISagaUnitOfWork - where TContext : DbContext - { - } - - public class SagaUnitOfWork : ICustomUnitOfWork - where TContext : DbContext - { - protected readonly TContext DbContext; - - public SagaUnitOfWork(TContext dbContext) - { - DbContext = dbContext; - } - - public async Task CommitAsync(CancellationToken cancellationToken) - { - await DbContext.SaveChangesAsync(cancellationToken); - } - - } -} -``` -2. While creating the DBContext make sure that the Saga related Tables have been initialized, by invoking the `Create` Method on `ConfigureSagaTables`: +1. While creating the DBContext make sure that the Saga related Tables have been initialized, by invoking the `Create` Method on `ConfigureSagaTables`: ```cs using Microsoft.EntityFrameworkCore; using Chronicle.Integrations.EFCore.EntityConfigurations; @@ -165,25 +136,14 @@ services.AddDbContext(builder => { var connStr = this.Configuration.GetConnectionString("db"); builder.UseSqlServer(connStr); -}); -services.AddScoped, SagaUnitOfWork>(); +}, ServiceLifetime.Transient); + static void TestChronicleBuilder(IChronicleBuilder cb) { cb.UseEFCorePersistence(); } services.AddChronicle(TestChronicleBuilder); ``` -4. Since unit of work is being used and is passed by the Application, therefore, do not forget to call `CommitAsync` mentod after all the work has been completed. Example can bee seen in the this file: Handlers -> OrderSagaHandler.cs -```cs -public async Task Handle(CreateOrder command, CancellationToken cancellationToken) -{ - await _coordinator.ProcessAsync(command, SagaContext.Empty); - // once every thing is processed in the handler, commit changes to DB. - // This can be moved else where depending on the user needs. - await SagaUnitOfWork.CommitAsync(); - return Unit.Value; -} -``` # Sample Application using Cutom Implementaion of `ISagaLog` & `ISagaStateRepository` with EFCore ##### Application Name: `EFCoreTestAppWithCutomSaga` diff --git a/src/Chronicle.Integrations.EFCore/EntityConfigurations/SagaLogDataEntityTypeConfiguration.cs b/src/Chronicle.Integrations.EFCore/EntityConfigurations/SagaLogDataEntityTypeConfiguration.cs index 83cce89..8d2e119 100644 --- a/src/Chronicle.Integrations.EFCore/EntityConfigurations/SagaLogDataEntityTypeConfiguration.cs +++ b/src/Chronicle.Integrations.EFCore/EntityConfigurations/SagaLogDataEntityTypeConfiguration.cs @@ -9,8 +9,8 @@ internal class SagaLogDataEntityTypeConfiguration : IEntityTypeConfiguration builder) { builder.ToTable("SagaLog", "dbo"); - builder.HasKey(c => c.logId); - builder.Property(c => c.logId).ValueGeneratedOnAdd(); + builder.HasKey(c => c.LogId); + builder.Property(c => c.LogId).ValueGeneratedOnAdd(); builder.Ignore(c => c.Id); builder.Ignore(c => c.Message); } diff --git a/src/Chronicle.Integrations.EFCore/Extensions.cs b/src/Chronicle.Integrations.EFCore/Extensions.cs index f5c2274..1cc3607 100644 --- a/src/Chronicle.Integrations.EFCore/Extensions.cs +++ b/src/Chronicle.Integrations.EFCore/Extensions.cs @@ -1,6 +1,4 @@ using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; -using Chronicle.Integrations.EFCore.Repositories; using Chronicle.Integrations.EFCore.Persistence; namespace Chronicle.Integrations.EFCore @@ -10,10 +8,8 @@ public static class Extensions public static IChronicleBuilder UseEFCorePersistence(this IChronicleBuilder builder) where TContext: DbContext { - builder.Services.AddTransient>(); - builder.Services.AddTransient>(); - builder.UseSagaLog(); - builder.UseSagaStateRepository(); + builder.UseSagaLog>(); + builder.UseSagaStateRepository>(); return builder; } diff --git a/src/Chronicle.Integrations.EFCore/Persistence/EFCoreSagaLog.cs b/src/Chronicle.Integrations.EFCore/Persistence/EFCoreSagaLog.cs index 8a4fe2c..b04dd4d 100644 --- a/src/Chronicle.Integrations.EFCore/Persistence/EFCoreSagaLog.cs +++ b/src/Chronicle.Integrations.EFCore/Persistence/EFCoreSagaLog.cs @@ -1,27 +1,32 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; using Newtonsoft.Json; -using Chronicle.Integrations.EFCore.Repositories; namespace Chronicle.Integrations.EFCore.Persistence { - internal class EFCoreSagaLog : ISagaLog + internal class EFCoreSagaLog : ISagaLog where TContext : DbContext { - private readonly ISagaLogRepository SagaLogRepository; + private readonly TContext _dbContext; - public EFCoreSagaLog(ISagaLogRepository _sagaLogRepository) + public EFCoreSagaLog(TContext dbContext) { - SagaLogRepository = _sagaLogRepository ?? throw new ArgumentNullException(nameof(_sagaLogRepository)); + _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext)); } public async Task> ReadAsync(SagaId id, Type type) - => await SagaLogRepository.ReadAsync(id, type); + => await _dbContext.Set() + .Where(sld => sld.SagaId == id.Id && sld.SagaType == type.FullName) + .ToArrayAsync(); public async Task WriteAsync(ISagaLogData message) { - await SagaLogRepository - .WriteAsync(new EFCoreSagaLogData(message.Id.Id, message.Type.ToString(), message.CreatedAt, JsonConvert.SerializeObject(message.Message))); + if (null == message) + throw new ArgumentNullException(nameof(message)); + await _dbContext.Set().AddAsync(new EFCoreSagaLogData(message.Id.Id, message.Type.ToString(), message.CreatedAt, JsonConvert.SerializeObject(message.Message))); + await _dbContext.SaveChangesAsync(); } } diff --git a/src/Chronicle.Integrations.EFCore/Persistence/EFCoreSagaLogData.cs b/src/Chronicle.Integrations.EFCore/Persistence/EFCoreSagaLogData.cs index 4afd79b..f1e2935 100644 --- a/src/Chronicle.Integrations.EFCore/Persistence/EFCoreSagaLogData.cs +++ b/src/Chronicle.Integrations.EFCore/Persistence/EFCoreSagaLogData.cs @@ -7,7 +7,7 @@ namespace Chronicle.Integrations.EFCore.Persistence { internal class EFCoreSagaLogData : ISagaLogData { - public int logId { get; set; } + public int LogId { get; set; } public string SagaId { get; set; } [NotMapped] public SagaId Id => SagaId; diff --git a/src/Chronicle.Integrations.EFCore/Persistence/EFCoreSagaState.cs b/src/Chronicle.Integrations.EFCore/Persistence/EFCoreSagaState.cs index e640302..40c32d1 100644 --- a/src/Chronicle.Integrations.EFCore/Persistence/EFCoreSagaState.cs +++ b/src/Chronicle.Integrations.EFCore/Persistence/EFCoreSagaState.cs @@ -1,29 +1,37 @@ using System; using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; using Newtonsoft.Json; -using Chronicle.Integrations.EFCore.Repositories; namespace Chronicle.Integrations.EFCore.Persistence { - internal class EFCoreSagaState : ISagaStateRepository + internal class EFCoreSagaState : ISagaStateRepository where TContext : DbContext { - public ISagaStateDBRepository SagaStateDBRepository { get; } + private readonly TContext _dbContext; - public EFCoreSagaState(ISagaStateDBRepository _sagaStateDBRepository) + public EFCoreSagaState(TContext dbContext) { - SagaStateDBRepository = _sagaStateDBRepository ?? throw new ArgumentNullException(nameof(_sagaStateDBRepository)); + _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext)); } public async Task ReadAsync(SagaId id, Type type) - { - var _currSagaState = await SagaStateDBRepository.ReadAsync(id, type); - return _currSagaState; - } + => await _dbContext.Set() + .FirstOrDefaultAsync(sld => sld.SagaId == id.Id && sld.SagaType == type.FullName); public async Task WriteAsync(ISagaState state) { - await SagaStateDBRepository - .WriteAsync(new EFCoreSagaStateData(state.Id.Id, state.Type.ToString(), state.State, JsonConvert.SerializeObject(state.Data))); + var entity = await _dbContext + .Set() + .FirstOrDefaultAsync(sld => sld.SagaId == state.Id.Id && sld.SagaType == state.Type.ToString()); + if (entity is {}) + { + _dbContext.Set().Remove(entity); + } + + await _dbContext.Set().AddAsync( + new EFCoreSagaStateData(state.Id.Id, state.Type.ToString(), state.State, JsonConvert.SerializeObject(state.Data)) + ); + await _dbContext.SaveChangesAsync(); } } } diff --git a/src/Chronicle.Integrations.EFCore/Persistence/EFCoreSagaStateData.cs b/src/Chronicle.Integrations.EFCore/Persistence/EFCoreSagaStateData.cs index 0a41079..6ea9a74 100644 --- a/src/Chronicle.Integrations.EFCore/Persistence/EFCoreSagaStateData.cs +++ b/src/Chronicle.Integrations.EFCore/Persistence/EFCoreSagaStateData.cs @@ -10,14 +10,7 @@ internal class EFCoreSagaStateData : ISagaState { public string SagaId { get; set; } [NotMapped] - public SagaId Id - { - get - { - var currId = (SagaId)SagaId.ToString(); - return currId; - } - } + public SagaId Id => SagaId.ToString(); public string SagaType { get; set; } [NotMapped] diff --git a/src/Chronicle.Integrations.EFCore/Repositories/ISagaLogRepository.cs b/src/Chronicle.Integrations.EFCore/Repositories/ISagaLogRepository.cs deleted file mode 100644 index 1242078..0000000 --- a/src/Chronicle.Integrations.EFCore/Repositories/ISagaLogRepository.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Chronicle.Integrations.EFCore.Persistence; - -namespace Chronicle.Integrations.EFCore.Repositories -{ - internal interface ISagaLogRepository - { - Task> ReadAsync(SagaId id, Type type); - - Task WriteAsync(EFCoreSagaLogData message); - } -} diff --git a/src/Chronicle.Integrations.EFCore/Repositories/ISagaStateDBRepository.cs b/src/Chronicle.Integrations.EFCore/Repositories/ISagaStateDBRepository.cs deleted file mode 100644 index 3142863..0000000 --- a/src/Chronicle.Integrations.EFCore/Repositories/ISagaStateDBRepository.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Chronicle.Integrations.EFCore.Persistence; - -namespace Chronicle.Integrations.EFCore.Repositories -{ - internal interface ISagaStateDBRepository - { - Task ReadAsync(SagaId id, Type type); - Task WriteAsync(EFCoreSagaStateData message); - } -} diff --git a/src/Chronicle.Integrations.EFCore/Repositories/ISagaUnitOfWork.cs b/src/Chronicle.Integrations.EFCore/Repositories/ISagaUnitOfWork.cs deleted file mode 100644 index 39f2da4..0000000 --- a/src/Chronicle.Integrations.EFCore/Repositories/ISagaUnitOfWork.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; - -namespace Chronicle.Integrations.EFCore.Repositories -{ - public interface ISagaUnitOfWork where TContext: DbContext - { - Task CommitAsync(CancellationToken cancellationToken = default(CancellationToken)); - } -} diff --git a/src/Chronicle.Integrations.EFCore/Repositories/SagaLogRepository.cs b/src/Chronicle.Integrations.EFCore/Repositories/SagaLogRepository.cs deleted file mode 100644 index 5a5f885..0000000 --- a/src/Chronicle.Integrations.EFCore/Repositories/SagaLogRepository.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using Chronicle.Integrations.EFCore.Persistence; - -namespace Chronicle.Integrations.EFCore.Repositories -{ - internal class SagaLogRepository : ISagaLogRepository where TContext : DbContext - { - private readonly TContext _dbContext; - - public SagaLogRepository(TContext dbContext) - { - _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext)); - } - - public async Task> ReadAsync(SagaId id, Type type) - { - return await _dbContext.Set() - .Where(sld => sld.SagaId == id.Id && sld.SagaType == type.FullName) - .ToArrayAsync(); - } - - public async Task WriteAsync(EFCoreSagaLogData message) - { - if (null == message) - throw new ArgumentNullException(nameof(message)); - await _dbContext.Set().AddAsync(message); - } - } -} diff --git a/src/Chronicle.Integrations.EFCore/Repositories/SagaStateRepository.cs b/src/Chronicle.Integrations.EFCore/Repositories/SagaStateRepository.cs deleted file mode 100644 index 4274b9b..0000000 --- a/src/Chronicle.Integrations.EFCore/Repositories/SagaStateRepository.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using Newtonsoft.Json; -using Chronicle.Integrations.EFCore.Persistence; - -namespace Chronicle.Integrations.EFCore.Repositories -{ - internal class SagaStateRepository : ISagaStateDBRepository where TContext : DbContext - { - private readonly TContext _dbContext; - - public SagaStateRepository(TContext dbContext) - { - _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext)); - } - - public async Task ReadAsync(SagaId id, Type type) - { - return await _dbContext.Set() - .FirstOrDefaultAsync(sld => sld.SagaId == id.Id && sld.SagaType == type.FullName); - } - - public async Task WriteAsync(EFCoreSagaStateData sagaState) - { - var entity = await _dbContext - .Set() - .FirstOrDefaultAsync(sld => sld.SagaId == sagaState.Id.Id && sld.SagaType == sagaState.SagaType); - if (entity != null) - { - _dbContext.Set().Remove(entity); - } - - await _dbContext.Set().AddAsync( - new EFCoreSagaStateData(sagaState.Id.Id, sagaState.SagaType, sagaState.State, JsonConvert.SerializeObject(sagaState.Data)) - ); - } - } -} diff --git a/src/EFCoreTestAppWithChronicleSaga/Handlers/OrderSagaHandler.cs b/src/EFCoreTestAppWithChronicleSaga/Handlers/OrderSagaHandler.cs index f2247e5..9e93fdf 100644 --- a/src/EFCoreTestAppWithChronicleSaga/Handlers/OrderSagaHandler.cs +++ b/src/EFCoreTestAppWithChronicleSaga/Handlers/OrderSagaHandler.cs @@ -13,37 +13,28 @@ namespace EFCoreTestApp.Handlers { public class OrderSagaHandler: IRequestHandler, INotificationHandler { - /* - NOTE: Remove ISagaUnitOfWork if the Chronicles Internal EFCore/SQL Server implementation is being used. - If implementing with a custom Solution enable it. - */ - private readonly ISagaCoordinator _coordinator; - private ICustomUnitOfWork SagaUnitOfWork { get; } + private readonly SagaDbContext _dbContext; - public OrderSagaHandler(ISagaCoordinator coordinator, ICustomUnitOfWork _sagaUnitOfWork) + public OrderSagaHandler(ISagaCoordinator coordinator, SagaDbContext dbContext) { _coordinator = coordinator; - SagaUnitOfWork = _sagaUnitOfWork; + _dbContext = dbContext; } public async Task Handle(CreateOrder command, CancellationToken cancellationToken) { + // DO WORK HERE await _coordinator.ProcessAsync(command, SagaContext.Empty); - // once every thing is processed in the handler, commit changes to DB. - // This can be moved else where depending on the user needs. - await SagaUnitOfWork.CommitAsync(); return Unit.Value; } public async Task Handle(OrderCreated notification, CancellationToken cancellationToken) { + // DO WORK HERE IEnumerable metadata = new List(); await _coordinator.ProcessAsync(notification, SagaContext.Create((SagaId)notification.OrderId.ToString(), notification.GetType().Name, metadata)); - // once every thing is processed in the handler, commit changes to DB. - // This can be moved else where depending on the user needs. - await SagaUnitOfWork.CommitAsync(); } } } diff --git a/src/EFCoreTestAppWithChronicleSaga/Persistence/ICustomUnitOfWork.cs b/src/EFCoreTestAppWithChronicleSaga/Persistence/ICustomUnitOfWork.cs deleted file mode 100644 index c277e35..0000000 --- a/src/EFCoreTestAppWithChronicleSaga/Persistence/ICustomUnitOfWork.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore; -using Chronicle.Integrations.EFCore.Repositories; - -namespace EFCoreTestApp.Persistence -{ - // Inhert from ISagaUnitOfWork from Chronicle.Integrations.EFCore.Repositories - public interface ICustomUnitOfWork : ISagaUnitOfWork where TContext : DbContext - { - } -} diff --git a/src/EFCoreTestAppWithChronicleSaga/Persistence/SagaUnitOfWork.cs b/src/EFCoreTestAppWithChronicleSaga/Persistence/SagaUnitOfWork.cs deleted file mode 100644 index 30d8660..0000000 --- a/src/EFCoreTestAppWithChronicleSaga/Persistence/SagaUnitOfWork.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; - -namespace EFCoreTestApp.Persistence -{ - public class SagaUnitOfWork : ICustomUnitOfWork where TContext : DbContext - { - protected readonly TContext DbContext; - - public SagaUnitOfWork(TContext dbContext) - { - DbContext = dbContext; - } - - public async Task CommitAsync(CancellationToken cancellationToken) - { - await DbContext.SaveChangesAsync(cancellationToken); - } - - } -} diff --git a/src/EFCoreTestAppWithChronicleSaga/Startup.cs b/src/EFCoreTestAppWithChronicleSaga/Startup.cs index 736c7c6..441d703 100644 --- a/src/EFCoreTestAppWithChronicleSaga/Startup.cs +++ b/src/EFCoreTestAppWithChronicleSaga/Startup.cs @@ -32,17 +32,11 @@ public Startup(IConfiguration configuration, IWebHostEnvironment env) public void ConfigureServices(IServiceCollection services) { services.AddControllers(); - /* - MAKE Sure that both SagaDbContext and ICustomUnitOfWork are registered as Scoped. - Since Unit of Work is being used, therefore the Chronicle EFCore doesnt save changes to DB, - infact it gives control back to the application to commit the changes as per requirement. - */ services.AddDbContext(builder => { var connStr = this.Configuration.GetConnectionString("db"); builder.UseSqlServer(connStr); - }); - services.AddScoped, SagaUnitOfWork>(); + }, ServiceLifetime.Transient); services.AddMediatR(new[]{ typeof(Startup).Assembly