![]() |
VOOZH | about |
dotnet add package Sharpino --version 6.0.5
NuGet\Install-Package Sharpino -Version 6.0.5
<PackageReference Include="Sharpino" Version="6.0.5" />
<PackageVersion Include="Sharpino" Version="6.0.5" />Directory.Packages.props
<PackageReference Include="Sharpino" />Project file
paket add Sharpino --version 6.0.5
#r "nuget: Sharpino, 6.0.5"
#:package Sharpino@6.0.5
#addin nuget:?package=Sharpino&version=6.0.5Install as a Cake Addin
#tool nuget:?package=Sharpino&version=6.0.5Install as a Cake Tool
<img src="ico/sharpino.png" alt="drawing" width="50"/>
👁 NuGet version (Sharpino)
👁 License: MIT
Sharpino is a library to support Event-Sourcing in F# based on the following principles:
Sharpino.Lib.Core:
Sharpino.Lib:
runTests.sh
dotnet run --configuration:rabbitmq
rabbitmq-server
Please read the for all the information about how to contribute to the project.
Or you can run the test in the single directories.
Faq and trivia:
<GenerateProgramFile>false</GenerateProgramFile>
I can't be exaustive, but few comparisons in basic examples are the following respect to the Equinox library.
A heartfelt thank you to Jetbrains who have generously provided free licenses to support this project.
In this section, I will describe the upcasting techniques that any application may use to allow read snapshots in old format. Goal: using upcast techniques to be able to read the old (serialized) version of typeX into a new version of it.
Maximum Pool Size=60 on the connectionString.RefreshableAsync<'A> interface instead of Refreshable<'A>. Not backward compatible for the details cache part. Hint: The upgrade consist mainly in using taskResult c.e. instead of Result to wrap the refresher function and getting rid of Async.AwaitTask inside it. Some examples follow the new interface (even some more comprehensive ones with better explanations will follow). "DistanceBetweenSnapshots": 100,
ALTER TABLE public.events_01_course ADD COLUMN distance_from_latest_snapshot int;
CREATE OR REPLACE FUNCTION insert_md_01_course_event_and_return_id(
IN event_in bytea,
IN aggregate_id uuid,
IN distance_from_latest_snapshot int,
IN md text
)
RETURNS int
LANGUAGE plpgsql
AS $$
DECLARE
inserted_id integer;
BEGIN
INSERT INTO events_01_course(event, aggregate_id, distance_from_latest_snapshot, timestamp, md)
VALUES(event_in::bytea, aggregate_id, distance_from_latest_snapshot, now(), md) RETURNING id INTO inserted_id;
return inserted_id;
END;
$$;
CREATE OR REPLACE FUNCTION insert_md_01_course_aggregate_event_and_return_id(
IN event_in bytea,
IN aggregate_id uuid,
IN distance_from_latest_snapshot int,
IN md text
)
RETURNS int
LANGUAGE plpgsql
AS $$
DECLARE
inserted_id integer;
event_id integer;
BEGIN
event_id := insert_md_01_course_event_and_return_id(event_in, aggregate_id, distance_from_latest_snapshot, md);
INSERT INTO aggregate_events_01_course(aggregate_id, event_id)
VALUES(aggregate_id, event_id) RETURNING id INTO inserted_id;
return event_id;
END;
$$;
dbmate new alter_mytable
insert the SQL statements provided above by changing the version (_01) and storagename (_student) leaving the lines --migrate:up on top and --migrate:down on botton.dbmate upVersion 4.7.7: using ZiggyCreatures.FusionCache with Azure Sql Server as distributed cache and Azure Message Bus to propagate cache related events (see Sharpino.Sample.19).
Version 4.7.6: using ZiggyCreatures.FusionCache instead of MemoryCache
Version 4.7.5: optimize the getAllAggregateStates and getAllAggregateStatesAsync by filtering out the (soft) deleted aggregates using new db functions
Version 4.7.4: fix getAllAggregateStatesAsync and getAggregateStatesInATimeIntervalAsync are able to filter out deleted states from resut
Version 4.7.3: logging config is managed by appSettings.json. "setLogger" calls are deprecated.
Version 4.7.2: removed sharpinoSettings.json. Settings are part of appSettings.json now
Version 4.7.1: the "runPreExecuteAggregateCommd" can use the MessageSenders (RabbitMq for example).
Version 4.7.0: interface "Aggregate" is not needed anymore. Type constraints are enough → Related members Id and Serializer needs to be defined in the aggregate. Important: if using typed Id (wrapping Guid), then it cannot use the name Id anymore. The actual Id must be still Guid (so for example I can have a StudentId field of type StudentId and I still need to expose the Id as a Guid, like StudentId.Value)
Released a template with a minimal yml docker file for postgres if needed. https://github.com/tonyx/sharpinoTemplate
dotnet install Sharpino.Templates
dotnet new sharpino
Version 4.6.3: handle task/CancellationToken in few more eventstore/stateview functions
Blogged Event Sourcing in F#: From Cross-Stream Invariants to Refreshable Details
Version 4.6.1: handle CancellationToken "scope" (for implicit disposal) in pg(Binary)EventStore, added GetAllAggregateEventsInATimeIntervalAsync (ResizeArray based).
Version 4.6.0: added support for CancellationToken in StateView async functions and their related db functions
Version 4.5.8: added runInitAsync and runMultipleInitAsync to CommandHanldler and related db functions with optional CancellationToken.
Version 4.5.7: using console logging by default on Core.fs, update dependencies
Version 4.5.6: fix an update of refreshable/cachable details.
Version 4.5.5: added cachable details/view: a detailed view can be Refreshable and cachable, so that it refreshes when any of its object is updated (i.e. a related event is stored):
Version 4.5.4: few more versions of eventstore functions handling task/cancellationToken
Version 4.5.3: removed some dupication in eventstore
Version 4.5.2: changed the definition of "undoer" in core (returning new state not only compensation events). Cleaned core a little. It may break existing code ("undoers" may need an update).
Version 4.5.1 fix async issues
Version 4.5.0 (deprecated): First step of introducing more async task based methods (with optional cancellationToken) on EventStore
Version 4.4.9: Replaced ConcurrentDictionary based cache with MemoryCache
Version 4.4.7: fix a problem of indexes in aggregateCache that was unoticed and harmless (until 4.4.6).
Version 4.4.6 (DEPRECATED. need fix): avoid an unnecessary access to last snapshot event id to get last aggregateSnapshot
Added an article on medium F# Domain Model with Event Sourcing vs C# with Entity Framework
Version 4.4.4: added bulk object initializations
Version 4.4.3: added support for net10.0
Version 4.4.2: mkAggregateSnapshot is reintroduced (was dropped in 4.4.1)
Version 4.4.1: fixed Avoid db call to get lastEventId before probing the cache. In the getAggregateFreshState the lastEventId is computed within the events involved in the "evolve" from last snapshot and not in a second step.
Version 4.4.0: added MessageSenders (replacing partially the old IEventBroker) to send events to a message bus after they have been stored. Some Rabbitmq examples are provided. (warning. There is no backward compatibility as the MessageSenders replaces IEventBroker)
Version 4.3.4: added more info in some error messages
Version 4.3.3: fix md parameter
Version 4.3.2: updated dependencies, fixed date error in pgBinaryEventStore
Version 4.3.1: reintroduced concurrent dictionary aggregate cache
Version 4.3.0: Aggregate Cache is type independent. Added a way to "preExecute" any type of commands and then send them to the command handler. So now executing an arbitrary number of command of any type is allowed (see example 10). Note: some adjustments in passing metadata to "delete" commands will make them non-backward compatible (just add metadata to the command to fix).
Version 4.2.3: concurrent dictionary aggregates cache
Version 4.2.1: added a variant of delete with aggregateCommand
Version 4.2.0: fixed again the delete's (tested only on an external application not included in the examples, sorry)
Version 4.1.8: some fixes on new features
Version 4.1.7: added an alternative to getAggregateFreshStater (getHistoryAggregateFreshState) that includes historical (i.e. deleted) aggregate and skip caching. No example or test provided (hack).
Version 4.1.6: added runDelete with aggregateCommand (see sample 9 for a use case)
Version 4.1.5: fixed dependencies declared in manifest/nuspec file
Version 4.1.4: added soft delete with predicate (usually predicate is: counter references must be zero). Needs at applicative level increment counter each time a reference is created and decrement it when the reference is removed (see sample 9). Warning: deletion is not an event! Is just a state of the latest snapshot of an aggregate. After carefully evaluated the pros and cons I decided in this way (hint: any independent stream evolving does not care if the reference of an external id does actually exist or not. Getting the state of any aggregate depends primarily on the latest snapshot. If that last snapshot is deleted then it is as if it doesn't exist anymore as long as also the caches is aware of this deletion i.e. it is invalidated).
an example of integration with Blazor: https://github.com/tonyx/sharpinoBlazor (a summary, in Italian, made by Gemini A. I.: https://g.co/gemini/share/528e98bd6dd8)
Version 4.1.3: fixed some SQL issues of new functions introduced in 4.1.1/4.1.2 (involving only new stuff)
Version 4.1.2: deprecated getFilteredAggregateStatesInATimeInterval, added getFilteredAggregateStatesInATimeInterval2, getAggregateStatesInATimeInterval, getAllAggregateStates
Version 4.1.1: event store:added GetAggregateIds and GetAggregateIdsInATimeInterval to event
Version 4.1.0: removed the saga-ish runCommands (as the "forceRun" equivalent versions of runCommands are enough)
Version 4.0.2: introduces Stateview.getFilteredAggregateStatesInATimeInterval
Version 4.0.0: same as 3.10.6, just restarting numeration.
Version 3.10.6: added runInitAndNAggregateCommandsMd on command handler (accepts an initial state of a new aggregate of a certain type and N aggregate commands related to a different type type providing distinct aggregateIds) - it is has been tested on a private application. Feel free to add tests on examples (Sample 8 may be a good fit for it).
Version 3.10.5: skip the mailboxprocessor in running commands. Removing duplicate code in Pg based eventstore implementations. Fixed a bug of runThreeNAggregregateCommands in handling indexes (will take a closer look for the next release).
Version 3.10.3: in some cases forceRunTwo/ThreeNAggregateCommands skip caching.
Current version 3.10.2: added runInit that just create initial instance of an aggregate. I will use it to substitute runInitAndCommand to avoid "expansion" of the aggregate state in the cache. The use of MailboxProcesor for commands is based on a compile time constant as will be removed in the future.
(instead of stream level lock which ).
Added Example 8 related to the transport-tycoon domain. It is a simple example of a transport company that manages vehicles and routes.
blogged Sharpino Internals. Inside a functional event-sourcing-library, part 4
Added Sharpino.Sample.7 which shows two equals solutions based on JSON and BINARY serialization respectively.
blogged Sharpino Internals. Inside a functional event-sourcing-library, part 3
Current version 3.0.9
Version 3.0.6: forceRunThreeNAggregateCommands has been improved (aggregates involved in more than one command uses a state that is the result of the previous command in the same transaction)
Version 3.0.5: forceRunNAggregateCommands has been improved (aggregates involved in more than one command uses a state that is the result of the previous command in the same transaction)
Version 3.0.4: forceRunTwoNAggregateCommands has been improved (aggregates involved in more than one command uses a state that is the result of the previous command in the same transaction)
Version 3.0.3: Fixed bug in multiple events writing in Postgres eventstore (a local branch of an incoming feature can reproduce this bub)
Version 3.0.2: StateCache substituted by StateCache2. Access cached contexts don't need checking lastEventId to the eventstore for comparison.
Version 3.0.0: some cache improvements, supporting net9.0 and net8.0 (ditched net7.0)
Version 2.7.7: The md field is mandatory for any event table: Any run(Aggregate)Commands are redirecting to the equivalent 'run(Aggregate)CommandsMd' that accepts metadata as a string.
Version 2.7.5: Can view history of events related to a set of aggregates in a time interval (StateView.getFilteredMultipleAggregateEventsInATimeInterval)
Version 2.7.4: A (quick)fix allows adding compensating events in pgEventstore in saga-ish that were rejected because of strict eventId control
Live example is here: restaurant management system tech stack: Blazor as Front end, Sharpino as backend, Postgres as event store, Azure as hosting.
Version 2.7.2: Support metadata field in db (any command has the correspondent commandMd that accepts any string as metadata). Those metadata can be used for debugging purposes. To use them any event table in the db needs a new nullable text field called "md". New db functions are also needed. See the functions like "insert_md{Version}{AggregateStorageName}_events_and_return_id" in the sql scripts in the SqlTemplate dir doing a proper substitution in {Version} and {Format} and {AggregateStorageName}. Similar function is in the ContextTemplate.sql.
Version 2.7.1: Bug fix
Version 2.6.8: Remove EventStoreDb and starting removing Kafka (for future rewrite or replacement).
Version 2.6.7: Optimize snapshotting by using the in-memory cached value to avoid multiple reads of the same aggregate.
Version 2.6.6: Can create new snapshots for aggregates that have no events yet (can happen when you want to do massive upcast/snapshot for any aggregate)
Version 2.6.4: the mkAggregateSnapshots and mkSnapshots are now public in commandhandler so that they can be used in the user application to create snapshots of the aggregates and contexts. This is userful after an aggregate refactoring update so that any application can do upcast of all aggregates and then store them as snapshots (and then foreget about the need to keep upcast logic active i.e. can get rid of any older version upcast chain).
Version 2.6.3: Stateview added getFilteredAggregateSnapshotsInATimeIntervalwhich returns a list of snapshots of a specific type/version of aggregate in a time interval filtered by some criteria no matter if any context contains references to those aggregates, so you can retrieve aggregates even if no context has references to them (for instance "inactive users").
Version 2.6.2: CommandHandler, PgEventStore and PgBinaryEventstore expose as setLogger (newLogger: ILogger) based on the ILogger interface replacing Log4net. You can then pass that value after retrieving it from the DI container (straightforward in a .net core/asp.net app).
Version 2.6.0: Added a function for the GDPR in command handler able to virtuallty delete snapshots and events, i.e. replace any event with an events that returns an empty version of the state and also replace any snapshot with the voided/empty version of that state (and also fill the cache with that empty value).
Version 2.5.9: Added the possibility via StateView to retrieve the initial state/initial snapshot of any aggregate to allow retrieving the data that the users claims. So when users unsubscribe to any app then they have the rights to get any data. This is possibile by getting the initial states and any following event. I think it will be ok to give the user a json of the initial snapshots and any events via an anonymous record and then let the use download that JSON.
Version 2.5.8: Added query for aggregate events in a time interval. StateView/Readmodel can use it passing a predicate to filter events (example: Query all the payment events). Aggregate should not keep those list ob objects to avoid unlimited grow.
A short pdf: why do we need upcastors for aggregates and not for events (sorry for typos)
Blog post: Upcasting aggregates in Sharpino
Blog post: comparing the example of the "Counter" app in Equnox and in Sharpino https://medium.com/@tonyx1/equinox-vs-sharpino-comparing-the-counter-example-0e2bd6e9bbf2
Version 2.5.4 added runInitAndTwoAggregateCommands that creates a new aggregate snapshot and run two commands in a single transaction.
Version 2.5.3 added runSagaThreeNAggregateCommands this is needed when transaction cannot be simultaneous for instance when it needs to involve the same aggregate in multiple commands. (A short example will come but here is an idea, pretending the aggregate types can be two, and not three: A1, A2, A3, A3 needs to merge into An: I cannot run the "indpendent" saga-free version of running multiple commands (pairs) because I should repeat the id of An many times which is invalid, so I run the saga version that executes the single "merge" i.e. merge A1 into An, then merge A2 into An etc...: if somethings goes wrong I have accuulted the "future undoers" that may rollback the eventually suffessful merges)
A "porting" of an example from Equinox https://github.com/tonyx/sharpinoinvoices
Version 2.5.2. add the runThreeNAggregateCommands (means being able to run simultaneusly n-ples of commands related to three different kind of aggregates)!
Kafka status: No update. Use the only database version of the events and the "doNothing" broker for (not) publishing.
Version 4.5.0 changed the signature of any command in user application. Commands and AggregateCommands return also the new computed state and not only the related events. Example:
| UpdateName name ->
dish.UpdateName name
|> Result.map (fun x -> (x, [NameUpdated name]))
Any application needs a little rewrite in the command part (vim macros may be helpful).
In this way the commandhandler takes advantage of it to be able to memoize the state in the cache, so that virtually the state will never be processed and at any state the cache will always be ready for the current state (unless the system restarts, and in that case the state will be taken by reading the last snapshot and processing the events from that point on).
let doNothingBroker =
{
notify = None
notifyAggregate = None
}
Usually the way we run commands against multiple aggregate doesn't require undoer, however it may happen. Plus: I am planning to use the undoer in the future for the proper user level undo/redo feature.
An example of the undoer for an aggregate is in the following module. Note: I try to avoid "undoer" that are meant to issue compensating events when two transactions are running in parallel and one fails and for any reason we decided to not use the cross aggregate transaction function in command handlers. The "undoers" are complex but still: they are in general not necessary (or if you want to suggest a way to simplify them it's fine).
module CartCommands =
type CartCommands =
| AddGood of Guid * int
| RemoveGood of Guid
interface AggregateCommand<Cart, CartEvents> with
member this.Execute (cart: Cart) =
match this with
| AddGood (goodRef, quantity) ->
cart.AddGood (goodRef, quantity)
|> Result.map (fun s -> (s, [GoodAdded (goodRef, quantity)]))
| RemoveGood goodRef ->
cart.RemoveGood goodRef
|> Result.map (fun s -> (s, [GoodRemoved goodRef]))
member this.Undoer =
match this with
| AddGood (goodRef, _) ->
Some
(fun (cart: Cart) (viewer: AggregateViewer<Cart>) ->
result {
let! (i, _) = viewer (cart.Id)
return
fun () ->
result {
let! (j, state) = viewer (cart.Id)
let! isGreater =
(j >= i)
|> Result.ofBool (sprintf "execution undo state '%d' must be after the undo command state '%d'" j i)
let result =
state.RemoveGood goodRef
|> Result.map (fun _ -> [GoodRemoved goodRef])
return! result
}
}
)
| RemoveGood goodRef ->
Some
(fun (cart: Cart) (viewer: AggregateViewer<Cart>) ->
result {
let! (i, state) = viewer (cart.Id)
let! goodQuantity = state.GetGoodAndQuantity goodRef
return
fun () ->
result {
let! (j, state) = viewer (cart.Id)
let! isGreater =
// this check depends also on the number of events generated by the command (i.e. the j >= (i+1) if command generates 2 event)
(j >= i)
|> Result.ofBool (sprintf "execution undo state '%d' must be after the undo command state '%d'" j i)
let result =
state.AddGood (goodRef, goodQuantity)
|> Result.map (fun _ -> [GoodAdded (goodRef, goodQuantity)])
return! result
}
}
)
Changes to the classic Blazor counter app to use Sharpino in the backend: https://github.com/tonyx/blazorCounterSharpino.git
Version 2.2.6: runCommands work in threads for aggregates and context using mailboxprocessors for aggregates (the number of those active mailboxprocessors can be limited in config)
Version 2.2.5: fix runCommand eventbroker notification.
Version 2.2.4: some changes in runCommand: no need to pass state and aggregateViewer as it will just use the ones based on the eventstore (source of truth). Supporting also net7.0. The "core" gets rid of TailCall attribute not compatible with net7.0. There is the possibility that including Sharpino.Core must be explicitly included. For an example of app that has been upagraded to the newest version of library see shopping cart
Version 2.1.3: added local fork of FsKafka (with library dependencies updated) to be able to use it in the project.
Version 2.1.0: going to remove newtonsoft, introduced FsPickler, FsKafka, changed kafka publisher way (binary and textencoding). Removed Kafkareceiver. Preparing to replace it with one based on FSKafka
I am porting the examples to use the newer version (2.0.6). The porting of the first example(Sharpino.Sample) is incomplete (At the moment I disabled the "migrate between version" function in that example).
version 2.0.7: a fix in runThreeCommand. CommandHandler will just use fresh data ignoring the viewer that has been passed.
version 2.0.6:
version 2.0.3: changes from "now()" to utcNow() format in eventstores (Postgres and inMemory) and Sql template scripts.
published version 2.0.0 supporting binary serialization for events and snapshots on Postgres. Note: the current examples provided are still referencing the previous 1.6.6 version. Here is an example compatible with 2.0.0. with binary serialization
added a few new examples (can be used for dojos) pub system
version 1.6.6: can use plain text instead of JSON data type for database (see scripts in SqlTemplate dir). The appSettings has a new settings for it:
{
"LockType":{"Case":"Optimistic"},
"RefreshTimeout": 100,
"CacheAggregateSize": 100,
"PgSqlJsonFormat":{"Case":"PlainText"},
"MailBoxCommandProcessorsSize": 100
}
The other option is:
"PgSqlJsonFormat":{"Case":"PgJson"}
Basically you may wan to write json fields into text fields for various reasons (on my side I experienced that an external library may require further tuning to properly work with jsonb fields in Postgres, so in that case a quick fix is just using text fields). Remember that we don't necessarily need Json fields as at the moment we just do serialize/deserialize and not querying on the json fields (at the moment).
old stuff deleted
More documentation (Sharpino gitbook)
<a href="https://www.buymeacoffee.com/Now7pmK92m" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" style="height: 60px !important;width: 217px !important;" ></a>
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 net8.0 is compatible. net8.0-android net8.0-android was computed. net8.0-browser net8.0-browser was computed. net8.0-ios net8.0-ios was computed. net8.0-maccatalyst net8.0-maccatalyst was computed. net8.0-macos net8.0-macos was computed. net8.0-tvos net8.0-tvos was computed. net8.0-windows net8.0-windows was computed. net9.0 net9.0 is compatible. net9.0-android net9.0-android was computed. net9.0-browser net9.0-browser was computed. net9.0-ios net9.0-ios was computed. net9.0-maccatalyst net9.0-maccatalyst was computed. net9.0-macos net9.0-macos was computed. net9.0-tvos net9.0-tvos was computed. net9.0-windows net9.0-windows was computed. net10.0 net10.0 is compatible. net10.0-android net10.0-android was computed. net10.0-browser net10.0-browser was computed. net10.0-ios net10.0-ios was computed. net10.0-maccatalyst net10.0-maccatalyst was computed. net10.0-macos net10.0-macos was computed. net10.0-tvos net10.0-tvos was computed. net10.0-windows net10.0-windows was computed. |
This package is not used by any NuGet packages.
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated | |
|---|---|---|---|
| 6.0.5 | 94 | 6/12/2026 | |
| 6.0.4 | 100 | 6/7/2026 | |
| 6.0.3 | 98 | 6/6/2026 | |
| 6.0.2 | 96 | 6/5/2026 | |
| 6.0.1 | 99 | 6/4/2026 | |
| 6.0.0 | 106 | 5/31/2026 | |
| 5.0.6 | 110 | 5/24/2026 | |
| 5.0.5 | 112 | 5/22/2026 | |
| 5.0.4 | 105 | 5/21/2026 | |
| 5.0.3 | 137 | 4/29/2026 | |
| 5.0.2 | 123 | 4/28/2026 | |
| 5.0.1 | 172 | 4/23/2026 | 5.0.1 is deprecated because it has critical bugs. |
| 5.0.0 | 178 | 4/22/2026 | 5.0.0 is deprecated because it has critical bugs. |
| 4.9.4 | 4,782 | 4/7/2026 | |
| 4.9.3 | 131 | 4/6/2026 | |
| 4.9.2 | 385 | 4/2/2026 | |
| 4.9.1 | 1,732 | 3/28/2026 | |
| 4.9.0 | 124 | 3/26/2026 | |
| 4.8.9 | 125 | 3/26/2026 | |
| 4.8.8 | 125 | 3/25/2026 |