Panama.Canal 6.0.2

There is a newer version of this package available.
See the version list below for details.
dotnet add package Panama.Canal --version 6.0.2                
NuGet\Install-Package Panama.Canal -Version 6.0.2                
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Panama.Canal" Version="6.0.2" />                
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add Panama.Canal --version 6.0.2                
#r "nuget: Panama.Canal, 6.0.2"                
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// Install Panama.Canal as a Cake Addin
#addin nuget:?package=Panama.Canal&version=6.0.2

// Install Panama.Canal as a Cake Tool
#tool nuget:?package=Panama.Canal&version=6.0.2                

Panama - A fluent dotnet core framework for developing event-based microservices

Panama is a dotnet core framework based on the command query responsibility segregation design pattern (CQRS overview).

The framework is organized around ICommand, IQuery, IValidate, IRollback interfaces:

ICommand

Commands are objects that can change the state of a domain. A chain of responsibilities in the form of multiple ICommand objects can be scaffolded on a handler to form a comprehensive domain event:

var result = await _provider
	.GetRequiredService<IHandler>()
	.Add(new User() { Email: diran.ogunlana@gmail.com })
	.Command<SaveUser>()
	.Command<PublishUser>()
	.Invoke();

These objects inherit from the ICommand interface and should only encapsulate a single business rule:

public class SaveUser : ICommand
{
	private UserDbContext _store;
	
	public SaveUser(UserDbContext store)
	{
		_store = store;
	}

	public bool Execute(IContext context)
	{
		var user = data.GetSingle<User>();
	
		_store.Entry(user).State = EntityState.Modified;
		_store.SaveChangesAsync();
	}
}

IQuery

Query objects represent readonly data store operations such as retreiving a collection of entities by a condition from a data store:

public class GetUser : IQuery
{
	private UserDbContext _store;
	
	public GetUser(UserDbContext store)
	{
		_store = store;
	}

	public bool Execute(IContext context)
	{
		//get id from payload
		var id = context.KvpGetSingle<string, string>("User.Id");
		
		var user = context.Users
			.Where(s => s.Id == id)
			.FirstOrDefault();
		
		//save queried user to handler context for processing down stream
		context.Data.Add(user);
	}
}

IValidate

Validators are objects that perform prerequisite operations against the handler context prior to query, command, or rollback operations:

public class EmailNotNullOrEmpty : IValidation
{
	public bool Execute(IContext context)
	{
		var models = context.DataGet<User>();
		if (models == null)
			throw new ValidationException("User(s) cannot be found.");

		foreach (var user in models)
			if (string.IsNullOrEmpty(user.Email))
				throw new ValidationException($"{user.Name} email is not valid.");
	}
}

IRollback

Rollback object perform the restoration operations against the domain in the event of an exception in processing the handler context. Take note of the use of the Snapshot filter on the context which creates a serialized copy of the object to preserve its original state:

public class RollbackUser : IRollback
{
	private UserDbContext _store;
	
	public RollbackUser(UserDbContext store)
	{
		_store = store;
	}

	public bool Execute(IContext context)
	{
		//get cached snapshot of previous state from handler context
		var existing = context.SnapshotGetSingle<User>();
		
		//save snapshot version e.g. prior to current changes
		_store.Entry(user).State = EntityState.Modified;
		
		await _store.SaveChangesAsync();
	}
}

Here is an example of a save domain event with validation and rollback capabilities:

var result = await _provider
	.GetRequiredService<IHandler>()
	.Add(new User() { 
		Email = "diran.ogunlana@gmail.com", 
		FirstName = "Diran" })
	.Validate<UserEmailNotNullOrEmpty>()
	.Validate<UserFirstNameNotNullOrEmpty>()
	.Query<GetUserByEmail>()
	.Command<CreateUserSnapshot>()
	.Command<SaveUser>()
	.Command<PublishUser>()
	.Rollback<RollbackUser>()
	.Invoke();

Getting Started

Default Configuration:

services.AddPanama(configuration: builder.Configuration);

For native logging support, add the Microsoft.Extensions.DependencyInjection and Microsoft.Extensions.Logging nuget packages with the following configuration:

services.AddLogging(loggingBuilder => {
	
	loggingBuilder.ClearProviders();
	loggingBuilder.SetMinimumLevel(LogLevel.Trace);
	
	// configure Logging with NLog, ex:
	// loggingBuilder.AddNLog(_configuration);
});

appsettings.json or environment variables can be for initial options configurations:

{
  "Logging": {
    "LogLevel": {
      "Default": "Debug"
    }
  },
  "AllowedHosts": "*",
  "Panama": {
    "Canal": {
	  "Options": {
        "Scope": [CLUSTER_NAMESPACE]
      }
    }
  }
}

Panama Canal - A transactional event bus framework for distributed microservices

The Canal provides a transactional messaging framework with saga support for dotnet core. The Canal integrates with native dotnet core DI, logging (dotnet core logging) and Entity Framework (dotnet ef core). Messages can be published using polling or event stream. Multiple message brokers can be configured and scoped for auto-scaling scenarios.

Panama Canal Architecture At A Glance:

image

Panama Canal Implementation At A Glance:

public class SaveWeatherForecast : ICommand
{
	private readonly IGenericChannelFactory _factory;
	private readonly WeatherForecastDbContext _store;

	public PublishWeatherForecast(
		IGenericChannelFactory factory, 
		WeatherForecastDbContext store)
	{
		_store = store;
		_factory = factory;
	}
	public async Task Execute(IContext context)
	{
		var model = context.Data.DataGet<WeatherForecast>();

		using (var channel = _factory.CreateChannel<DatabaseFacade, IDbContextTransaction>(_store.Database, context.Token))
		{
			_store.Entry(model).State = EntityState.Modified;
			_store.SaveChangesAsync();
			
			await context.Bus()
				.Channel(channel)
				.Token(context.Token)
				.Topic("forecast.created")
				.Data(model)
				.Post();

			await channel.Commit();
		}
	}
}

In the above example, a IChannel which creates a transaction to emit the forecast message if and only if the forecast was stored successfully via the Commit() function.

A saga can be used for more exhaustive use cases:

var model = context.Data.DataGet<WeatherForecast>();

using (var channel = _factory.CreateChannel<DatabaseFacade, IDbContextTransaction>(_store.Database, context.Token))
{
	...
	
	await context.Saga<CreateWeatherForcastSaga>()
		.Channel(channel)
		.Data(model)
		.Start();

	await channel.Commit();
}

see CreateWeatherForcast for the full implementation sample

Getting Started

Default Services Configuration:

UseDefaultStore and UseDefaultBroker are in-memory services used in unit testing scenarios

services.AddPanama(
	configuration: builder.Configuration,
	setup: options => {
		options.UseCanal(canal => {
			canal.UseDefaultStore();
			canal.UseDefaultBroker();
			canal.UseDefaultScheduler();
		});
	});

NOTE: Panama Canal must have a store and atleast one broker service configured

MySql & RabbitMQ Services Configuration:

services.AddPanama(
	configuration: builder.Configuration,
	setup: options => {
		options.UseCanal(canal => {
			canal.UseMySqlStore();
			canal.UseRabbitMq();
			canal.UseDefaultScheduler();
		});
	});

appsettings.json or environment variables can be for initial options configurations:

{
  "ConnectionStrings": {
    "MySql": [DB_CONNECTION]
  },
  "Logging": {
    "LogLevel": {
      "Default": "Debug"
    }
  },
  "AllowedHosts": "*",
  "Panama": {
    "Canal": {
	  "Options": {
        "Scope": [CLUSTER_NAMESPACE]
      },
      "Brokers": {
        "RabbitMQ": {
          "Options": {
            "Port": [RABBIT_PORT],
            "Host": [RABBIT_HOST],
            "Username": [RABBIT_USERNAME],
            "Password": [RABBIT_PASSWORD]
          }
        }
      },
      "Stores": {
        "MySql": {
          "Options": {
            "Port": [DB_PORT],
            "Host": [DB_HOST],
            "Username": [DB_USERNAME],
            "Password": [DB_PASSWORD],
            "Database": [DB_NAME]
          }
        }
      }
    }
  }
}

For inquiries and support, contact: Diran Ogunlana

Product Compatible and additional computed target framework versions.
.NET net6.0 is compatible.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (2)

Showing the top 2 NuGet packages that depend on Panama.Canal:

Package Downloads
Panama.Canal.MySQL

A MySql store implementation for the Panama Canal transactional messaging framework

Panama.Canal.RabbitMQ

A RabbitMq broker implementation for the Panama Canal transactional messaging framework

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
6.0.4 260 5/16/2023
6.0.3 144 5/16/2023
6.0.2 153 5/16/2023
6.0.1 201 5/16/2023
6.0.0 133 5/16/2023

fixed saga descriptor bug when no saga is present