Skip to content
This repository was archived by the owner on Feb 6, 2025. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,86 @@ The result looks as follows:

![Result](https://user-images.githubusercontent.com/7096476/53180548-0c885900-35f6-11e9-864b-6b6d13641f2a.png)

# Sample Application using Chronicle Implementation of EFCore
##### Application Name: `EFCoreTestAppWithChronicleSaga`
There are certain prerequisites to fulfill inorder for the internal implementation to work:
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;

namespace EFCoreTestApp.Persistence
{
public class SagaDbContext : DbContext
{
public SagaDbContext(DbContextOptions<SagaDbContext> 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<SagaDbContext>(builder =>
{
var connStr = this.Configuration.GetConnectionString("db");
builder.UseSqlServer(connStr);
}, ServiceLifetime.Transient);

static void TestChronicleBuilder(IChronicleBuilder cb)
{
cb.UseEFCorePersistence<SagaDbContext>();
}
services.AddChronicle(TestChronicleBuilder);
```

# 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
Custom Saga Persistence has been applied in Starup file using the following code:
```
static void TestChronicleBuilder(IChronicleBuilder cb)
{
cb.UseSagaLog<EFCoreSagaLog>();
cb.UseSagaStateRepository<EFCoreSagaState>();
}
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/).

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<Title>Chronicle.Integrations.EFCore</Title>
<AssemblyName>Chronicle.Integrations.EFCore</AssemblyName>
<PackageId>Chronicle_.Integrations.EFCore</PackageId>
<PackageTags>chronicle;saga;efcore</PackageTags>
<Description>Implementation of saga pattern for .NET Core</Description>
<Authors>Umer Iftikhar</Authors>
<VersionPrefix>3.1.1</VersionPrefix>
<Version>3.1.1</Version>
<AssemblyVersion>3.1.1</AssemblyVersion>
<FileVersion>3.1.1</FileVersion>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Chronicle_" Version="3.*" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="3.1.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="3.1.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="3.1.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.0.0" />
<PackageReference Include="System.Data.SqlClient" Version="4.8.0" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.1.8" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -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());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Chronicle.Integrations.EFCore.Persistence;

namespace Chronicle.Integrations.EFCore.EntityConfigurations
{
internal class SagaLogDataEntityTypeConfiguration : IEntityTypeConfiguration<EFCoreSagaLogData>
{
public void Configure(EntityTypeBuilder<EFCoreSagaLogData> 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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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<EFCoreSagaStateData>
{
public void Configure(EntityTypeBuilder<EFCoreSagaStateData> builder)
{
builder.ToTable("SagaState", "dbo");
builder.HasKey(r => r.SagaId);
builder.Ignore(c => c.Id);
builder.Ignore(c => c.Data);
}
}
}
17 changes: 17 additions & 0 deletions src/Chronicle.Integrations.EFCore/Extensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using Microsoft.EntityFrameworkCore;
using Chronicle.Integrations.EFCore.Persistence;

namespace Chronicle.Integrations.EFCore
{
public static class Extensions
{
public static IChronicleBuilder UseEFCorePersistence<TContext>(this IChronicleBuilder builder)
where TContext: DbContext
{
builder.UseSagaLog<EFCoreSagaLog<TContext>>();
builder.UseSagaStateRepository<EFCoreSagaState<TContext>>();

return builder;
}
}
}
33 changes: 33 additions & 0 deletions src/Chronicle.Integrations.EFCore/Persistence/EFCoreSagaLog.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json;

namespace Chronicle.Integrations.EFCore.Persistence
{
internal class EFCoreSagaLog<TContext> : ISagaLog where TContext : DbContext
{
private readonly TContext _dbContext;

public EFCoreSagaLog(TContext dbContext)
{
_dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
}

public async Task<IEnumerable<ISagaLogData>> ReadAsync(SagaId id, Type type)
=> await _dbContext.Set<EFCoreSagaLogData>()
.Where(sld => sld.SagaId == id.Id && sld.SagaType == type.FullName)
.ToArrayAsync();

public async Task WriteAsync(ISagaLogData message)
{
if (null == message)
throw new ArgumentNullException(nameof(message));
await _dbContext.Set<EFCoreSagaLogData>().AddAsync(new EFCoreSagaLogData(message.Id.Id, message.Type.ToString(), message.CreatedAt, JsonConvert.SerializeObject(message.Message)));
await _dbContext.SaveChangesAsync();
}
}

}
30 changes: 30 additions & 0 deletions src/Chronicle.Integrations.EFCore/Persistence/EFCoreSagaLogData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using System;
using System.ComponentModel.DataAnnotations.Schema;
using System.Reflection;
using Newtonsoft.Json;

namespace Chronicle.Integrations.EFCore.Persistence
{
internal 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;
}
}
}
37 changes: 37 additions & 0 deletions src/Chronicle.Integrations.EFCore/Persistence/EFCoreSagaState.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using System;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json;

namespace Chronicle.Integrations.EFCore.Persistence
{
internal class EFCoreSagaState<TContext> : ISagaStateRepository where TContext : DbContext
{
private readonly TContext _dbContext;

public EFCoreSagaState(TContext dbContext)
{
_dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
}

public async Task<ISagaState> ReadAsync(SagaId id, Type type)
=> await _dbContext.Set<EFCoreSagaStateData>()
.FirstOrDefaultAsync(sld => sld.SagaId == id.Id && sld.SagaType == type.FullName);

public async Task WriteAsync(ISagaState state)
{
var entity = await _dbContext
.Set<EFCoreSagaStateData>()
.FirstOrDefaultAsync(sld => sld.SagaId == state.Id.Id && sld.SagaType == state.Type.ToString());
if (entity is {})
{
_dbContext.Set<EFCoreSagaStateData>().Remove(entity);
}

await _dbContext.Set<EFCoreSagaStateData>().AddAsync(
new EFCoreSagaStateData(state.Id.Id, state.Type.ToString(), state.State, JsonConvert.SerializeObject(state.Data))
);
await _dbContext.SaveChangesAsync();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using System;
using Newtonsoft.Json;
using System.ComponentModel.DataAnnotations.Schema;
using System.Reflection;
using System.Linq;

namespace Chronicle.Integrations.EFCore.Persistence
{
internal class EFCoreSagaStateData : ISagaState
{
public string SagaId { get; set; }
[NotMapped]
public SagaId Id => SagaId.ToString();

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 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; }

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);
}

}
}
Loading