diff --git a/.gitignore b/.gitignore index f52518365..a18ab64b0 100644 --- a/.gitignore +++ b/.gitignore @@ -195,3 +195,6 @@ FakesAssemblies/ # FAKE /.fake + +# JetBrains Rider +/.idea diff --git a/Documentation/.gitignore b/Documentation/.gitignore deleted file mode 100644 index 9c5f57827..000000000 --- a/Documentation/.gitignore +++ /dev/null @@ -1 +0,0 @@ -_build \ No newline at end of file diff --git a/Documentation/Aggregates.rst b/Documentation/Aggregates.rst deleted file mode 100644 index fa71c0d16..000000000 --- a/Documentation/Aggregates.rst +++ /dev/null @@ -1,114 +0,0 @@ -.. _aggregates: - -Aggregates -========== - -Initially before you can create a aggregate, you need to create its -identity. You can create your own implementation by implementing the -``IIdentity`` interface or you can use a base class ``Identity<>`` that -EventFlow provides, like this. - -.. code-block:: c# - - public class TestId : Identity - { - public TestId(string value) : base(value) - { - } - } - -The :ref:`Identity\<\> ` value object -provides generic functionality to create and validate aggregate root -IDs. Please read the documentation regarding the bundled ``Identity<>`` -type as it provides several useful features, e.g. several different -schemes for ID generation, one that minimizes MSSQL database -fragmentation. - -Next, to create a new aggregate, simply inherit from -``AggregateRoot<,>`` like this, making sure to pass test aggregate own -type as the first generic argument and the identity as the second. - -.. code-block:: c# - - public class TestAggregate : AggregateRoot - { - public TestAggregate(TestId id) - : base(id) - { - } - } - - -.. _events: - -Events ------- - -In an event source system like EventFlow, aggregate root data are stored -on events. - -.. code-block:: c# - - public class PingEvent : AggregateEvent - { - public string Data { get; } - - public PingEvent(string data) - { - Data = data; - } - } - -Please make sure to read the section on :ref:`value objects and -events ` for some important notes on creating -events. - -Emitting events ---------------- - -In order to emit an event from an aggregate, call the ``protected`` -``Emit(...)`` method which applies the event and adds it to the list of -uncommitted events. - -.. code-block:: c# - - public void Ping(string data) - { - // Fancy domain logic here that validates aggregate state... - - if (string.IsNullOrEmpty(data)) - { - throw DomainError.With("Ping data empty") - } - - Emit(new PingEvent(data)) - } - -Remember not to do any changes to the aggregate with the these methods, -as the state is only stored through events. - - -.. _aggregates_applying_events: - -Applying events ---------------- - -Currently EventFlow has three methods of applying events to the -aggregate when emitted or loaded from the event store. Which you choose -is up to you, implementing ``IEmit`` is the most convenient, -but will expose public ``Apply`` methods. - -- Create a method called ``Apply`` that takes the event as argument. To - get the method signature right, implement the ``IEmit`` on - your aggregate. This is the default fallback and you will get an - exception if no other strategies are configured. Although you *can* - implement ``IEmit``, its optional, the ``Apply`` methods - can be ``protected`` or ``private`` -- Create a state object by inheriting from ``AggregateState<,,>`` and - registering using the protected ``Register(...)`` in the aggregate - root constructor -- Register a specific handler for a event using the protected - ``Register(e => Handler(e))`` from within the constructor -- Register an event applier using - ``Register(IEventApplier eventApplier)``, which could be a e.g. state - object diff --git a/Documentation/Commands.rst b/Documentation/Commands.rst deleted file mode 100644 index 4f8619646..000000000 --- a/Documentation/Commands.rst +++ /dev/null @@ -1,157 +0,0 @@ -.. _commands: - -Commands -======== - -Commands are the basic value objects, or models, that represent write -operations that you can perform in your domain. - -As an example, one might implement create this command for updating user -passwords. - -.. code-block:: c# - - public class UserUpdatePasswordCommand : Command - { - public Password NewPassword { get; } - public Password OldPassword { get; } - - public UserUpdatePasswordCommand( - UserId id, - Password newPassword, - Password oldPassword) - : base(id) - { - Username = username; - Password = password; - } - } - -Note that the ``Password`` class is merely a value object created to -hold the password and do basic validation. Read the article regarding -:ref:`value objects ` for more information. Also, you -don't have to use the default EventFlow ``Command<,>`` implementation, -you can create your own, it merely have to implement the ``ICommand<,>`` -interface. - -A command by itself doesn't do anything and will throw an exception if -published. To make a command work, you need to implement one (and only -one) command handler which is responsible for invoking the aggregate. - -.. code-block:: c# - - public class UserUpdatePasswordCommandHandler : - CommandHandler - { - public override Task ExecuteAsync( - UserAggregate aggregate, - UserUpdatePasswordCommand command, - CancellationToken cancellationToken) - { - aggregate.UpdatePassword( - command.OldPassword, - command.NewPassword); - - return Task.FromResult(0); - } - } - -Ensure idempotency ------------------- - -Detecting duplicate operations can be hard, especially if you have a -distributed application, or simply a web application. Consider the -following simplified scenario. - -1. The user wants to change his password -2. The user fills in the "change password form" -3. As user is impatient, or by accident, the user submits the for twice -4. The first web request completes and the password is changed. However, - as the browser is waiting on the first web request, this result is - ignored -5. The second web request throws a domain error as the "old password" - doesn't match as the current password has already been changed -6. The user is presented with a error on the web page - -Handling this is simple, merely ensure that the aggregate is idempotent -is regards to password changes. But instead of implementing this -yourself, EventFlow has support for it and its simple to utilize and is -done per command. - -To use the functionality, merely ensure that commands that represent the -same operation has the same ``ISourceId`` which implements ``IIdentity`` -like the example blow. - -.. code-block:: c# - - public class UserUpdatePasswordCommand : Command - { - public Password NewPassword { get; } - public Password OldPassword { get; } - - public UserCreateCommand( - UserId id, - ISourceId sourceId, - Password newPassword, - Password oldPassword) - : base(id, sourceId) - { - Username = username; - Password = password; - } - } - -Note the use of the other ``protected`` constructor of ``Command<,>`` -that takes a ``ISourceId`` in addition to the aggregate root identity. - -If a duplicate command is detected, a ``DuplicateOperationException`` is -thrown. The application could then ignore the exception or report the -problem to the end user. - -The default ``ISourceId`` history size of the aggregate root, is ten. -But it can be configured using the ``SetSourceIdHistory(...)`` that must -be called from within the aggregate root constructor. - -Easier ISourceId calculation -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Ensuring the correct calculation of the command ``ISourceId`` can be -somewhat cumbersome, which is why EventFlow provides another base -command you can use, the ``DistinctCommand<,>``. By using the -``DistinctCommand<,>`` you merely have to implement the -``GetSourceIdComponents()`` and providing the ``IEnumerable`` -that makes the command unique. The bytes is used to create a -deterministic GUID to be used as an ``ISourceId``. - -.. code-block:: c# - - public class UserUpdatePasswordCommand : - DistinctCommand - { - public Password NewPassword { get; } - public Password OldPassword { get; } - - public UserUpdatePasswordCommand( - UserId id, - Password newPassword, - Password oldPassword) - : base(id) - { - Username = username; - Password = password; - } - - protected override IEnumerable GetSourceIdComponents() - { - yield return NewPassword.GetBytes(); - yield return OldPassword.GetBytes(); - } - } - -The ``GetBytes()`` merely returns the ``Encoding.UTF8.GetBytes(...)`` of -the password. - -.. CAUTION:: - - Don't use the ``GetHashCode()``, as the implementation - is different for e.g. ``string`` on 32 bit and 64 bit .NET. diff --git a/Documentation/Configuration.rst b/Documentation/Configuration.rst deleted file mode 100644 index 36833ee2c..000000000 --- a/Documentation/Configuration.rst +++ /dev/null @@ -1,16 +0,0 @@ -.. _configuration: - -Configuration -============= - -EventFlow can be configured by invoking ``eventFlowOptions.Configure(c => ...)```, or -by providing a custom implementation of ``IEventFlowConfiguration``. - -Each configuration is described below. The default values should be good enough -for most production setups. - -.. literalinclude:: ../Source/EventFlow/Configuration/IEventFlowConfiguration.cs - :linenos: - :dedent: 4 - :language: c# - :lines: 28-65 diff --git a/Documentation/Customize.rst b/Documentation/Customize.rst deleted file mode 100644 index 59aed9626..000000000 --- a/Documentation/Customize.rst +++ /dev/null @@ -1,66 +0,0 @@ -Customize -========= - -If you are looking for how to configure EventFlow, look at the -:ref:`configuration ` documentation. - -When ever EventFlow doesn't meet your needs, e.g. if you want to collect -statistics on each command execution time, you can customize EventFlow. - -Basically EventFlow relies on an IoC container to allow developers to -customize the different parts of EventFlow. - -Note: Read the section "Changing IoC container" for details on how to -change the IoC container used if you have specific needs like e.g. -integrating EventFlow into an Owin application. - -You have two options for when you want to customize EventFlow - -- Decorate an implementation -- Replace an implementation - - -.. _ioc-decorator: - -Decorating implementations --------------------------- - -In the case of collecting statistics, you might want to wrap the -existing ``ICommandBus`` with a decorator class the can collect -statistics on command execution times. - -.. code-block:: c# - - void ConfigureEventFlow() - { - var resolver = EventFlowOptions.new - .RegisterServices(DecorateCommandBus) - ... - .CreateResolver(); - } - - void DecorateCommandBus(IServiceRegistration sr) - { - sr.Decorate((r, cb) => new StatsCommandBus(sb)); - } - - class StatsCommandBus : ICommandBus - { - private readonly _internalCommandBus; - - public StatsCommandBus(ICommandBus commandBus) - { - _internalCommandBus = commandBus; - } - - // Here follow implementations of ICommandBus that call the - // internal command bus and logs statistics - ... - } - -Registering new implementations -------------------------------- - -The more drastic step is to completely replace an implementation. For -this you use the ``Register(...)`` and related methods on -``IServiceRegistration`` instead of the ``Decorate(...)`` method. diff --git a/Documentation/DosAndDonts.rst b/Documentation/DosAndDonts.rst deleted file mode 100644 index 24e83817d..000000000 --- a/Documentation/DosAndDonts.rst +++ /dev/null @@ -1,63 +0,0 @@ -.. _dos-and-donts: - -Do's and don'ts -=============== - -Whenever creating an application that uses CQRS+ES there are several -things you need to keep in mind to make it easier and minimize the -potential bugs. This guide will give you some details on typical -problems and how EventFlow can help you minimize the risk. - -Business rules --------------- - -Specifications -^^^^^^^^^^^^^^^^^^ - -`Consider` moving complex business rules to :ref:`specifications `. -This eases both readability, testability and re-use. - - -Events ------- - -Produce clean JSON -^^^^^^^^^^^^^^^^^^ - -Make sure that when your aggregate events are JSON serialized, they -produce clean JSON as it makes it easier to work with and enable you to -easier deserialize the events in the future. - -- No type information -- No hints of value objects (see :ref:`value objects `) - -Here's an example of good clean event JSON produced from a create user -event. - -.. code:: json - - { - "Username": "root", - "PasswordHash": "1234567890ABCDEF", - "EMail": "root@example.org", - } - -Keep old event types -^^^^^^^^^^^^^^^^^^^^ - -Keep in mind, that you need to keep the event types in your code for as -long as these events are in the event source, which in most cases are -*forever* as storage is cheap and information, i.e., your domain events, -are expensive. - -However, you should still clean your code, have a look at how you can -:ref:`upgrade and version your events ` for details on -how EventFlow supports you in this. - - -Subscribers and out of order events -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Be very careful if aggregates emits multiple events for a single command, -subscribers will almost certainly -:ref:`receive these out of order `. \ No newline at end of file diff --git a/Documentation/EventStore.rst b/Documentation/EventStore.rst deleted file mode 100644 index af7ea5e96..000000000 --- a/Documentation/EventStore.rst +++ /dev/null @@ -1,99 +0,0 @@ -.. _eventstores: - -Event stores -============ - -By default EventFlow uses a in-memory event store. But EventFlow provides -support for alternatives. - -- :ref:`In-memory ` (for test) -- :ref:`Microsoft SQL Server ` -- :ref:`Files ` (for test) - - -.. _eventstore-inmemory: - -In-memory ---------- - -.. IMPORTANT:: - - In-memory event store shouldn't be used for production environments, only for tests. - - -Using the in-memory event store is easy as its enabled by default, no need -to do anything. - - -.. _eventstore-mssql: - -MSSQL event store ------------------ - -See :ref:`MSSQL setup ` for details on how to get started -using Microsoft SQL Server in EventFlow. - -Configure EventFlow to use MSSQL as event store, simply add the -``UseMssqlEventStore()`` as shown here. - -.. code-block:: c# - - IRootResolver rootResolver = EventFlowOptions.New - ... - .UseMssqlEventStore() - ... - .CreateResolver(); - - -Create and migrate required MSSQL databases -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Before you can use the MSSQL event store, the required database and -tables must be created. The database specified in your MSSQL connection -will *not* be automatically created, you have to do this yourself. - -To make EventFlow create the required tables, execute the following -code. - -.. code-block:: c# - - var msSqlDatabaseMigrator = rootResolver.Resolve(); - EventFlowEventStoresMsSql.MigrateDatabase(msSqlDatabaseMigrator); - - -You should do this either on application start or preferably upon -application install or update, e.g., when the web site is installed. - -.. IMPORTANT:: - - If you utilize user permission in your application, then you - need to grant the event writer access to the user defined table type - ``eventdatamodel_list_type``. EventFlow uses this type to pass entire - batches of events to the database. - - -.. _eventstore-files: - -Files ------ - -.. IMPORTANT:: - - Files event shouldn't be used for production environments, only for tests. - - -The file based event store is useful if you have a set of events that represents -a certain scenario and would like to create a test that verifies that the domain -handles it correctly. - -To use the file based event store, simply invoke ``.UseFilesEventStore`("...")`` -with the path containing the files. - -.. code-block:: c# - - var storePath = @"c:\eventstore" - var rootResolver = EventFlowOptions.New - ... - .UseFilesEventStore(FilesEventStoreConfiguration.Create(storePath)) - ... - .CreateResolver(); diff --git a/Documentation/EventUpgrade.rst b/Documentation/EventUpgrade.rst deleted file mode 100644 index e3b088ff9..000000000 --- a/Documentation/EventUpgrade.rst +++ /dev/null @@ -1,84 +0,0 @@ -.. _event-upgrade: - -Event upgrade -============= - -At some point you might find the need to replace a event with zero or -more events. Some use cases might be - -- A previous application version introduced a domain error in the form - of a wrong event being emitted from the aggregate -- Domain has changed, either from a change in requirements or simply - from a better understanding of the domain - -EventFlow event upgraders are invoked whenever the event stream is -loaded from the event store. Each event upgrader receives the entire -event stream one event at a time. - -A new instance of a event upgrader is created each time an aggregate is -loaded. This enables you to store information from previous events on -the upgrader instance to be used later, e.g. to determine an action to -take on a event or provide additional information for a new event. - -Note that the *ordering* of event upgraders is important as you might -implement two upgraders, one upgrade a event from V1 to V2 and then -another upgrading V2 to V3. EventFlow orders the event upgraders by name -before starting the event upgrade. - -.. CAUTION:: - - Be careful when working with event upgraders that return zero or more - than one event, as this have an influence on the aggregate version and - you need to make sure that the aggregate sequence number on upgraded - events are valid in regard to the aggregate history. - - -Example - removing a damaged event ----------------------------------- - -To remove an event, simply check and only return the event if its no the -event you want to remove. - -.. code-block:: c# - - public class DamagedEventRemover : IEventUpgrader - { - public IEnumerable> Upgrade( - IDomainEvent domainEvent) - { - var damagedEvent = domainEvent as IDomainEvent; - if (damagedEvent == null) - { - yield return domainEvent; - } - } - } - -Example - replace event ------------------------ - -To one event to another, you should use the -``IDomainEventFactory.Upgrade`` to help migrate meta data and create the -new event. - -.. code-block:: c# - - public class UpgradeMyEventV1ToMyEventV2 : IEventUpgrader - { - private readonly IDomainEventFactory _domainEventFactory; - - public UpgradeTestEventV1ToTestEventV2(IDomainEventFactory domainEventFactory) - { - _domainEventFactory = domainEventFactory; - } - - public IEnumerable> Upgrade( - IDomainEvent domainEvent) - { - var myEventV1 = domainEvent as IDomainEvent; - yield return myEventV1 == null - ? domainEvent - : _domainEventFactory.Upgrade( - domainEvent, new MyEventV2()); - } - } diff --git a/Documentation/FAQ.rst b/Documentation/FAQ.rst deleted file mode 100644 index ce4f7ab05..000000000 --- a/Documentation/FAQ.rst +++ /dev/null @@ -1,51 +0,0 @@ -.. _faq: - -FAQ - frequently asked questions -================================ - -How can I ensure that only specific users can execute commands? -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -You should implement a *decorator* for the ``ICommandBus`` that does the -authentication. Have a look at the :ref:`decorator ` documentation -to see how this can be achieved. - - -Why isn't there a "global sequence number" on domain events? -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -While this is easy to support in some event stores like MSSQL, it -doesn't really make sense from a domain perspective. Greg Young also has -this to say on the subject: - - Order is only assured per a handler within an aggregate root - boundary. There is no assurance of order between handlers or between - aggregates. Trying to provide those things leads to the dark side. > - `Greg - Young `__ - - -Why doesn't EventFlow have a unit of work concept? -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Short answer, you shouldn't need it. But Mike has a way better answer: - - In the Domain, everything flows in one direction: forward. When - something bad happens, a correction is applied. The Domain doesn't - care about the database and UoW is very coupled to the db. In my - opinion, it's a pattern which is usable only with data access - objects, and in probably 99% of the cases you won't be needing it. - As with the Singleton, there are better ways but everything depends - on proper domain design. > `Mike - Mogosanu `__ - -If your case falls within the 1% case, write an decorator for the -``ICommandBus`` that starts a transaction, use MSSQL as event store and -make sure your read models are stored in MSSQL as well. - - -Why are subscribers are receiving events out of order? -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -It might be your aggregates are emitting multiple events. Read about -:ref:`subscribers and out of order events `. diff --git a/Documentation/GettingStarted.rst b/Documentation/GettingStarted.rst deleted file mode 100644 index 98774ada4..000000000 --- a/Documentation/GettingStarted.rst +++ /dev/null @@ -1,297 +0,0 @@ -.. _getting-started: - -Getting started -=============== - -Initializing EventFlow always start with an ``EventFlowOptions.New`` as this -performs the initial bootstrap and starts the fluent configuration API. The -very minimum initialization of EventFlow can be done in a single line, but -wouldn't serve any purpose as no domain has been configured. - -.. code-block:: c# - - var resolver = EventFlowOptions.New.CreateResolver(); - - -The above line does configures several important defaults - -- Custom internal IoC container -- In-memory :ref:`event store ` -- Console logger -- A "null" snapshot store, that merely writes a warning if used (no need to - do anything before going to production if you aren't planning to use - snapshots) -- And lastly, default implementations of all the internal parts of EventFlow - -.. IMPORTANT:: - Before using EventFlow in a production environment, you should configure an - alternative **event store**, an alternative **IoC container** and another - **logger** that sends log messages to your production log store. - - - :ref:`IoC container ` - - :ref:`Log ` - - :ref:`Event store ` - - :ref:`Snapshots ` - - -To start using EventFlow, a domain must be configure which consists of the -following parts - -- :ref:`Aggregate ` -- :ref:`Aggregate identity ` -- :ref:`Aggregate events ` -- :ref:`Commands and command handlers ` (optional, but highly recommended) - -In addition to the above, EventFlow provides several optional features. Whether -or not these features are utilized, depends on the application in which -EventFlow is used. - -- :ref:`Read models ` -- :ref:`Subscribers ` -- :ref:`Event upgraders ` -- :ref:`Queries ` -- :ref:`Jobs ` -- :ref:`Snapshots ` -- :ref:`Sagas ` -- :ref:`Metadata providers ` - -Example application -------------------- - -To get started, we start with our entire example application which consists of -one of each of the required parts: aggregate, event, aggregate identity, command -and a command handler. After we will go through the individual parts created. - -.. NOTE:: - The example code provided here is located within the EventFlow code base - exactly as shown, so if you would like to debug and step through the - entire flow, checkout the code and execute the ``GettingStartedExample`` - test. - - -All classes create for the example application are prefixed with ``Example``. - -.. literalinclude:: ../Source/EventFlow.Tests/Documentation/GettingStarted/ExampleTests.cs - :linenos: - :dedent: 12 - :language: c# - :lines: 41-76 - -The above example publishes the ``ExampleCommand`` to the aggregate with the -``exampleId`` identity with the magical value of ``42``. After the command has -been published, the accompanying read model ``ExampleReadModel`` is fetched -and we verify that the magical number has reached it. - -During the execution of the example application, a single event is emitted and -stored in the in-memory event store. The JSON for the event is shown here. - -.. code-block:: json - - { - "MagicNumber": 42 - } - -The event data itself is straightforward as it is merely the JSON serialization of -an instance of the type ``ExampleEvent`` with the value we defined. A bit more -interesting is the metadata that EventFlow stores along the event, which is -used by the EventFlow event store. - -.. code-block:: json - - { - "timestamp": "2016-11-09T20:56:28.5019198+01:00", - "aggregate_sequence_number": "1", - "aggregate_name": "ExampleAggrenate", - "aggregate_id": "example-c1d4a2b1-c75b-4c53-ae44-e67ee1ddfd79", - "event_id": "event-d5622eaa-d1d3-5f57-8023-4b97fabace90", - "timestamp_epoch": "1478721389", - "batch_id": "52e9d7e9-3a98-44c5-926a-fc416e20556c", - "source_id": "command-69176516-07b7-4142-beaf-dba82586152c", - "event_name": "example", - "event_version": "1" - } - -All the built-in meta data is available on each instance of ``IDomainEvent<,,>``, -which is accessible from event handlers for e.g. read models or subscribers. It -also possible create your own :ref:`meta data providers ` -or add additional EventFlow built-in providers as needed. - - -Aggregate identity ------------------- - -The aggregate ID is in EventFlow represented as a value objected that inherits -from the ``IIdentity`` interface. You can provide your own implementation, but -EventFlow provides a convenient implementation that will suit most needs. Be -sure to read the read the section about the :ref:`Identity\<\> ` class -to get details on how to use it. - -For our example application we use the built-in class making the implementation -very simple. - -.. literalinclude:: ../Source/EventFlow.Tests/Documentation/GettingStarted/ExampleId.cs - :linenos: - :dedent: 4 - :language: c# - :lines: 29-34 - - -Aggregate ---------- - -Now we'll take a look at the ``ExampleAggregate``. Its rather simple as the -only thing it can, is apply the magic number once. - -.. literalinclude:: ../Source/EventFlow.Tests/Documentation/GettingStarted/ExampleAggregate.cs - :linenos: - :dedent: 4 - :language: c# - :lines: 30-55 - -Be sure to read the section on :ref:`aggregates ` to get all the -details right, but for now the most important thing to note, is that the state -of the aggregate (updating the ``_magicNumber`` variable) happens in the -``Apply(ExampleEvent)`` method. This is the event sourcing part of EventFlow in -effect. As state changes are only saved as events, mutating the aggregate state -must happen in such a way that the state changes are replayed the next the -aggregate is loaded. EventFlow has a :ref:`set of different approaches ` -that you can select from, but in this example we use the `Apply` methods as -they are the simplest. - -The ``ExampleAggregate`` exposes the ``SetMagicNumer(int)`` method, which -is used to expose the business rules for changing the magic number. If the -magic number hasn't been set before, the event ``ExampleEvent`` is emitted -and the aggregate state is mutated. - -.. IMPORTANT:: - The ``Apply(ExampleEvent)`` is invoked by the ``Emit(...)`` method, so - after the event has been emitted, the aggregate state has changed. - - -Event ------ - -Next up is the event which represents some that **has** happend in our domain. -In this example, its merely that some magic number has been set. Normally -these events should have a really, really good name and represent something in the -ubiquitous language for the domain. - -.. literalinclude:: ../Source/EventFlow.Tests/Documentation/GettingStarted/ExampleEvent.cs - :linenos: - :dedent: 4 - :language: c# - :lines: 30-41 - -We have applied the ``[EventVersion("example", 1)]`` to our event, marking it -as the ``example`` event version ``1``, which directly corresponds to the -``event_name`` and ``event_version`` from the meta data store along side the -event mentioned. The information is used by EventFlow to tie name and version to -a specific .NET type. - -.. IMPORTANT:: - Even though the using the ``EventVersion`` attribute is optional, its - **highly recommended**. EventFlow will infer the information if it isn't - provided and thus making it vulnerable to e.g. type renames. - -.. IMPORTANT:: - Once have aggregates in your production environment that have emitted - an event, you should never change the .NET implementation. You can deprecate - it, but you should never change the type or the data stored in the event - store. - - -Command -------- - -Commands are the entry point to the domain and if you remember from the example -application, they are published using the ``ICommandBus`` as shown here. - -.. literalinclude:: ../Source/EventFlow.Tests/Documentation/GettingStarted/ExampleTests.cs - :linenos: - :dedent: 16 - :language: c# - :lines: 57-62 - -In EventFlow commands are simple value objects that merely how the arguments for -the command execution. All commands implement the ``ICommand<,>`` interface, but -EventFlow provides an easy-to-use base class that you can use. - -.. literalinclude:: ../Source/EventFlow.Tests/Documentation/GettingStarted/ExampleCommand.cs - :linenos: - :dedent: 4 - :language: c# - :lines: 29-42 - -A command doesn't do anything without a command handler. In fact, EventFlow -will throw an exception if a command doesn't have exactly **one** command -handler registered. - - -Command handler ---------------- - -The command handler provides the glue between the command, the aggregate and -the IoC container as it defines how a command is executed. Typically they are -rather simple, but they could contain more complex logic. How much is up to you. - -.. literalinclude:: ../Source/EventFlow.Tests/Documentation/GettingStarted/ExampleCommandHandler.cs - :linenos: - :dedent: 4 - :language: c# - :lines: 31-43 - -The ``ExampleCommandHandler`` in our case here merely invokes the -``SetMagicNumer`` on the aggregate. - -.. IMPORTANT:: - Everything inside the ``ExecuteAsync(...)`` method of a command handler - **may** be executed more than once if there's an optimistic concurrency - exception, i.e., something else has happened to the aggregate since it - as loaded from the event store and its therefor automatically reloaded by - EventFlow. It is therefor essential that the command handler doesn't mutate - anything other than the aggregate. - - -Read model ----------- - -If you ever need to access the data in your aggregates efficiently, its important -that :ref:`read models ` are used. Loading aggregates from the -event store takes time and its impossible to query for e.g. aggregates that have -a specific value in its state. - -In our example we merely use the built-in in-memory read model store. Its useful -in many cases, e.g. executing automated domain tests in an CI build. - -.. literalinclude:: ../Source/EventFlow.Tests/Documentation/GettingStarted/ExampleReadModel.cs - :linenos: - :dedent: 4 - :language: c# - :lines: 30-43 - -Notice the ``IDomainEvent domainEvent`` -argument, its merely a wrapper around the specific event we implemented -earlier. The ``IDomainEvent<,,>`` provides additional information, e.g. any -meta data store along side the event. - -The main difference between the event instance emitted in the aggregate and the -instance wrapped here, is that the event has been committed to the event store. - - -Next steps ----------- - -Although the implementation in this guide enables you to create a complete -application, there are several topics that are recommended as next steps. - -- Read the :ref:`dos and donts ` section -- Use :ref:`value objects ` to produce cleaner JSON -- If your application need to act on an emitted event, create a - :ref:`subscriber ` -- Check the :ref:`configuration ` to make sure everything - is as you would like it -- Setup a persistent event store using e.g. - :ref:`Microsoft SQL Server ` -- Create :ref:`read models ` for efficient querying -- Consider the use of :ref:`specifications ` to ease - creation of business rules diff --git a/Documentation/Identity.rst b/Documentation/Identity.rst deleted file mode 100644 index 7d208529e..000000000 --- a/Documentation/Identity.rst +++ /dev/null @@ -1,75 +0,0 @@ -.. _identity: - -Identity -======== - -The ``Identity<>`` value object provides generic functionality to create -and validate the IDs of e.g. aggregate roots. Its basically a wrapper -around a ``Guid``. - -Lets say we want to create a new identity named ``TestId`` we could do -it like this. - -.. code-block:: c# - - public class TestId : Identity - { - public TestId(string value) - : base(value) - { - } - } - -- The identity follow the form ``{class without "Id"}-{guid}`` e.g. - ``test-c93fdb8c-5c9a-4134-bbcd-87c0644ca34f`` for the above - ``TestId`` example -- The internal ``Guid`` can be generated using one of the following - methods/properties. Note that you can access the ``Guid`` factories - directly by accessing the static methods on the ``GuidFactories`` - class -- ``New``: Uses the standard ``Guid.NewGuid()`` -- ``NewDeterministic(...)``: Creates a name-based ``Guid`` using the - algorithm from `RFC 4122 `__ - §4.3, which allows identities to be generated based on known data, - e.g. an user e-mail, i.e., it always returns the same identity for - the same arguments -- ``NewComb()``: Creates a sequential ``Guid`` that can be used to e.g. - avoid database fragmentation -- A ``string`` can be tested to see if its a valid identity using the - static ``bool IsValid(string)`` method -- Any validation errors can be gathered using the static - ``IEnumerable Validate(string)`` method - -.. IMPORTANT:: - - Its very important to name the constructor argument ``value`` - as it is significant when the identity type is deserialized. - - -Here's some examples on we can use our newly created ``TestId`` - -.. code-block:: c# - - // Uses the default Guid.NewGuid() - var testId = TestId.New - -.. code-block:: c# - - // Create a namespace, put this in a constant somewhere - var emailNamespace = Guid.Parse("769077C6-F84D-46E3-AD2E-828A576AAAF3"); - - // Creates an identity with the value "test-9181a444-af25-567e-a866-c263b6f6119a" - var testId = TestId.NewDeterministic(emailNamespace, "test@example.com"); - -.. code-block:: c# - - // Creates a new identity every time, but an identity when used in e.g. - // database indexes, minimizes fragmentation - var testId = TestId.NewComb() - - -.. NOTE:: - - Be sure to read the section about - :ref:`value objects ` as the ``Identity<>`` is basically a - value object. diff --git a/Documentation/IoC.rst b/Documentation/IoC.rst deleted file mode 100644 index 89182cd81..000000000 --- a/Documentation/IoC.rst +++ /dev/null @@ -1,28 +0,0 @@ -.. _ioc-container: - -IoC container -============= - -EventFlow has a custom minimal IoC container implementation, but before -using EventFlow in a production environment, its recommended to change -to `Autofac `__ or provide another. - -Autofac -------- - -EventFlow provides the NuGet package ``EventFlow.Autofac`` that allows -you to set the internal ``ContainerBuilder`` used during EventFlow -initialization. - -Pass the ``ContainerBuilder`` to EventFlow and call -``CreateContainer()`` when configuration is done to create the -container. - -.. code-block:: c# - - var containerBuilder = new ContainerBuilder(); - - var container = EventFlowOptions.With - .UseAutofacContainerBuilder(containerBuilder) // Must be the first line! - ... - .CreateContainer(); diff --git a/Documentation/Jobs.rst b/Documentation/Jobs.rst deleted file mode 100644 index 93e3a892b..000000000 --- a/Documentation/Jobs.rst +++ /dev/null @@ -1,119 +0,0 @@ -.. _jobs: - -Jobs -==== - -A job is basically a task that you either don't want to execute in the -current context, on the current server or execute at a later time. -EventFlow provides basic functionality for jobs. - -There are areas where you might find jobs very useful, here are some -examples - -- Publish a command at a specific time in the future -- Transient error handling - -.. code-block:: c# - - var jobScheduler = resolver.Resolve(); - var job = PublishCommandJob.Create(new SendEmailCommand(id), resolver); - await jobScheduler.ScheduleAsync( - job, - TimeSpan.FromDays(7), - CancellationToken.None) - .ConfigureAwait(false); - -In the above example the ``SendEmailCommand`` command will be published -in seven days. - - -Be careful when using jobs --------------------------- - -When working with jobs, you should be aware of the following - -- The default implementation does executes the job *now*, i.e., in the - current context. To get another behavior, install e.g. - ``EventFlow.Hangfire`` to get support for scheduled jobs. Read below - for details on how to configure Hangfire -- Your jobs should serialize to JSON properly, see the section on - `value objects <./ValueObjects.md>`__ for more information -- If you use the provided ``PublishCommandJob``, make sure that your - commands serialize properly as well - - -Create your own jobs --------------------- - -To create your own jobs, your job merely needs to implement the ``IJob`` -interface and be registered in EventFlow. - -Here's an example of a job implementing ``IJob`` - -.. code-block:: c# - - [JobVersion("LogMessage", 1)] - public class LogMessageJob : IJob - { - public LogMessageJob(string message) - { - Message = message; - } - - public string Message { get; } - - public Task ExecuteAsync( - IResolver resolver, - CancellationToken cancellationToken) - { - var log = resolver.Resolve(); - log.Debug(Message); - } - } - -Note that the ``JobVersion`` attribute specifies the job name and -version to EventFlow and this is how EventFlow distinguishes between the -different job types. This makes it possible for you to reorder your -code, even rename the job type, as long as you keep the same attribute -values its considered the same job in EventFlow. If the attribute is -omitted, the name will be the type name and version will be ``1``. - -Here's how the job is registered in EventFlow. - -.. code-block:: c# - - var resolver = EventFlowOptions.new - .AddJobs(typeof(LogMessageJob)) - ... - .CreateResolver(); - -Then to schedule the job - -.. code-block:: c# - - var jobScheduler = resolver.Resolve(); - var job = new LogMessageJob("Great log message"); - await jobScheduler.ScheduleAsync( - job, - TimeSpan.FromDays(7), - CancellationToken.None) - .ConfigureAwait(false); - -Hangfire --------- - -To use `Hangfire `__ as the job scheduler, install -the NuGet package ``EventFlow.Hangfire`` and configure EventFlow to use -the scheduler like this. - -.. code-block:: c# - - var resolver = EventFlowOptions.new - .UseHangfireJobScheduler() // This line - ... - .CreateResolver(); - -.. NOTE:: - - The ``UseHangfireJobScheduler()`` doesn't do any Hangfire - configuration, but merely registers the proper scheduler in EventFlow. diff --git a/Documentation/Log.rst b/Documentation/Log.rst deleted file mode 100644 index 72f55f5bf..000000000 --- a/Documentation/Log.rst +++ /dev/null @@ -1,8 +0,0 @@ -.. _log: - -Log -=== - -The default log implementation of EventFLow logs to the console. To have another -behavior, register an implementation of ``ILog``, use the ``Log`` as a base class -to make the implementation easier. \ No newline at end of file diff --git a/Documentation/MSSQL.rst b/Documentation/MSSQL.rst deleted file mode 100644 index 5323280a5..000000000 --- a/Documentation/MSSQL.rst +++ /dev/null @@ -1,21 +0,0 @@ -.. _setup-mssql: - -Microsoft SQL Server -==================== - -To setup EventFlow Microsoft SQL Server integration, install the NuGet -package ``EventFlow.MsSql`` and add this to your EventFlow setup. - -.. code-block:: c# - - IRootResolver rootResolver = EventFlowOptions.New - .ConfigureMsSql(MsSqlConfiguration.New - .SetConnectionString(@"Server=.\SQLEXPRESS;Database=MyApp;User Id=sa;Password=???")) - ... - .CreateResolver(); - -After setting up Microsoft SQL Server support in EventFlow, you can -continue to configure it. - -- :ref:`Event store ` -- :ref:`Read model store ` diff --git a/Documentation/Makefile b/Documentation/Makefile deleted file mode 100644 index 8e761167e..000000000 --- a/Documentation/Makefile +++ /dev/null @@ -1,225 +0,0 @@ -# Makefile for Sphinx documentation -# - -# You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = sphinx-build -PAPER = -BUILDDIR = _build - -# Internal variables. -PAPEROPT_a4 = -D latex_paper_size=a4 -PAPEROPT_letter = -D latex_paper_size=letter -ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . -# the i18n builder cannot share the environment and doctrees with the others -I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . - -.PHONY: help -help: - @echo "Please use \`make ' where is one of" - @echo " html to make standalone HTML files" - @echo " dirhtml to make HTML files named index.html in directories" - @echo " singlehtml to make a single large HTML file" - @echo " pickle to make pickle files" - @echo " json to make JSON files" - @echo " htmlhelp to make HTML files and a HTML help project" - @echo " qthelp to make HTML files and a qthelp project" - @echo " applehelp to make an Apple Help Book" - @echo " devhelp to make HTML files and a Devhelp project" - @echo " epub to make an epub" - @echo " epub3 to make an epub3" - @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" - @echo " latexpdf to make LaTeX files and run them through pdflatex" - @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" - @echo " text to make text files" - @echo " man to make manual pages" - @echo " texinfo to make Texinfo files" - @echo " info to make Texinfo files and run them through makeinfo" - @echo " gettext to make PO message catalogs" - @echo " changes to make an overview of all changed/added/deprecated items" - @echo " xml to make Docutils-native XML files" - @echo " pseudoxml to make pseudoxml-XML files for display purposes" - @echo " linkcheck to check all external links for integrity" - @echo " doctest to run all doctests embedded in the documentation (if enabled)" - @echo " coverage to run coverage check of the documentation (if enabled)" - @echo " dummy to check syntax errors of document sources" - -.PHONY: clean -clean: - rm -rf $(BUILDDIR)/* - -.PHONY: html -html: - $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." - -.PHONY: dirhtml -dirhtml: - $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." - -.PHONY: singlehtml -singlehtml: - $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml - @echo - @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." - -.PHONY: pickle -pickle: - $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle - @echo - @echo "Build finished; now you can process the pickle files." - -.PHONY: json -json: - $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json - @echo - @echo "Build finished; now you can process the JSON files." - -.PHONY: htmlhelp -htmlhelp: - $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp - @echo - @echo "Build finished; now you can run HTML Help Workshop with the" \ - ".hhp project file in $(BUILDDIR)/htmlhelp." - -.PHONY: qthelp -qthelp: - $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp - @echo - @echo "Build finished; now you can run "qcollectiongenerator" with the" \ - ".qhcp project file in $(BUILDDIR)/qthelp, like this:" - @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/EventFlow.qhcp" - @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/EventFlow.qhc" - -.PHONY: applehelp -applehelp: - $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp - @echo - @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." - @echo "N.B. You won't be able to view it unless you put it in" \ - "~/Library/Documentation/Help or install it in your application" \ - "bundle." - -.PHONY: devhelp -devhelp: - $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp - @echo - @echo "Build finished." - @echo "To view the help file:" - @echo "# mkdir -p $$HOME/.local/share/devhelp/EventFlow" - @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/EventFlow" - @echo "# devhelp" - -.PHONY: epub -epub: - $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub - @echo - @echo "Build finished. The epub file is in $(BUILDDIR)/epub." - -.PHONY: epub3 -epub3: - $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 - @echo - @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." - -.PHONY: latex -latex: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo - @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." - @echo "Run \`make' in that directory to run these through (pdf)latex" \ - "(use \`make latexpdf' here to do that automatically)." - -.PHONY: latexpdf -latexpdf: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through pdflatex..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -.PHONY: latexpdfja -latexpdfja: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through platex and dvipdfmx..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -.PHONY: text -text: - $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text - @echo - @echo "Build finished. The text files are in $(BUILDDIR)/text." - -.PHONY: man -man: - $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man - @echo - @echo "Build finished. The manual pages are in $(BUILDDIR)/man." - -.PHONY: texinfo -texinfo: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo - @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." - @echo "Run \`make' in that directory to run these through makeinfo" \ - "(use \`make info' here to do that automatically)." - -.PHONY: info -info: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo "Running Texinfo files through makeinfo..." - make -C $(BUILDDIR)/texinfo info - @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." - -.PHONY: gettext -gettext: - $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale - @echo - @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." - -.PHONY: changes -changes: - $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes - @echo - @echo "The overview file is in $(BUILDDIR)/changes." - -.PHONY: linkcheck -linkcheck: - $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck - @echo - @echo "Link check complete; look for any errors in the above output " \ - "or in $(BUILDDIR)/linkcheck/output.txt." - -.PHONY: doctest -doctest: - $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest - @echo "Testing of doctests in the sources finished, look at the " \ - "results in $(BUILDDIR)/doctest/output.txt." - -.PHONY: coverage -coverage: - $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage - @echo "Testing of coverage in the sources finished, look at the " \ - "results in $(BUILDDIR)/coverage/python.txt." - -.PHONY: xml -xml: - $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml - @echo - @echo "Build finished. The XML files are in $(BUILDDIR)/xml." - -.PHONY: pseudoxml -pseudoxml: - $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml - @echo - @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." - -.PHONY: dummy -dummy: - $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy - @echo - @echo "Build finished. Dummy builder generates no files." diff --git a/Documentation/Metadata.rst b/Documentation/Metadata.rst deleted file mode 100644 index 405d3afb3..000000000 --- a/Documentation/Metadata.rst +++ /dev/null @@ -1,75 +0,0 @@ -.. _metadata-providers: - -Metadata -======== - -Metadata is all the "additional" information that resides with a emitted -event, some of which is required information. - -In EventFlow metadata is merely a ``IEnumerable`` of -``KeyValuePair`` for which each is a metadata entry. - -Out of the box these metadata keys are added to each aggregate event. - -- ``event_name`` and ``event_version`` - A name and version for the - event which is used during event deserialization. -- ``timestamp`` - A ``DateTimeOffset`` for when the event was emitted - from the aggregate. -- ``aggregate_sequence_number`` - The version the aggregate was after - the event was emitted, e.g. ``1`` for the very first event emitted. - - -.. _metadata-providers-custom: - -Custom metadata provider ------------------------- - -If you require additional information to be stored along with each -event, then you can implement the ``IMetadataProvider`` interface and -register the class using e.g. ``.AddMetadataProvider(...)`` on -``EventFlowOptions``. - -Additional built-in providers ------------------------------ - -EventFlow ships with a collection of ready-to-use providers in some of -its NuGet packages. - -EventFlow -~~~~~~~~~ - -- **AddEventTypeMetadataProvider** -- ``event_type_assembly_name`` - Assembly name of the assembly - containing the event -- ``event_type_assembly_version`` - Assembly version of the assembly - containing the event -- ``event_type_fullname`` - Full name of the event corresponding to - ``Type.FullName`` for the aggregate event type. -- **AddGuidMetadataProvider** -- ``guid`` - A new ``Guid`` for each event. -- **AddMachineNameMetadataProvider** -- ``environment_machinename`` - Adds the machine name handling the - event from ``Environment.MachineName`` - -EventFlow.Owin -~~~~~~~~~~~~~~ - -- **AddRequestHeadersMetadataProvider** -- ``request_header[HEADER]`` - Adds all headers from the OWIN request - as metadata, each as a separate entry for which ``HEADER`` in the is - replace with the name of the header. E.g. the - ``request_header[Connection]`` might contain the value - ``Keep-Alive``. -- **AddUriMetadataProvider** -- ``request_uri`` - OWIN request URI. -- ``request_method`` - OWIN request method. -- **AddUserHostAddressMetadataProvider** -- ``user_host_address`` - The provider tries to find the correct user - host address by inspecting request headers, i.e., if you have a load - balancer in front of your application, then the request IP is not the - real user address, but the load balancer should send the user IP as a - header. -- ``user_host_address_source_header`` - The header for of which the - user host address was taken. -- ``remote_ip_address`` - The remote IP address. Note that this might - be the IP address of your load balancer. diff --git a/Documentation/Queries.rst b/Documentation/Queries.rst deleted file mode 100644 index f2e2d4802..000000000 --- a/Documentation/Queries.rst +++ /dev/null @@ -1,81 +0,0 @@ -.. _queries: - -Queries -======= - -Creating queries in EventFlow is simple. - -First create a value object that contains the data required for the -query. In this example we want to search for users based on their -username. - -.. code-block:: c# - - public class GetUserByUsernameQuery : IQuery - { - public string Username { get; } - - public GetUserByUsernameQuery(string username) - { - Username = username; - } - } - -Next create a query handler that implements how the query is processed. - -.. code-block:: c# - - public class GetUserByUsernameQueryHandler : - IQueryHandler - { - private IUserReadModelRepository _userReadModelRepository; - - public GetUserByUsernameQueryHandler( - IUserReadModelRepository userReadModelRepository) - { - _userReadModelRepository = userReadModelRepository; - } - - Task ExecuteQueryAsync( - GetUserByUsernameQuery query, - CancellationToken cancellationToken) - { - return _userReadModelRepository.GetByUsernameAsync( - query.Username, - cancellationToken) - } - } - -Last step is to register the query handler in EventFlow. Here we show -the simple, but cumbersome version, you should use one of the overloads -that scans an entire assembly. - -.. code-block:: c# - - ... - EventFlowOptions.New - .AddQueryHandler() - ... - -Then in order to use the query in your application, you need a reference -to the ``IQueryProcessor``, which in our case is stored in the -``_queryProcessor`` field. - -.. code-block:: c# - - ... - var user = await _queryProcessor.ProcessAsync( - new GetUserByUsernameQuery("root") - cancellationToken) - .ConfigureAwait(false); - ... - -Queries shipped with EventFlow ------------------------------- - -- ``ReadModelByIdQuery``: Supported by both the in-memory - and MSSQL read model stores automatically as soon as you define the - read model use using the EventFlow options for that store -- ``InMemoryQuery``: Takes a ``Predicate`` and - returns ``IEnumerable``, making it possible to search all - your in-memory read models based on any predicate diff --git a/Documentation/RabbitMQ.rst b/Documentation/RabbitMQ.rst deleted file mode 100644 index d5d1408bd..000000000 --- a/Documentation/RabbitMQ.rst +++ /dev/null @@ -1,24 +0,0 @@ -.. _setup-rabbitmq: - -RabbitMQ -======== - -To setup EventFlow RabbitMQ_ integration, install the NuGet package -``EventFlow.RabbitMQ`` and add this to your EventFlow setup. - -.. code-block:: c# - - var uri = new Uri("amqp://localhost"); - - var resolver = EventFlowOptions.with - .PublishToRabbitMq(RabbitMqConfiguration.With(uri)) - ... - .CreateResolver(); - - -After setting up RabbitMQ support in EventFlow, you can continue to configure it. - -- :ref:`Publish all domain events ` - - -.. _RabbitMQ: https://www.rabbitmq.com/ diff --git a/Documentation/ReadStores.rst b/Documentation/ReadStores.rst deleted file mode 100644 index e5eec5a75..000000000 --- a/Documentation/ReadStores.rst +++ /dev/null @@ -1,224 +0,0 @@ -.. _read-stores: - -Read model stores -================= - -In order to create query handlers that perform and enable them search -across multiple fields, read models or projections are used. - -To get started you can use the built-in in-memory read model store, but -EventFlow supports a few others as well. - -- :ref:`In-memory ` -- :ref:`Microsoft SQL Server ` -- :ref:`Elasticsearch ` - - -Creating read models --------------------- - -Read models are a flattened view of a subset or all aggregate domain -events created specifically for efficient queries. - -Here's a simple example of how a read model for doing searches for -usernames could look. The read model handles the ``UserCreated`` domain -event to get the username and user ID. - -.. code-block:: c# - - public class UserReadModel : IReadModel, - IAmReadModelFor - { - public string UserId { get; set; } - public string Username { get; set; } - - public void Apply( - IReadModelContext context, - IDomainEvent domainEvent) - { - UserId = domainEvent.AggregateIdentity.Value; - Username = domainEvent.AggregateEvent.Username.Value; - } - } - -The read model applies all ``UserCreated`` events and thereby merely saves -the latest value instead of the entire history, which makes it much easier to -store in an efficient manner. - - -Read model locators -------------------- - -Typically the ID of read models are the aggregate identity, but -sometimes this isn't the case. Here are some examples. - -- Items from a collection on the aggregate root -- Deterministic ID created from event data -- Entity within the aggregate - -To create read models in these cases, use the EventFlow concept of read -model locators, which is basically a mapping from a domain event to a -read model ID. - -As an example, consider if we could add several nicknames to a user. We -might have a domain event called ``UserNicknameAdded`` similar to this. - -.. code-block:: c# - - public class UserNicknameAdded : AggregateEvent - { - public Nickname Nickname { get; set; } - } - -We could then create a read model locator that would return the ID for -each nickname we add via the event like this. - -.. code-block:: c# - - public class UserNicknameReadModelLocator : IReadModelLocator - { - public IEnumerable GetReadModelIds(IDomainEvent domainEvent) - { - var userNicknameAdded = domainEvent as - IDomainEvent; - if (userNicknameAdded == null) - { - yield break; - } - - yield return userNicknameAdded.Nickname.Id; - } - } - -And then use a read model similar to this that represent each nickname. - -.. code-block:: c# - - public class UserNicknameReadModel : IReadModel, - IAmReadModelFor - { - public string UserId { get; set; } - public string Nickname { get; set; } - - public void Apply( - IReadModelContext context, - IDomainEvent domainEvent) - { - UserId = domainEvent.AggregateIdentity.Value; - Nickname = domainEvent.AggregateEvent.Nickname.Value; - } - } - -We could then use this nickname read model to query all the nicknames -for a given user by search for read models that have a specific -``UserId``. - - -Read store implementations --------------------------- - -EventFlow has built-in support for several different read model stores. - - -.. _read-store-inmemory: - -In-memory -~~~~~~~~~ - -The in-memory read store is easy to use and easy to configure. All read -models are stored in-memory, so if EventFlow is restarted all read -models are lost. - -To configure the in-memory read model store, simply call -``UseInMemoryReadStoreFor<>`` or ``UseInMemoryReadStoreFor<,>`` with -your read model as the generic argument. - -.. code-block:: c# - - var resolver = EventFlowOptions.New - ... - .UseInMemoryReadStoreFor() - .UseInMemoryReadStoreFor() - ... - .CreateResolver(); - - -.. _read-store-mssql: - -Microsoft SQL Server -~~~~~~~~~~~~~~~~~~~~ - -To configure the MSSQL read model store, simply call -``UseMssqlReadModel<>`` or ``UseMssqlReadModel<,>`` with your read model -as the generic argument. - -.. code-block:: c# - - var resolver = EventFlowOptions.New - ... - .UseMssqlReadModel() - .UseMssqlReadModel() - ... - .CreateResolver(); - -By convention, EventFlow uses the table named ``ReadModel-[CLASS NAME]`` -as the table to store the read models rows in. If you need to change -this, use the ``Table`` from the -``System.ComponentModel.DataAnnotations.Schema`` namespace. So in the -above example, the read model ``UserReadModel`` would be stored in a -table called ``ReadModel-UserReadModel`` unless stated otherwise. - -To allow EventFlow to find the read models stored, a single column is -required to have the ``MsSqlReadModelIdentityColumn`` attribute. This -will be used to store the read model ID. - -You should also create a ``int`` column that has the -``MsSqlReadModelVersionColumn`` attribute to tell EventFlow which column -is used to store the read model version in. - -.. IMPORTANT:: - - EventFlow expect the read model to exist, and thus any - maintenance of the database schema for the read models must be handled - before EventFlow is initialized. Or, at least before the read models are - used in EventFlow. - - -.. _read-store-elasticsearch: - -Elasticsearch -~~~~~~~~~~~~~ - -To configure the -`Elasticsearch `__ read -model store, simply call ``UseElasticsearchReadModel<>`` or -``UseElasticsearchReadModel<,>`` with your read model as the generic -argument. - -.. code-block:: c# - - var resolver = EventFlowOptions.New - ... - .ConfigureElasticsearch(new Uri("http://localhost:9200/")) - ... - .UseElasticsearchReadModel() - .UseElasticsearchReadModel() - ... - .CreateResolver(); - -Overloads of ``ConfigureElasticsearch(...)`` is available for -alternative Elasticsearch configurations. - -.. IMPORTANT:: - - Make sure to create any mapping the read model requires in Elasticsearch - *before* using the read model in EventFlow. - - -If EventFlow is requested to *purge* a specific read model, it does it -by deleting the index. Thus make sure to create one separate index per -read model. - -If you want to control the index a specific read model is stored in, -create an implementation of ``IReadModelDescriptionProvider`` and -register it in the `EventFlow IoC <./Customize.md>`__. diff --git a/Documentation/Sagas.rst b/Documentation/Sagas.rst deleted file mode 100644 index ad7ff7a2c..000000000 --- a/Documentation/Sagas.rst +++ /dev/null @@ -1,134 +0,0 @@ -.. _sagas: - -Sagas -===== - -To coordinates messages between bounded contexts and aggregates -EventFlow provides a simple saga system. - -- **Saga identity** -- **Saga** -- **Saga locator** -- **Zero or more aggregates** - -This example is based on the chapter "A Saga on Sagas" from the `CQRS -Journey `__ by -Microsoft, in which we want to model the process of placing an order. - -1. User sends command ``PlaceOrder`` to the ``OrderAggregate`` -2. ``OrderAggregate`` emits an ``OrderCreated`` event -3. ``OrderSaga`` handles ``OrderCreated`` by sending a - ``MakeReservation`` command to the ``ReservationAggregate`` -4. ``ReservationAggregate`` emits a ``SeatsReserved`` event -5. ``OrderSaga`` handles ``SeatsReserved`` by sending a ``MakePayment`` - command to the ``PaymentAggregate`` -6. ``PaymentAggregate`` emits a ``PaymentAccepted`` event -7. ``OrderSaga`` handles ``PaymentAccepted`` by emitting a - ``OrderConfirmed`` event with all the details, which via subscribers - updates the user, the ``OrderAggregate`` and the - ``ReservationAggregate`` - -Next we need an ``ISagaLocator`` which basically maps domain events to a -saga identity allowing EventFlow to find it in its store. - -In our case we will add the order ID to event metadata of all events -related to a specific order. - -.. code-block:: c# - - public class OrderSagaLocator : ISagaLocator - { - public Task LocateSagaAsync( - IDomainEvent domainEvent, - CancellationToken cancellationToken) - { - var orderId = domainEvent.Metadata["order-id"]; - var orderSagaId = new OrderSagaId($"ordersaga-{orderId}"); - - return Task.FromResult(orderSagaId); - } - } - -Alternatively the order identity could be added to every domain event -emitted from the ``OrderAggregate``, ``ReservationAggregate`` and -``PaymentAggregate`` aggregates that the ``OrderSaga`` subscribes to, -but this would depend on whether or not the order identity is part of -the ubiquitous language for your domain. - -.. code-block:: c# - - public class OrderSaga - : AggregateSaga, - ISagaIsStartedBy - { - public Task HandleAsync( - IDomainEvent domainEvent, - ISagaContext sagaContext, - CancellationToken cancellationToken) - { - // Update saga state with useful details. - Emit(new OrderStarted(/*...*/)); - - // Make the reservation - Publish(new MakeReservation(/*...*/)); - } - - public void Apply(OrderStarted e) - { - // Update our aggregate state with relevant order details - } - } - -.. IMPORTANT:: - - Even though the method for publishing commands is named - ``Publish``, the commands are only published to the command bus - **after** the aggregate has been successfully committed to the event - store (just like events). If an unexpected exception is throw by this - command publish, it should be handled by a custom implementation of - ``ISagaErrorHandler``. - - -The next few events and commands are omitted, but at last the -``PaymentAggregate`` emits its ``PaymentAccepted`` event and the saga -completes and emit the final ``OrderConfirmed`` event. - -.. code-block:: c# - - public class OrderSaga - : AggregateSaga, - ... - ISagaHandles - { - - ... - - public Task HandleAsync( - IDomainEvent domainEvent, - ISagaContext sagaContext, - CancellationToken cancellationToken) - { - Emit(new OrderConfirmed(/*...*/)) - } - - public void Apply(OrderConfirmed e) - { - // As this is the last event, we complete the saga by calling Complete() - Complete(); - } - } - -.. NOTE:: - - An ``AggregateSaga<,,>`` is only considered in its ``running`` - state if there has been an event and it hasn't been marked as completed - (by invoking the ``protected`` ``Complete()`` method on the - ``AggregateSaga<,,>``). - - -Alternative saga store ----------------------- - -By default EventFlow is configured to use event sourcing and aggregate -roots for storage of sagas. However, you can implement your own storage -system by implementing ``ISagaStore`` and registering it. diff --git a/Documentation/Snapshots.rst b/Documentation/Snapshots.rst deleted file mode 100644 index 653cec5ed..000000000 --- a/Documentation/Snapshots.rst +++ /dev/null @@ -1,251 +0,0 @@ -.. _snapshots: - -Snapshots -========= - -When working with long-lived aggregates, performance when loading -aggregates, and thereby making changes to them, becomes a real concern. -Consider aggregates that are comprised of several thousands of events, -some of which needs to go through a rigorous -`update <./EventUpgrade.md>`__ process before they are applied to the -aggregates. - -EventFlow support aggregate snapshots, which is basically a capture of -the entire aggregate state every few events. So instead of loading the -entire aggregate event history, the latest snapshot is loaded, then -applied to the aggregate and then the remaining events that wasn't -captured in the snapshot. - -To configure an aggregate root to support snapshots, inherit from -``SnapshotAggregateRoot<,,>`` and define a serializable snapshot type -that is marked with the ``ISnapshot`` interface. - -.. code-block:: c# - - [SnapshotVersion("user", 1)] - public class UserSnapshot : ISnapshot - { - ... - } - - public class UserAggregate : - SnapshotAggregateRoot - { - protected override Task CreateSnapshotAsync( - CancellationToken cancellationToken) - { - // Create a UserSnapshot based on the current aggregate state - ... - } - - protected override Task LoadSnapshotAsync( - UserSnapshot snapshot, - ISnapshotMetadata metadata, - CancellationToken cancellationToken) - { - // Load the UserSnapshot into the current aggregate - ... - } - } - -When using aggregate snapshots there are several important details to -remember - -- Aggregates must not make any assumptions regarding the existence of - snapshots -- Aggregates must not assume that if a snapshots are created with - increasing aggregate sequence numbers -- Snapshots must be created in such a way, that the represent the - entire history up to the point of snapshot creation - -Snapshot strategy ------------------ - -When implementing an aggregate root that inherits from -``SnapshotAggregateRoot<,,>``, you need to pass the base class an -implementation of ``ISnapshotStrategy``. The strategy is used to -determine when a snapshot should be created, e.g. every 100 events. - -EventFlow ships with two that should be enough for most purposes as they -can be configured. - -- ``SnapshotEveryFewVersionsStrategy:`` Snapshots are created after a - predefined number of events, the default is ``100``, but another - frequency can be specified -- ``SnapshotRandomlyStrategy:`` Snapshots are created randomly with a - predefined chance, the default is ``1%``, but another can be - specified - -Upgrading snapshots -------------------- - -As an application grows over time, the data required to be stored within -a snapshots will change. Either because some become obsolete or merely -because a better way of storing aggregate state is found. If this -happens, the snapshots persisted in the snapshot store could potentially -become useless as aggregates are unable to apply them. The easy solution -would be to make change-by-addition and make sure that the old snapshots -can be desterilized into the new version. - -EventFlow provides an alternative solution, which is basically allowing -developers to upgrade snapshots similar to how `events are -upgraded <./EventUpgrade.md>`__. - -Lets say we have an application that has developed three snapshots -versions over time. - -.. code-block:: c# - - [SnapshotVersion("user", 1)] - public class UserSnapshotV1 : ISnapshot - { - ... - } - - [SnapshotVersion("user", 2)] - public class UserSnapshotV1 : ISnapshot - { - ... - } - - [SnapshotVersion("user", 3)] - public class UserSnapshot : ISnapshot - { - ... - } - -Note how version three of the ``UserAggregate`` snapshot is called -``UserSnapshot`` and not ``UserSnapshotV3``, its basically to help -developers tell which snapshot version is the current one. - -Remember to add the ``[SnapshotVersion]`` attribute as it enables -control of the snapshot definition name. If left out, EventFlow will -make a guess, which will be tied to the name of the class type. - -The next step will be to implement upgraders, or mappers, that can -upgrade one snapshot to another. - -.. code-block:: c# - - public class UserSnapshotV1ToV2Upgrader : - ISnapshotUpgrader - { - public Task UpgradeAsync( - UserSnapshotV1 userSnapshotV1, - CancellationToken cancellationToken) - { - // Map from V1 to V2 and return - } - } - - public class UserSnapshotV2ToV3Upgrader : - ISnapshotUpgrader - { - public Task UpgradeAsync( - UserSnapshotV2 userSnapshotV2, - CancellationToken cancellationToken) - { - // Map from V2 to V3 and return - } - } - -The snapshot types and upgraders then only needs to be registered in -EventFlow. - -.. code-block:: c# - - var resolver = EventFlowOptions.New - ... - .AddSnapshotUpgraders(myAssembly) - .AddSnapshots(myAssembly) - ... - .CreateResolver(); - -Now, when ever a snapshot is loaded from the snapshot store, its -automatically upgraded to the latest version and the aggregate only -needs to concern itself with the latest version. - -Snapshot store implementations ------------------------------- - -EventFlow has built-in support for some snapshot stores (more *will* be -implemented). - -Null (or none) -~~~~~~~~~~~~~~ - -The default implementation used by EventFlow does absolutely nothing -besides logging a warning if used. It exist only to help developers to -select a proper snapshot store. Making in-memory the default -implementation could present problems if snapshots were configured, but -the snapshot store configuration forgotten. - -In-memory -~~~~~~~~~ - -For testing, or small applications, the in-memory snapshot store is -configured by merely calling ``UseInMemorySnapshotStore()``. - -.. code-block:: c# - - var resolver = EventFlowOptions.New - ... - .UseInMemorySnapshotStore() - ... - .CreateResolver(); - -Microsoft SQL Server -~~~~~~~~~~~~~~~~~~~~ - -To use the MSSQL snapshot store you need to install the NuGet package -``EventFlow.MsSql``. - -Configuration -^^^^^^^^^^^^^ - -Configure the MSSQL connection and snapshot store as shown here. - -.. code-block:: c# - - var rootResolver = EventFlowOptions.New - ... - .ConfigureMsSql(MsSqlConfiguration.New - .SetConnectionString(@"Server=.\SQLEXPRESS;Database=MyApp;User Id=sa;Password=???")) - .UseMsSqlSnapshotStore() - ... - .CreateResolver(); - -Note that if you already use MSSQL for event- or read model store, you -only need to invoke the ``ConfigureMsSql`` extension *once*. - -Create and migrate required MSSQL databases -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Before you can use the MSSQL snapshot store, the required database and -tables must be created. The database specified in your MSSQL connection -*will not* be automatically created, you have to do this yourself. - -To make EventFlow create the required tables, execute the following -code. - -.. code-block:: c# - - var msSqlDatabaseMigrator = rootResolver.Resolve(); - EventFlowSnapshotStoresMsSql.MigrateDatabase(msSqlDatabaseMigrator); - -You should do this either on application start or preferably upon -application install or update, e.g., when the web site is installed. - -Custom -~~~~~~ - -If none of the above stores are adequate, a custom implementation is -possible by implementing the interface ``ISnapshotPersistence``. -However, there are some rules that the snapshot persistence store *must* -follow. - -- Its valid to store snapshots in any order, e.g. first version 3 then - 2 -- Its valid to overwrite existing snapshots version, e.g. storing - version 3 then version 3 again -- Fallback to old snapshots is allowed diff --git a/Documentation/Specifications.rst b/Documentation/Specifications.rst deleted file mode 100644 index 71f6589ef..000000000 --- a/Documentation/Specifications.rst +++ /dev/null @@ -1,88 +0,0 @@ -.. _specifications: - -Specifications -============== - -EventFlow ships with an implementation of the -`specification pattern `_ -which could be used to e.g. make complex business rules easier to read and test. - -To use the specification implementation shipped with EventFlow, simply create a -class that inherits from ``Specification``. - -.. code-block:: c# - - public class BelowFiveSpecification : Specification - { - protected override IEnumerable IsNotSatisfiedBecause(int i) - { - if (5 <= i) - { - yield return string.Format("{0} is not below five", i); - } - } - } - - -Note that instead of simply returning a ``bool`` to indicate whether or not the -specification is satisfied, this implementation requires a reason (or reasons) -why **not** the specification is satisfied. - -The ``ISpecification`` interface has two methods defined, the traditional -``IsSatisfiedBy`` and the addition ``WhyIsNotSatisfiedBy``, which returns an -empty enumerable if the specification was indeed satisfied. - -.. code-block:: c# - - public interface ISpecification - { - bool IsSatisfiedBy(T obj); - - IEnumerable WhyIsNotSatisfiedBy(T obj); - } - - -As specifications really become powerful when they are combined, EventFlow also -ships with a series of extension methods for the ``ISpecification`` interface -that allows easy combination of implemented specifications. - -.. code-block:: c# - - // Throws a `DomainError` exception if obj doesn't satisfy the specification - spec.ThrowDomainErrorIfNotStatisfied(obj); - - // Builds a new specification that requires all input specifications to be - // satified - var allSpec = specEnumerable.All(); - - // Builds a new specification that requires a predefined amount of the - // input specifications to be satisfied - var atLeastSpec = specEnumerable.AtLeast(4); - - // Builds a new specification that requires the two input specifications - // to be satisfied - var andSpec = spec1.And(spec2); - - // Builds a new specification that requires one of the two input - // specifications to be satisfied - var orSpec = spec1.Or(spec2); - - // Builds a new specification that requires the input specification - // not to be satisfied - var notSpec = spec.Not(); - - -If you need a simple expression to combine with other more complex specifications -you can use the bundled ``ExpressionSpecification``, which is a specification -wrapper for an expression. - -.. code-block:: c# - - var spec = new ExpressionSpecification(i => 1 < i && i < 3); - - // 'str' will contain the value "i => ((1 < i) && (i < 3))" - var str = spec.ToString(); - - -If the specification isn't satisfied, a string representation of the expression -is returned. \ No newline at end of file diff --git a/Documentation/Subscribers.rst b/Documentation/Subscribers.rst deleted file mode 100644 index 54de5427a..000000000 --- a/Documentation/Subscribers.rst +++ /dev/null @@ -1,190 +0,0 @@ -.. _subscribers: - -Subscribers -============ - -Whenever your application needs to act when a specific event is emitted -from your domain you create a class that implement one of the following -two interfaces which is. - -- ``ISubscribeSynchronousTo``: Executed - synchronous -- ``ISubscribeAsynchronousTo``: Executed - asynchronous - -Any implemented subscribers needs to be registered to this interface, -either using ``AddSubscriber(...)``, ``AddSubscribers(...)`` or -``AddDefaults(...)`` during initialization. If you have configured a -custom IoC container, you can register the implementations using it -instead. - -.. NOTE:: - - The *synchronous* and *asynchronous* here has nothing to do - with the .NET framework keywords ``async``, ``await`` or the Task - Parallel Library. It refers to how the subscribers are executed. Read - below for details. - - -.. _subscribers-sync: - -Synchronous subscribers ------------------------ - -Synchronous subscribers in EventFlow are executed one at a time for each -emitted domain event in order. This e.g. guarantees that all subscribers -have been executed when the ``ICommandBus.PublishAsync(...)`` returns. - -The ``ISubscribeSynchronousTo<,,>`` is shown here. - -.. code-block:: c# - - public interface ISubscribeSynchronousTo - where TAggregate : IAggregateRoot - where TIdentity : IIdentity - where TEvent : IAggregateEvent - { - Task HandleAsync( - IDomainEvent domainEvent, - CancellationToken cancellationToken); - } - -.. _out-of-order-event-subscribers: - -Out of order events -^^^^^^^^^^^^^^^^^^^ - -As synchronous subscribers are by their very nature executed -synchronously, emitting multiple events from an aggregate and letting -subscribers publish new commands based on this can however lead to some -unexpected behavior as "innermost" subscribers will be executed before -first. - -1. Aggregate emits events ``Event 1`` and ``Event 2`` -2. Subscriber handles ``Event 1`` and publishes a command that results - in ``Event 3`` being emitted -3. Subscriber handles ``Event 3`` (doesn't affect the domain) -4. Subscriber handles ``Event 2`` - -In the above example the subscriber will handle the events in the -following order ``Event 1``, ``Event 3`` and then ``Event 2``. While -this *could* occur in a distributed system or executing subscribers on -different threads, its a certainty when using synchronous subscribers. - - -Exceptions swallowed by default -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -By default any exceptions thrown by a subscriber are **swallowed** -by EventFlow after it has been logged as an error. Depending on the -application this might be the preferred behavior, but in some cases -it isn't. If subscriber exception should be thrown, and thus allowing -them to be caught in e.g. command handlers, the behaivor can be disabled -by setting the ``ThrowSubscriberExceptions`` to ``true`` like illustrated -here. - -.. code-block:: c# - - using (var resolver = EventFlowOptions.New - .Configure(c => c.ThrowSubscriberExceptions = true) - .CreateResolver()) - { - ... - } - - -.. _subscribers-async: - -Asynchronous subscribers ------------------------- - -Asynchronous subscribers in EventFlow are executed using a scheduled job. - -.. IMPORTANT:: - Asynchronous subscribers are **disabled by default** and must be - enabled using the following configuration. - -.. code-block:: c# - - eventFlowOptions.Configure(c => IsAsynchronousSubscribersEnabled = true); - - -.. IMPORTANT:: - As asynchronous subscribers are executed using a job, its important - to configure proper job scheduling by e.g. using the - ``EventFlow.Hangfire`` NuGet package. - -The ``ISubscribeAsynchronousTo<,,>`` is shown here and is, besides its -name, identical to its synchronous counterpart. - -.. code-block:: c# - - public interface ISubscribeAsynchronousTo - where TAggregate : IAggregateRoot - where TIdentity : IIdentity - where TEvent : IAggregateEvent - { - Task HandleAsync( - IDomainEvent domainEvent, - CancellationToken cancellationToken); - } - -.. NOTE:: - - Setting ``ThrowSubscriberExceptions = true`` has **no effect** - on asynchronous subscribers. - - -Subscribe to every event ------------------------- - -Instead of subscribing to every single domain, you can register an -implementation of ``ISubscribeSynchronousToAll`` which is defined as -shown here. - -.. code-block:: c# - - public interface ISubscribeSynchronousToAll - { - Task HandleAsync( - IReadOnlyCollection domainEvents, - CancellationToken cancellationToken); - } - -Any registered implementations will be notified for every domain event -emitted. - - -.. _subscribers-rabbitmq: - -RabbitMQ -^^^^^^^^ - -See :ref:`RabbitMQ setup ` for details on how to get -started using RabbitMQ_. - -After RabbitMQ has been configured, all domain events are published -to a exchange named ``eventflow`` with routing keys in the following -format. - -:: - - eventflow.domainevent.[Aggregate name].[Event name].[Event version] - -Which will be the following for an event named ``CreateUser`` version -``1`` for the ``MyUserAggregate``. - -:: - - eventflow.domainevent.my-user.create-user.1 - -Note the lowercasing and adding of ``-`` whenever there's a capital -letter. - -All the above is the default behavior, if you don't like it replace e.g. -the service ``IRabbitMqMessageFactory`` to customize what routing key or -exchange to use. Have a look at how -`EventFlow `__ has done its -implementation to get started. - -.. _RabbitMQ: https://www.rabbitmq.com/ diff --git a/Documentation/ValueObjects.rst b/Documentation/ValueObjects.rst deleted file mode 100644 index fc9217b52..000000000 --- a/Documentation/ValueObjects.rst +++ /dev/null @@ -1,112 +0,0 @@ -.. _value-objects: - -Event serialization and value objects -===================================== - -One of the important parts the creating a event sourced application, is -to ensure that you always can read your event streams. It seems simple -enough, but it is a problem, especially for with large applications that -undergo refactoring or domain changes. - -The basic idea is to store events in a structure that's easy to access -and migrate if the need should arise. EventFlow, like many other event -sourced systems, stores its event using JSON. - -Making pretty and clean JSON ----------------------------- - -You might wonder "but, why?", and the reason is somewhat similar to the -reasoning behind `semantic -URLs `__. - -Consider the following value object used to validate and contain -usernames in an application. - -.. code-block:: c# - - public class Username - { - public string Value { get; } - - public Username(string value) - { - if (string.IsNullOrEmpty(value) || value.Length <= 4) - { - throw DomainError.With($"Invalid username '{value}'"); - } - - Value = value; - } - } - -First we do some cleanup and re-write it using EventFlows -``SingleValueObject<>``. - -.. code-block:: c# - - public class Username : SingleValueObject - { - public Username(string value) : base(value) - { - if (string.IsNullOrEmpty(value) || value.Length <= 4) - { - throw DomainError.With($"Invalid username '{value}'"); - } - } - } - -Now it looks simple and we might think we can use this value object -directly in our domain events. We could, but the resulting JSON will -look like this. - -.. code:: json - - { - "Username" : { - "Value": "my-awesome-username", - } - } - -This doesn't look very good. First, that extra property doesn't make it -easier to read and it takes up more space when serializing and -transmitting the event. - -In addition, if you use the value object on a web API, people using the -API will need to wrap the properties in their DTOs in a similarly. What -we would like is to have our serialized event to look like this instead -and still use the value object in our events. - -.. code:: json - - { - "Username" : "my-awesome-username" - } - -To do this, we use the custom JSON serializer EventFlow has for single -value objects called ``SingleValueObjectConverter`` on our ``Username`` -class like this. - -.. code-block:: c# - - [JsonConverter(typeof(SingleValueObjectConverter))] // Only this line added - public class Username : SingleValueObject - { - public Username(string value) : base(value) - { - if (string.IsNullOrEmpty(value) || value.Length <= 4) - { - throw DomainError.With($"Invalid username '{value}'"); - } - } - } - -The JSON converter understands the single value object and will -serialize and deserialize it correctly. - -Using this converter also enables to you replace e.g. raw ``string`` and -``int`` properties with value objects on existing events as they will be -"JSON compatible". - -.. NOTE:: - - Consider applying this to any classes that inherit from ``Identity<>``. diff --git a/Documentation/_static/.keepme b/Documentation/_static/.keepme deleted file mode 100644 index e69de29bb..000000000 diff --git a/Documentation/_templates/.keepme b/Documentation/_templates/.keepme deleted file mode 100644 index e69de29bb..000000000 diff --git a/Documentation/conf.py b/Documentation/conf.py deleted file mode 100644 index 8a662848d..000000000 --- a/Documentation/conf.py +++ /dev/null @@ -1,338 +0,0 @@ -# -*- coding: utf-8 -*- -# -# This file is execfile()d with the current directory set to its -# containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -# -# import os -# import sys -# sys.path.insert(0, os.path.abspath('.')) - -# -- General configuration ------------------------------------------------ - -# If your documentation needs a minimal Sphinx version, state it here. -# -# needs_sphinx = '1.0' - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. -extensions = [ - 'sphinx.ext.autodoc', -] - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# The suffix(es) of source filenames. -# You can specify multiple suffix as a list of string: -# -# source_suffix = ['.rst', '.md'] -source_suffix = ['.rst'] - - -# The encoding of source files. -# -source_encoding = 'utf-8-sig' - -# The master toctree document. -master_doc = 'index' - -# General information about the project. -project = u'EventFlow' -copyright = u'2015-2017, Rasmus Mikkelsen' -author = u'Rasmus Mikkelsen' - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. -version = u'0.1' -# The full version, including alpha/beta/rc tags. -release = u'0.1' - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -# -# This is also used if you do content translation via gettext catalogs. -# Usually you set "language" from the command line for these cases. -language = None - -# There are two options for replacing |today|: either, you set today to some -# non-false value, then it is used: -# -# today = '' -# -# Else, today_fmt is used as the format for a strftime call. -# -# today_fmt = '%B %d, %Y' - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -# This patterns also effect to html_static_path and html_extra_path -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] - -# The reST default role (used for this markup: `text`) to use for all -# documents. -# -# default_role = None - -# If true, '()' will be appended to :func: etc. cross-reference text. -# -# add_function_parentheses = True - -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -# -# add_module_names = True - -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -# -# show_authors = False - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' - -# A list of ignored prefixes for module index sorting. -# modindex_common_prefix = [] - -# If true, keep warnings as "system message" paragraphs in the built documents. -# keep_warnings = False - -# If true, `todo` and `todoList` produce output, else they produce nothing. -todo_include_todos = False - - -# -- Options for HTML output ---------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -# -html_theme = 'sphinx_rtd_theme' - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -# -# html_theme_options = {} - -# Add any paths that contain custom themes here, relative to this directory. -# html_theme_path = [] - -# The name for this set of Sphinx documents. -# " v documentation" by default. -# -# html_title = u'EventFlow v0.1' - -# A shorter title for the navigation bar. Default is the same as html_title. -# -# html_short_title = None - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -# -# html_logo = '../icon-128.png' - -# The name of an image file (relative to this directory) to use as a favicon of -# the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. -# -# html_favicon = None - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] - -# Add any extra paths that contain custom files (such as robots.txt or -# .htaccess) here, relative to this directory. These files are copied -# directly to the root of the documentation. -# -# html_extra_path = [] - -# If not None, a 'Last updated on:' timestamp is inserted at every page -# bottom, using the given strftime format. -# The empty string is equivalent to '%b %d, %Y'. -# -# html_last_updated_fmt = None - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -# -# html_use_smartypants = True - -# Custom sidebar templates, maps document names to template names. -# -# html_sidebars = {} - -# Additional templates that should be rendered to pages, maps page names to -# template names. -# -# html_additional_pages = {} - -# If false, no module index is generated. -# -# html_domain_indices = True - -# If false, no index is generated. -# -# html_use_index = True - -# If true, the index is split into individual pages for each letter. -# -# html_split_index = False - -# If true, links to the reST sources are added to the pages. -# -# html_show_sourcelink = True - -# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -# -# html_show_sphinx = True - -# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -# -# html_show_copyright = True - -# If true, an OpenSearch description file will be output, and all pages will -# contain a tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -# -# html_use_opensearch = '' - -# This is the file name suffix for HTML files (e.g. ".xhtml"). -# html_file_suffix = None - -# Language to be used for generating the HTML full-text search index. -# Sphinx supports the following languages: -# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' -# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr', 'zh' -# -# html_search_language = 'en' - -# A dictionary with options for the search language support, empty by default. -# 'ja' uses this config value. -# 'zh' user can custom change `jieba` dictionary path. -# -# html_search_options = {'type': 'default'} - -# The name of a javascript file (relative to the configuration directory) that -# implements a search results scorer. If empty, the default will be used. -# -# html_search_scorer = 'scorer.js' - -# Output file base name for HTML help builder. -htmlhelp_basename = 'EventFlowdoc' - -# -- Options for LaTeX output --------------------------------------------- - -latex_elements = { - # The paper size ('letterpaper' or 'a4paper'). - # - # 'papersize': 'letterpaper', - - # The font size ('10pt', '11pt' or '12pt'). - # - # 'pointsize': '10pt', - - # Additional stuff for the LaTeX preamble. - # - # 'preamble': '', - - # Latex figure (float) alignment - # - # 'figure_align': 'htbp', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, -# author, documentclass [howto, manual, or own class]). -latex_documents = [ - (master_doc, 'EventFlow.tex', u'EventFlow Documentation', - u'Rasmus Mikkelsen', 'manual'), -] - -# The name of an image file (relative to this directory) to place at the top of -# the title page. -# -# latex_logo = None - -# For "manual" documents, if this is true, then toplevel headings are parts, -# not chapters. -# -# latex_use_parts = False - -# If true, show page references after internal links. -# -# latex_show_pagerefs = False - -# If true, show URL addresses after external links. -# -# latex_show_urls = False - -# Documents to append as an appendix to all manuals. -# -# latex_appendices = [] - -# It false, will not define \strong, \code, itleref, \crossref ... but only -# \sphinxstrong, ..., \sphinxtitleref, ... To help avoid clash with user added -# packages. -# -# latex_keep_old_macro_names = True - -# If false, no module index is generated. -# -# latex_domain_indices = True - - -# -- Options for manual page output --------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, 'eventflow', u'EventFlow Documentation', - [author], 1) -] - -# If true, show URL addresses after external links. -# -# man_show_urls = False - - -# -- Options for Texinfo output ------------------------------------------- - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - (master_doc, 'EventFlow', u'EventFlow Documentation', - author, 'EventFlow', 'Async/await first CQRS+ES and DDD framework for .NET.', - 'Miscellaneous'), -] - -# Documents to append as an appendix to all manuals. -# -# texinfo_appendices = [] - -# If false, no module index is generated. -# -# texinfo_domain_indices = True - -# How to display URL addresses: 'footnote', 'no', or 'inline'. -# -# texinfo_show_urls = 'footnote' - -# If true, do not generate a @detailmenu in the "Top" node's menu. -# -# texinfo_no_detailmenu = False diff --git a/Documentation/index.rst b/Documentation/index.rst deleted file mode 100644 index be16de4e5..000000000 --- a/Documentation/index.rst +++ /dev/null @@ -1,77 +0,0 @@ -Welcome to EventFlow's documentation! -===================================== - -.. image:: ../icon-128.png - :alt: EventFlow logo - :height: 128 - :width: 128 - :align: right - -EventFlow is a basic CQRS+ES framework designed to be easy to use. - -Have a look at our :ref:`getting started guide `, the -:ref:`do’s and don’ts ` and the :ref:`FAQ `. - - -Contents: - -.. toctree:: - :maxdepth: 2 - - GettingStarted - - -.. toctree:: - :maxdepth: 2 - :caption: Basics - - Identity - Aggregates - Commands - Subscribers - Metadata - Queries - Sagas - Jobs - EventUpgrade - - -.. toctree:: - :maxdepth: 2 - :caption: Stores - - EventStore - ReadStores - - -.. toctree:: - :maxdepth: 2 - :caption: Integration - - MSSQL - RabbitMQ - - -.. toctree:: - :maxdepth: 2 - :caption: Additional reading - - Configuration - IoC - Log - Snapshots - Customize - ValueObjects - DosAndDonts - Specifications - FAQ - - - - - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`search` diff --git a/Documentation/make.bat b/Documentation/make.bat deleted file mode 100644 index 6297b4687..000000000 --- a/Documentation/make.bat +++ /dev/null @@ -1,281 +0,0 @@ -@ECHO OFF - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set BUILDDIR=_build -set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . -set I18NSPHINXOPTS=%SPHINXOPTS% . -if NOT "%PAPER%" == "" ( - set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% - set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% -) - -if "%1" == "" goto help - -if "%1" == "help" ( - :help - echo.Please use `make ^` where ^ is one of - echo. html to make standalone HTML files - echo. dirhtml to make HTML files named index.html in directories - echo. singlehtml to make a single large HTML file - echo. pickle to make pickle files - echo. json to make JSON files - echo. htmlhelp to make HTML files and a HTML help project - echo. qthelp to make HTML files and a qthelp project - echo. devhelp to make HTML files and a Devhelp project - echo. epub to make an epub - echo. epub3 to make an epub3 - echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter - echo. text to make text files - echo. man to make manual pages - echo. texinfo to make Texinfo files - echo. gettext to make PO message catalogs - echo. changes to make an overview over all changed/added/deprecated items - echo. xml to make Docutils-native XML files - echo. pseudoxml to make pseudoxml-XML files for display purposes - echo. linkcheck to check all external links for integrity - echo. doctest to run all doctests embedded in the documentation if enabled - echo. coverage to run coverage check of the documentation if enabled - echo. dummy to check syntax errors of document sources - goto end -) - -if "%1" == "clean" ( - for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i - del /q /s %BUILDDIR%\* - goto end -) - - -REM Check if sphinx-build is available and fallback to Python version if any -%SPHINXBUILD% 1>NUL 2>NUL -if errorlevel 9009 goto sphinx_python -goto sphinx_ok - -:sphinx_python - -set SPHINXBUILD=python -m sphinx.__init__ -%SPHINXBUILD% 2> nul -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.http://sphinx-doc.org/ - exit /b 1 -) - -:sphinx_ok - - -if "%1" == "html" ( - %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/html. - goto end -) - -if "%1" == "dirhtml" ( - %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. - goto end -) - -if "%1" == "singlehtml" ( - %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. - goto end -) - -if "%1" == "pickle" ( - %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can process the pickle files. - goto end -) - -if "%1" == "json" ( - %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can process the JSON files. - goto end -) - -if "%1" == "htmlhelp" ( - %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can run HTML Help Workshop with the ^ -.hhp project file in %BUILDDIR%/htmlhelp. - goto end -) - -if "%1" == "qthelp" ( - %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can run "qcollectiongenerator" with the ^ -.qhcp project file in %BUILDDIR%/qthelp, like this: - echo.^> qcollectiongenerator %BUILDDIR%\qthelp\EventFlow.qhcp - echo.To view the help file: - echo.^> assistant -collectionFile %BUILDDIR%\qthelp\EventFlow.ghc - goto end -) - -if "%1" == "devhelp" ( - %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. - goto end -) - -if "%1" == "epub" ( - %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The epub file is in %BUILDDIR%/epub. - goto end -) - -if "%1" == "epub3" ( - %SPHINXBUILD% -b epub3 %ALLSPHINXOPTS% %BUILDDIR%/epub3 - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The epub3 file is in %BUILDDIR%/epub3. - goto end -) - -if "%1" == "latex" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "latexpdf" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - cd %BUILDDIR%/latex - make all-pdf - cd %~dp0 - echo. - echo.Build finished; the PDF files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "latexpdfja" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - cd %BUILDDIR%/latex - make all-pdf-ja - cd %~dp0 - echo. - echo.Build finished; the PDF files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "text" ( - %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The text files are in %BUILDDIR%/text. - goto end -) - -if "%1" == "man" ( - %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The manual pages are in %BUILDDIR%/man. - goto end -) - -if "%1" == "texinfo" ( - %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. - goto end -) - -if "%1" == "gettext" ( - %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The message catalogs are in %BUILDDIR%/locale. - goto end -) - -if "%1" == "changes" ( - %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes - if errorlevel 1 exit /b 1 - echo. - echo.The overview file is in %BUILDDIR%/changes. - goto end -) - -if "%1" == "linkcheck" ( - %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck - if errorlevel 1 exit /b 1 - echo. - echo.Link check complete; look for any errors in the above output ^ -or in %BUILDDIR%/linkcheck/output.txt. - goto end -) - -if "%1" == "doctest" ( - %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest - if errorlevel 1 exit /b 1 - echo. - echo.Testing of doctests in the sources finished, look at the ^ -results in %BUILDDIR%/doctest/output.txt. - goto end -) - -if "%1" == "coverage" ( - %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage - if errorlevel 1 exit /b 1 - echo. - echo.Testing of coverage in the sources finished, look at the ^ -results in %BUILDDIR%/coverage/python.txt. - goto end -) - -if "%1" == "xml" ( - %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The XML files are in %BUILDDIR%/xml. - goto end -) - -if "%1" == "pseudoxml" ( - %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. - goto end -) - -if "%1" == "dummy" ( - %SPHINXBUILD% -b dummy %ALLSPHINXOPTS% %BUILDDIR%/dummy - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. Dummy builder generates no files. - goto end -) - -:end diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 27e8869fe..99ef5ab82 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,4 +1,83 @@ -### New in 0.49 (not released yet) +### New in 0.50 (not released yet) + +* New: While EventFlow tries to limit the about of painful API changes, the + introduction of execution/command results are considered a necessary step + towards as better API. + + Commands and command handlers have been updated to support execution + results. Execution results is meant to be an alternative to throwing domain + exceptions to do application flow. In short, before you were required to + throw an exception if you wanted to abort execution and "return" a failure + message. + + The introduction of execution results changes this, as it allows + returning a failed result that is passed all the way back to the command + publisher. Execution results are generic and can thus contain e.g. any + validation results that a UI might need. The `ICommandBus.PublishAsync` + signature has changed to reflect this. + + from + ```csharp + Task PublishAsync( + ICommand command) + where TAggregate : IAggregateRoot + where TIdentity : IIdentity + where TSourceIdentity : ISourceId + ``` + to + ```csharp + Task PublishAsync( + ICommand command, + CancellationToken cancellationToken) + where TAggregate : IAggregateRoot + where TIdentity : IIdentity + where TExecutionResult : IExecutionResult + ``` + + Command handler signature has changed from + + ```csharp + Task ExecuteAsync( + TAggregate aggregate, + TCommand command, + CancellationToken cancellationToken); + ``` + to + ```csharp + Task ExecuteCommandAsync( + TAggregate aggregate, + TCommand command, + CancellationToken cancellationToken) + ``` + + Migrating to the new structure should be seamless if your current code base + inherits its command handlers from the provided `CommandHandler<,,>` base + class. + +* Breaking: Source IDs on commands have been reworked to "make room" for + execution results on commands. The generic parameter from `ICommand<,,>` + and `ICommandHandler<,,,>` has been removed in favor of the new execution + results. `ICommand.SourceId` is now of type `ISourceId` instead of using + the generic type and the `ICommandBus.PublishAsync` no longer returns + `Task` + + To get code that behaves similar to the previous version, simply take the + `ISourceId` from the command, i.e., instead of this + + ```csharp + var sourceId = await commandBus.PublishAsync(command); + ``` + write this + ```csharp + await commandBus.PublishAsync(command); + var sourceId = command.SourceId; + ``` + (`CancellationToken` and `.ConfigureAwait(false)` omitted fromt he above) + +* Breaking: Upgraded NuGet dependency on `RabbitMQ.Client` from `>= 4.1.3` + to `>= 5.0.1` + +### New in 0.49.3031 (released 2017-09-07) * Breaking: Upgraded `EventStore.Client` dependency to version 4.0 * Breaking: Changed target framework for `EventFlow.EventStores.EventStore` to diff --git a/Source/EventFlow.Autofac.Tests/app.config b/Source/EventFlow.Autofac.Tests/app.config index 227193e63..069e3fd6d 100644 --- a/Source/EventFlow.Autofac.Tests/app.config +++ b/Source/EventFlow.Autofac.Tests/app.config @@ -7,7 +7,7 @@ - + diff --git a/Source/EventFlow.Elasticsearch.Tests/app.config b/Source/EventFlow.Elasticsearch.Tests/app.config index 227193e63..069e3fd6d 100644 --- a/Source/EventFlow.Elasticsearch.Tests/app.config +++ b/Source/EventFlow.Elasticsearch.Tests/app.config @@ -7,7 +7,7 @@ - + diff --git a/Source/EventFlow.EventStores.EventStore.Tests/EventStoreRunner.cs b/Source/EventFlow.EventStores.EventStore.Tests/EventStoreRunner.cs index 7ea16cf34..576824362 100644 --- a/Source/EventFlow.EventStores.EventStore.Tests/EventStoreRunner.cs +++ b/Source/EventFlow.EventStores.EventStore.Tests/EventStoreRunner.cs @@ -41,7 +41,7 @@ public class EventStoreRunner private static readonly SoftwareDescription SoftwareDescription = SoftwareDescription.Create( "eventstore", new Version(4, 0, 2), - "http://download.geteventstore.com/binaries/EventStore-OSS-Win-v4.0.2.zip"); + "https://eventstore.org/downloads/EventStore-OSS-Win-v4.0.2.zip"); public class EventStoreInstance : IDisposable { diff --git a/Source/EventFlow.EventStores.EventStore.Tests/app.config b/Source/EventFlow.EventStores.EventStore.Tests/app.config index cc2b7f330..069e3fd6d 100644 --- a/Source/EventFlow.EventStores.EventStore.Tests/app.config +++ b/Source/EventFlow.EventStores.EventStore.Tests/app.config @@ -5,13 +5,9 @@ - - - - - + diff --git a/Source/EventFlow.Examples.Shipping.Tests/app.config b/Source/EventFlow.Examples.Shipping.Tests/app.config index cc2b7f330..069e3fd6d 100644 --- a/Source/EventFlow.Examples.Shipping.Tests/app.config +++ b/Source/EventFlow.Examples.Shipping.Tests/app.config @@ -5,13 +5,9 @@ - - - - - + diff --git a/Source/EventFlow.Hangfire.Tests/app.config b/Source/EventFlow.Hangfire.Tests/app.config index 86b21fd5f..27ef945c8 100644 --- a/Source/EventFlow.Hangfire.Tests/app.config +++ b/Source/EventFlow.Hangfire.Tests/app.config @@ -11,7 +11,7 @@ - + diff --git a/Source/EventFlow.MsSql.Tests/app.config b/Source/EventFlow.MsSql.Tests/app.config index cc2b7f330..069e3fd6d 100644 --- a/Source/EventFlow.MsSql.Tests/app.config +++ b/Source/EventFlow.MsSql.Tests/app.config @@ -5,13 +5,9 @@ - - - - - + diff --git a/Source/EventFlow.Owin.Tests/app.config b/Source/EventFlow.Owin.Tests/app.config index ba992b548..c63e68119 100644 --- a/Source/EventFlow.Owin.Tests/app.config +++ b/Source/EventFlow.Owin.Tests/app.config @@ -15,7 +15,7 @@ - + diff --git a/Source/EventFlow.RabbitMQ.Tests/RabbitMqConsumer.cs b/Source/EventFlow.RabbitMQ.Tests/RabbitMqConsumer.cs index ecc9eb2b9..e5ed33948 100644 --- a/Source/EventFlow.RabbitMQ.Tests/RabbitMqConsumer.cs +++ b/Source/EventFlow.RabbitMQ.Tests/RabbitMqConsumer.cs @@ -44,7 +44,7 @@ public RabbitMqConsumer(Uri uri, Exchange exchange, IEnumerable routingK { var connectionFactory = new ConnectionFactory { - Uri = uri.ToString(), + Uri = uri, }; _connection = connectionFactory.CreateConnection(); _model = _connection.CreateModel(); diff --git a/Source/EventFlow.RabbitMQ.Tests/app.config b/Source/EventFlow.RabbitMQ.Tests/app.config index cc2b7f330..069e3fd6d 100644 --- a/Source/EventFlow.RabbitMQ.Tests/app.config +++ b/Source/EventFlow.RabbitMQ.Tests/app.config @@ -5,13 +5,9 @@ - - - - - + diff --git a/Source/EventFlow.RabbitMQ/EventFlow.RabbitMQ.csproj b/Source/EventFlow.RabbitMQ/EventFlow.RabbitMQ.csproj index bc3f49be9..0f9d75abd 100644 --- a/Source/EventFlow.RabbitMQ/EventFlow.RabbitMQ.csproj +++ b/Source/EventFlow.RabbitMQ/EventFlow.RabbitMQ.csproj @@ -24,9 +24,9 @@ - + - + \ No newline at end of file diff --git a/Source/EventFlow.RabbitMQ/Integrations/RabbitMqConnectionFactory.cs b/Source/EventFlow.RabbitMQ/Integrations/RabbitMqConnectionFactory.cs index d073a2a38..22863871f 100644 --- a/Source/EventFlow.RabbitMQ/Integrations/RabbitMqConnectionFactory.cs +++ b/Source/EventFlow.RabbitMQ/Integrations/RabbitMqConnectionFactory.cs @@ -68,7 +68,7 @@ private async Task CreateConnectionFactoryAsync(Uri uri, Canc connectionFactory = new ConnectionFactory { - Uri = uri.ToString(), + Uri = uri, UseBackgroundThreadsForIO = true, // TODO: As soon as RabbitMQ supports async/await, set to false TopologyRecoveryEnabled = true, AutomaticRecoveryEnabled = true, diff --git a/Source/EventFlow.SQLite.Tests/app.config b/Source/EventFlow.SQLite.Tests/app.config index cc2b7f330..069e3fd6d 100644 --- a/Source/EventFlow.SQLite.Tests/app.config +++ b/Source/EventFlow.SQLite.Tests/app.config @@ -5,13 +5,9 @@ - - - - - + diff --git a/Source/EventFlow.Sql.Tests/app.config b/Source/EventFlow.Sql.Tests/app.config index 227193e63..069e3fd6d 100644 --- a/Source/EventFlow.Sql.Tests/app.config +++ b/Source/EventFlow.Sql.Tests/app.config @@ -7,7 +7,7 @@ - + diff --git a/Source/EventFlow.Tests/Documentation/GettingStarted/ExampleAggregate.cs b/Source/EventFlow.TestHelpers/Aggregates/Commands/ThingyMaybePingCommand.cs similarity index 51% rename from Source/EventFlow.Tests/Documentation/GettingStarted/ExampleAggregate.cs rename to Source/EventFlow.TestHelpers/Aggregates/Commands/ThingyMaybePingCommand.cs index a0a2166b2..a1384ed5f 100644 --- a/Source/EventFlow.Tests/Documentation/GettingStarted/ExampleAggregate.cs +++ b/Source/EventFlow.TestHelpers/Aggregates/Commands/ThingyMaybePingCommand.cs @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// The MIT License (MIT) // // Copyright (c) 2015-2017 Rasmus Mikkelsen // Copyright (c) 2015-2017 eBay Software Foundation @@ -20,37 +20,41 @@ // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// -using EventFlow.Aggregates; -using EventFlow.Exceptions; +using System.Threading; +using System.Threading.Tasks; +using EventFlow.Aggregates.ExecutionResults; +using EventFlow.Commands; +using EventFlow.TestHelpers.Aggregates.ValueObjects; +using Newtonsoft.Json; -namespace EventFlow.Tests.Documentation.GettingStarted +namespace EventFlow.TestHelpers.Aggregates.Commands { - /// The aggregate root - public class ExampleAggregate : - AggregateRoot, - IEmit + [CommandVersion("ThingyMaybePing", 1)] + public class ThingyMaybePingCommand : Command { - private int? _magicNumber; - - public ExampleAggregate(ExampleId id) : base(id) { } + public PingId PingId { get; } + public bool IsSuccess { get; } - // Method invoked by our command - public void SetMagicNumer(int magicNumber) + [JsonConstructor] + public ThingyMaybePingCommand(ThingyId aggregateId, PingId pingId, bool isSuccess) + : base(aggregateId, CommandId.New) { - if (_magicNumber.HasValue) - throw DomainError.With("Magic number already set"); - - Emit(new ExampleEvent(magicNumber)); + PingId = pingId; + IsSuccess = isSuccess; } + } - // We apply the event as part of the event sourcing system. EventFlow - // provides several different methods for doing this, e.g. state objects, - // the Apply method is merely the simplest - public void Apply(ExampleEvent aggregateEvent) + public class ThingyMaybePingCommandHandler : + CommandHandler + { + public override Task ExecuteCommandAsync( + ThingyAggregate aggregate, + ThingyMaybePingCommand command, + CancellationToken cancellationToken) { - _magicNumber = aggregateEvent.MagicNumber; + var executionResult = aggregate.PingMaybe(command.PingId, command.IsSuccess); + return Task.FromResult(executionResult); } } } \ No newline at end of file diff --git a/Source/EventFlow.TestHelpers/Aggregates/ThingyAggregate.cs b/Source/EventFlow.TestHelpers/Aggregates/ThingyAggregate.cs index 5834f1a9e..dedb71727 100644 --- a/Source/EventFlow.TestHelpers/Aggregates/ThingyAggregate.cs +++ b/Source/EventFlow.TestHelpers/Aggregates/ThingyAggregate.cs @@ -26,6 +26,7 @@ using System.Threading; using System.Threading.Tasks; using EventFlow.Aggregates; +using EventFlow.Aggregates.ExecutionResults; using EventFlow.Exceptions; using EventFlow.Snapshots; using EventFlow.Snapshots.Strategies; @@ -84,6 +85,14 @@ public void Ping(PingId pingId) Emit(new ThingyPingEvent(pingId)); } + public IExecutionResult PingMaybe(PingId pingId, bool isSuccess) + { + Emit(new ThingyPingEvent(pingId)); + return isSuccess + ? ExecutionResult.Success() + : ExecutionResult.Failed(); + } + public void RequestSagaStart() { Emit(new ThingySagaStartRequestedEvent()); diff --git a/Source/EventFlow.TestHelpers/EventFlow.TestHelpers.csproj b/Source/EventFlow.TestHelpers/EventFlow.TestHelpers.csproj index 5b8a4c3bb..e61a8ded8 100644 --- a/Source/EventFlow.TestHelpers/EventFlow.TestHelpers.csproj +++ b/Source/EventFlow.TestHelpers/EventFlow.TestHelpers.csproj @@ -22,13 +22,13 @@ https://raw.githubusercontent.com/eventflow/EventFlow/develop/icon-128.png https://raw.githubusercontent.com/eventflow/EventFlow/develop/LICENSE en-US - UPDATED BY BUILD + UPDATED BY BUILD - - - - + + + + @@ -37,7 +37,4 @@ - - - \ No newline at end of file diff --git a/Source/EventFlow.TestHelpers/Extensions/CommandBusExtensions.cs b/Source/EventFlow.TestHelpers/Extensions/CommandBusExtensions.cs index 924bc5764..759e53a4d 100644 --- a/Source/EventFlow.TestHelpers/Extensions/CommandBusExtensions.cs +++ b/Source/EventFlow.TestHelpers/Extensions/CommandBusExtensions.cs @@ -24,6 +24,7 @@ using System.Threading; using System.Threading.Tasks; using EventFlow.Aggregates; +using EventFlow.Aggregates.ExecutionResults; using EventFlow.Commands; using EventFlow.Core; @@ -31,12 +32,12 @@ namespace EventFlow.TestHelpers.Extensions { public static class CommandBusExtensions { - public static Task PublishAsync( + public static Task PublishAsync( this ICommandBus commandBus, - ICommand command) + ICommand command) where TAggregate : IAggregateRoot where TIdentity : IIdentity - where TSourceIdentity : ISourceId + where TExecutionResult : IExecutionResult { return commandBus.PublishAsync(command, CancellationToken.None); } diff --git a/Source/EventFlow.TestHelpers/Suites/TestSuiteForScheduler.cs b/Source/EventFlow.TestHelpers/Suites/TestSuiteForScheduler.cs index cb918eab1..98608a5be 100644 --- a/Source/EventFlow.TestHelpers/Suites/TestSuiteForScheduler.cs +++ b/Source/EventFlow.TestHelpers/Suites/TestSuiteForScheduler.cs @@ -23,7 +23,6 @@ using System; using System.Collections.Concurrent; -using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using EventFlow.Aggregates; diff --git a/Source/EventFlow.TestHelpers/Suites/TestSuiteForServiceRegistration.cs b/Source/EventFlow.TestHelpers/Suites/TestSuiteForServiceRegistration.cs index d7e6f909f..9fd9cd2c3 100644 --- a/Source/EventFlow.TestHelpers/Suites/TestSuiteForServiceRegistration.cs +++ b/Source/EventFlow.TestHelpers/Suites/TestSuiteForServiceRegistration.cs @@ -428,10 +428,8 @@ public void AbstractCommandHandlerIsNotRegistered() } public abstract class AbstractTestCommandHandler : - ICommandHandler + CommandHandler { - public abstract Task ExecuteAsync(ThingyAggregate aggregate, ThingyPingCommand command, - CancellationToken cancellationToken); } [Test] diff --git a/Source/EventFlow.TestHelpers/app.config b/Source/EventFlow.TestHelpers/app.config index 227193e63..069e3fd6d 100644 --- a/Source/EventFlow.TestHelpers/app.config +++ b/Source/EventFlow.TestHelpers/app.config @@ -7,7 +7,7 @@ - + diff --git a/Source/EventFlow.Tests/Documentation/GettingStarted/ExampleTests.cs b/Source/EventFlow.Tests/Documentation/GettingStarted/ExampleTests.cs deleted file mode 100644 index 2f8e8597c..000000000 --- a/Source/EventFlow.Tests/Documentation/GettingStarted/ExampleTests.cs +++ /dev/null @@ -1,78 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015-2017 Rasmus Mikkelsen -// Copyright (c) 2015-2017 eBay Software Foundation -// https://github.com/eventflow/EventFlow -// -// Permission is hereby granted, free of charge, to any person obtaining a copy of -// this software and associated documentation files (the "Software"), to deal in -// the Software without restriction, including without limitation the rights to -// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -// the Software, and to permit persons to whom the Software is furnished to do so, -// subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// - -using System.Threading; -using System.Threading.Tasks; -using EventFlow.Extensions; -using EventFlow.Queries; -using EventFlow.TestHelpers; -using FluentAssertions; -using NUnit.Framework; - -namespace EventFlow.Tests.Documentation.GettingStarted -{ - [Category(Categories.Integration)] - public class ExampleTests - { - [Test] - public async Task GettingStartedExample() - { - // We wire up EventFlow with all of our classes. Instead of adding events, - // commands, etc. explicitly, we could have used the the simpler - // AddDefaults(Assembly) instead. - using (var resolver = EventFlowOptions.New - .AddEvents(typeof(ExampleEvent)) - .AddCommands(typeof(ExampleCommand)) - .AddCommandHandlers(typeof(ExampleCommandHandler)) - .UseInMemoryReadStoreFor() - .CreateResolver()) - { - // Create a new identity for our aggregate root - var exampleId = ExampleId.New; - - // Define some important value - const int magicNumber = 42; - - // Resolve the command bus and use it to publish a command - var commandBus = resolver.Resolve(); - await commandBus.PublishAsync( - new ExampleCommand(exampleId, magicNumber), - CancellationToken.None) - .ConfigureAwait(false); - - // Resolve the query handler and use the built-in query for fetching - // read models by identity to get our read model representing the - // state of our aggregate root - var queryProcessor = resolver.Resolve(); - var exampleReadModel = await queryProcessor.ProcessAsync( - new ReadModelByIdQuery(exampleId), - CancellationToken.None) - .ConfigureAwait(false); - - // Verify that the read model has the expected magic number - exampleReadModel.MagicNumber.Should().Be(42); - } - } - } -} \ No newline at end of file diff --git a/Source/EventFlow.Tests/EventFlow.Tests.csproj b/Source/EventFlow.Tests/EventFlow.Tests.csproj index a8f4e3da8..58640fcdd 100644 --- a/Source/EventFlow.Tests/EventFlow.Tests.csproj +++ b/Source/EventFlow.Tests/EventFlow.Tests.csproj @@ -8,9 +8,6 @@ - - - diff --git a/Source/EventFlow.Tests/IntegrationTests/Aggregates/AggregateStoreTests.cs b/Source/EventFlow.Tests/IntegrationTests/Aggregates/AggregateStoreTests.cs new file mode 100644 index 000000000..06b68f1d6 --- /dev/null +++ b/Source/EventFlow.Tests/IntegrationTests/Aggregates/AggregateStoreTests.cs @@ -0,0 +1,67 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015-2017 Rasmus Mikkelsen +// Copyright (c) 2015-2017 eBay Software Foundation +// https://github.com/eventflow/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using System.Threading; +using System.Threading.Tasks; +using EventFlow.Configuration; +using EventFlow.TestHelpers; +using EventFlow.TestHelpers.Aggregates; +using EventFlow.TestHelpers.Aggregates.Commands; +using EventFlow.TestHelpers.Aggregates.ValueObjects; +using FluentAssertions; +using NUnit.Framework; + +namespace EventFlow.Tests.IntegrationTests.Aggregates +{ + [Category(Categories.Integration)] + public class AggregateStoreTests : IntegrationTest + { + [TestCase(true, 1)] + [TestCase(false, 0)] + public async Task ExecutionResultShouldControlEventStore(bool isSuccess, int expectedAggregateVersion) + { + // Arrange + var pingId = PingId.New; + var thingyId = ThingyId.New; + + // Act + var executionResult = await CommandBus.PublishAsync( + new ThingyMaybePingCommand(thingyId, pingId, isSuccess), + CancellationToken.None) + .ConfigureAwait(false); + executionResult.IsSuccess.Should().Be(isSuccess); + + // Assert + var thingyAggregate = await AggregateStore.LoadAsync( + thingyId, + CancellationToken.None) + .ConfigureAwait(false); + thingyAggregate.Version.Should().Be(expectedAggregateVersion); + } + + protected override IRootResolver CreateRootResolver(IEventFlowOptions eventFlowOptions) + { + return eventFlowOptions.CreateResolver(); + } + } +} \ No newline at end of file diff --git a/Source/EventFlow.Tests/IntegrationTests/CommandResultTests.cs b/Source/EventFlow.Tests/IntegrationTests/CommandResultTests.cs new file mode 100644 index 000000000..81cd4ca23 --- /dev/null +++ b/Source/EventFlow.Tests/IntegrationTests/CommandResultTests.cs @@ -0,0 +1,118 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015-2017 Rasmus Mikkelsen +// Copyright (c) 2015-2017 eBay Software Foundation +// https://github.com/eventflow/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using System.Threading; +using System.Threading.Tasks; +using EventFlow.Aggregates.ExecutionResults; +using EventFlow.Commands; +using EventFlow.Extensions; +using EventFlow.TestHelpers; +using EventFlow.TestHelpers.Aggregates; +using EventFlow.Tests.UnitTests.Specifications; +using FluentAssertions; +using NUnit.Framework; + +namespace EventFlow.Tests.IntegrationTests +{ + [Category(Categories.Integration)] + public class CommandResultTests + { + public class TestExecutionResult : ExecutionResult + { + public TestExecutionResult( + int magicNumber, + bool isSuccess) + { + MagicNumber = magicNumber; + IsSuccess = isSuccess; + } + + public int MagicNumber { get; } + public override bool IsSuccess { get; } + } + + public class TestSuccessResultCommand : Command + { + public TestSuccessResultCommand(ThingyId aggregateId) : base(aggregateId, Core.SourceId.New) + { + } + } + + public class TestSuccessResultCommandHandler : CommandHandler + { + public override Task ExecuteCommandAsync( + ThingyAggregate aggregate, + TestSuccessResultCommand command, + CancellationToken cancellationToken) + { + return Task.FromResult(new TestExecutionResult(42, true)); + } + } + + + public class TestFailedResultCommand : Command + { + public TestFailedResultCommand(ThingyId aggregateId) : base(aggregateId, Core.SourceId.New) + { + } + } + + public class TestFailedResultCommandHandler : CommandHandler + { + public override Task ExecuteCommandAsync( + ThingyAggregate aggregate, + TestFailedResultCommand command, + CancellationToken cancellationToken) + { + var specification = new TestSpecifications.IsTrueSpecification(); + return Task.FromResult(specification.IsNotSatisfiedByAsExecutionResult(false)); + } + } + + [Test] + public async Task CommandResult() + { + using (var resolver = EventFlowOptions.New + .AddCommandHandlers( + typeof(TestSuccessResultCommandHandler), + typeof(TestFailedResultCommandHandler)) + .CreateResolver(false)) + { + var commandBus = resolver.Resolve(); + + var success = await commandBus.PublishAsync( + new TestSuccessResultCommand(ThingyId.New), + CancellationToken.None) + .ConfigureAwait(false); + success.IsSuccess.Should().BeTrue(); + success.MagicNumber.Should().Be(42); + + var failed = await commandBus.PublishAsync( + new TestFailedResultCommand(ThingyId.New), + CancellationToken.None) + .ConfigureAwait(false); + failed.IsSuccess.Should().BeFalse(); + } + } + } +} \ No newline at end of file diff --git a/Source/EventFlow.Tests/IntegrationTests/Sagas/AlternativeSagaStoreTestClasses.cs b/Source/EventFlow.Tests/IntegrationTests/Sagas/AlternativeSagaStoreTestClasses.cs index 6524b285c..f2ff670df 100644 --- a/Source/EventFlow.Tests/IntegrationTests/Sagas/AlternativeSagaStoreTestClasses.cs +++ b/Source/EventFlow.Tests/IntegrationTests/Sagas/AlternativeSagaStoreTestClasses.cs @@ -27,6 +27,7 @@ using System.Threading; using System.Threading.Tasks; using EventFlow.Aggregates; +using EventFlow.Aggregates.ExecutionResults; using EventFlow.Commands; using EventFlow.Core; using EventFlow.Sagas; @@ -126,11 +127,11 @@ public async Task PublishAsync(ICommandBus commandBus, CancellationToken cancell } } - protected void Publish( - ICommand command) + protected void Publish( + ICommand command) where TCommandAggregate : IAggregateRoot where TCommandAggregateIdentity : IIdentity - where TCommandSourceIdentity : ISourceId + where TExecutionResult : IExecutionResult { _unpublishedCommands.Add((b, c) => b.PublishAsync(command, c)); } diff --git a/Source/EventFlow.Tests/UnitTests/Aggregates/AggregateStoreTests.cs b/Source/EventFlow.Tests/UnitTests/Aggregates/AggregateStoreTests.cs index 1a0b24333..76dd9f54d 100644 --- a/Source/EventFlow.Tests/UnitTests/Aggregates/AggregateStoreTests.cs +++ b/Source/EventFlow.Tests/UnitTests/Aggregates/AggregateStoreTests.cs @@ -26,6 +26,7 @@ using System.Threading; using System.Threading.Tasks; using EventFlow.Aggregates; +using EventFlow.Aggregates.ExecutionResults; using EventFlow.Configuration; using EventFlow.Core; using EventFlow.Core.RetryStrategies; @@ -177,6 +178,74 @@ await Sut.UpdateAsync( Times.Once); } + [Test] + public async Task UpdateAsyncExecutionResult_EventsAreNotCommittedNorPublishedIfExecutionResultIsFalse() + { + // Arrange + Arrange_EventStore_LoadEventsAsync(); + Arrange_EventStore_StoreAsync(ManyDomainEvents(1).ToArray()); + + // Sut + await Sut.UpdateAsync( + A(), + A(), + (a, c) => + { + a.Ping(A()); + return Task.FromResult(ExecutionResult.Failed()); + }, + CancellationToken.None) + .ConfigureAwait(false); + + // Assert + _eventStoreMock.Verify( + m => m.StoreAsync( + It.IsAny(), + It.IsAny>(), + It.IsAny(), + It.IsAny()), + Times.Never); + _domainEventPublisherMock.Verify( + m => m.PublishAsync( + It.Is>(e => e.Count == 1), + It.IsAny()), + Times.Never); + } + + [Test] + public async Task UpdateAsyncExecutionResult_EventsAreCommittedAndPublishedIfExecutionResultIsTrue() + { + // Arrange + Arrange_EventStore_LoadEventsAsync(); + Arrange_EventStore_StoreAsync(ManyDomainEvents(1).ToArray()); + + // Sut + await Sut.UpdateAsync( + A(), + A(), + (a, c) => + { + a.Ping(A()); + return Task.FromResult(ExecutionResult.Success()); + }, + CancellationToken.None) + .ConfigureAwait(false); + + // Assert + _eventStoreMock.Verify( + m => m.StoreAsync( + It.IsAny(), + It.IsAny>(), + It.IsAny(), + It.IsAny()), + Times.Once); + _domainEventPublisherMock.Verify( + m => m.PublishAsync( + It.Is>(e => e.Count == 1), + It.IsAny()), + Times.Once); + } + private void Arrange_EventStore_StoreAsync(params IDomainEvent[] domainEvents) { _eventStoreMock diff --git a/Source/EventFlow.Tests/UnitTests/CommandBusTests.cs b/Source/EventFlow.Tests/UnitTests/CommandBusTests.cs index a547149aa..f9d69d52e 100644 --- a/Source/EventFlow.Tests/UnitTests/CommandBusTests.cs +++ b/Source/EventFlow.Tests/UnitTests/CommandBusTests.cs @@ -22,10 +22,10 @@ // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. using System; -using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using EventFlow.Aggregates; +using EventFlow.Aggregates.ExecutionResults; using EventFlow.Commands; using EventFlow.Configuration; using EventFlow.Core; @@ -61,43 +61,51 @@ public void SetUp() public async Task CommandHandlerIsInvoked() { // Arrange - ArrangeWorkingEventStore(); - var commandHandler = ArrangeCommandHandlerExists(); + ArrangeWorkingEventStore(); + var commandHandler = ArrangeCommandHandlerExists(); // Act await Sut.PublishAsync(new ThingyPingCommand(ThingyId.New, PingId.New)).ConfigureAwait(false); // Assert - commandHandler.Verify(h => h.ExecuteAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + commandHandler.Verify(h => h.ExecuteCommandAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); } - private void ArrangeWorkingEventStore() + private void ArrangeWorkingEventStore() + where TExecutionResult : IExecutionResult { _aggregateStoreMock - .Setup(s => s.UpdateAsync(It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny())) - .Callback, CancellationToken>((i, s, f, c) => f(A(), c)) - .Returns(() => Task.FromResult>(Many>())); + .Setup(s => s.UpdateAsync( + It.IsAny(), + It.IsAny(), + It.IsAny>>(), + It.IsAny())) + .Callback>, CancellationToken>((i, s, f, c) => f(A(), c)) + .Returns(() => Task.FromResult((IAggregateUpdateResult)new AggregateStore.AggregateUpdateResult(default(TExecutionResult), Many>()))); } - private void ArrangeCommandHandlerExists( - ICommandHandler commandHandler) + private void ArrangeCommandHandlerExists( + ICommandHandler commandHandler) where TAggregate : IAggregateRoot where TIdentity : IIdentity - where TSourceIdentity : ISourceId - where TCommand : ICommand + where TCommand : ICommand + where TExecutionResult : IExecutionResult { _resolverMock - .Setup(r => r.ResolveAll(typeof(ICommandHandler))) + .Setup(r => r.ResolveAll(typeof(ICommandHandler))) .Returns(new[] { commandHandler }); } - private Mock> ArrangeCommandHandlerExists() + private Mock> ArrangeCommandHandlerExists() where TAggregate : IAggregateRoot where TIdentity : IIdentity - where TSourceIdentity : ISourceId - where TCommand : ICommand + where TCommand : ICommand + where TExecutionResult : IExecutionResult { - var mock = new Mock>(); + var mock = new Mock>(); + mock + .Setup(m => m.ExecuteCommandAsync(It.IsAny(), It.IsAny(),It.IsAny())) + .Returns(Task.FromResult(default(TExecutionResult))); ArrangeCommandHandlerExists(mock.Object); return mock; } diff --git a/Source/EventFlow.Tests/UnitTests/Commands/CommandTests.cs b/Source/EventFlow.Tests/UnitTests/Commands/CommandTests.cs index 91772fa81..aec4c9e19 100644 --- a/Source/EventFlow.Tests/UnitTests/Commands/CommandTests.cs +++ b/Source/EventFlow.Tests/UnitTests/Commands/CommandTests.cs @@ -22,6 +22,7 @@ // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. using EventFlow.Aggregates; +using EventFlow.Aggregates.ExecutionResults; using EventFlow.Commands; using EventFlow.TestHelpers; using EventFlow.TestHelpers.Aggregates; @@ -34,7 +35,7 @@ namespace EventFlow.Tests.UnitTests.Commands [Category(Categories.Unit)] public class CommandTests : Test { - public class CriticalCommand : Command + public class CriticalCommand : Command { public string CriticalData { get; } diff --git a/Source/EventFlow.Tests/UnitTests/Commands/DistinctCommandTests.cs b/Source/EventFlow.Tests/UnitTests/Commands/DistinctCommandTests.cs index 3bc784519..adc3383ec 100644 --- a/Source/EventFlow.Tests/UnitTests/Commands/DistinctCommandTests.cs +++ b/Source/EventFlow.Tests/UnitTests/Commands/DistinctCommandTests.cs @@ -23,6 +23,8 @@ using System; using System.Collections.Generic; +using EventFlow.Aggregates; +using EventFlow.Aggregates.ExecutionResults; using EventFlow.Commands; using EventFlow.Extensions; using EventFlow.TestHelpers; @@ -36,7 +38,7 @@ namespace EventFlow.Tests.UnitTests.Commands [Category(Categories.Unit)] public class DistinctCommandTests { - public class MyDistinctCommand : DistinctCommand + public class MyDistinctCommand : DistinctCommand { public int MagicNumber { get; } diff --git a/Source/EventFlow.Tests/app.config b/Source/EventFlow.Tests/app.config index 227193e63..069e3fd6d 100644 --- a/Source/EventFlow.Tests/app.config +++ b/Source/EventFlow.Tests/app.config @@ -7,7 +7,7 @@ - + diff --git a/Source/EventFlow/Aggregates/AggregateStore.cs b/Source/EventFlow/Aggregates/AggregateStore.cs index aca017575..e3972a963 100644 --- a/Source/EventFlow/Aggregates/AggregateStore.cs +++ b/Source/EventFlow/Aggregates/AggregateStore.cs @@ -26,12 +26,14 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using EventFlow.Aggregates.ExecutionResults; using EventFlow.Configuration; using EventFlow.Core; using EventFlow.Core.RetryStrategies; using EventFlow.EventStores; using EventFlow.Exceptions; using EventFlow.Extensions; +using EventFlow.Logs; using EventFlow.Snapshots; using EventFlow.Subscribers; @@ -39,6 +41,8 @@ namespace EventFlow.Aggregates { public class AggregateStore : IAggregateStore { + private static readonly IReadOnlyCollection EmptyDomainEventCollection = new IDomainEvent[] { }; + private readonly ILog _log; private readonly IResolver _resolver; private readonly IAggregateFactory _aggregateFactory; private readonly IEventStore _eventStore; @@ -46,12 +50,14 @@ public class AggregateStore : IAggregateStore private readonly ITransientFaultHandler _transientFaultHandler; public AggregateStore( + ILog log, IResolver resolver, IAggregateFactory aggregateFactory, IEventStore eventStore, ISnapshotStore snapshotStore, ITransientFaultHandler transientFaultHandler) { + _log = log; _resolver = resolver; _aggregateFactory = aggregateFactory; _eventStore = eventStore; @@ -78,7 +84,30 @@ public async Task> UpdateAsync where TIdentity : IIdentity { - var domainEvents = await _transientFaultHandler.TryAsync( + var aggregateUpdateResult = await UpdateAsync( + id, + sourceId, + async (a, c) => + { + await updateAggregate(a, c).ConfigureAwait(false); + return ExecutionResult.Success(); + }, + cancellationToken) + .ConfigureAwait(false); + + return aggregateUpdateResult.DomainEvents; + } + + public async Task> UpdateAsync( + TIdentity id, + ISourceId sourceId, + Func> updateAggregate, + CancellationToken cancellationToken) + where TAggregate : IAggregateRoot + where TIdentity : IIdentity + where TExecutionResult : IExecutionResult + { + var aggregateUpdateResult = await _transientFaultHandler.TryAsync( async c => { var aggregate = await LoadAsync(id, c).ConfigureAwait(false); @@ -90,29 +119,41 @@ public async Task> UpdateAsync $"Execution failed on aggregate '{typeof(TAggregate).PrettyPrint()}', disregarding any events emitted"); + return new AggregateUpdateResult( + result, + EmptyDomainEventCollection); + } + + var domainEvents = await aggregate.CommitAsync( _eventStore, _snapshotStore, sourceId, cancellationToken) .ConfigureAwait(false); + + return new AggregateUpdateResult( + result, + domainEvents); }, Label.Named("aggregate-update"), cancellationToken) .ConfigureAwait(false); - if (domainEvents.Any()) + if (aggregateUpdateResult.Result.IsSuccess && + aggregateUpdateResult.DomainEvents.Any()) { var domainEventPublisher = _resolver.Resolve(); await domainEventPublisher.PublishAsync( - domainEvents, + aggregateUpdateResult.DomainEvents, cancellationToken) .ConfigureAwait(false); } - return domainEvents; + return aggregateUpdateResult; } public async Task> StoreAsync( @@ -140,5 +181,20 @@ await domainEventPublisher.PublishAsync( return domainEvents; } + + internal class AggregateUpdateResult : IAggregateUpdateResult + where TExecutionResult : IExecutionResult + { + public TExecutionResult Result { get; } + public IReadOnlyCollection DomainEvents { get; } + + public AggregateUpdateResult( + TExecutionResult result, + IReadOnlyCollection domainEvents) + { + Result = result; + DomainEvents = domainEvents; + } + } } } \ No newline at end of file diff --git a/Source/EventFlow.Tests/Documentation/GettingStarted/ExampleCommand.cs b/Source/EventFlow/Aggregates/AggregateUpdateResult.cs similarity index 73% rename from Source/EventFlow.Tests/Documentation/GettingStarted/ExampleCommand.cs rename to Source/EventFlow/Aggregates/AggregateUpdateResult.cs index 2b0c2799a..db143e890 100644 --- a/Source/EventFlow.Tests/Documentation/GettingStarted/ExampleCommand.cs +++ b/Source/EventFlow/Aggregates/AggregateUpdateResult.cs @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// The MIT License (MIT) // // Copyright (c) 2015-2017 Rasmus Mikkelsen // Copyright (c) 2015-2017 eBay Software Foundation @@ -20,24 +20,16 @@ // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// -using EventFlow.Commands; +using System.Collections.Generic; +using EventFlow.Aggregates.ExecutionResults; -namespace EventFlow.Tests.Documentation.GettingStarted +namespace EventFlow.Aggregates { - /// Command for update magic number - public class ExampleCommand : - Command + public interface IAggregateUpdateResult + where TExecutionResult : IExecutionResult { - public ExampleCommand( - ExampleId aggregateId, - int magicNumber) - : base(aggregateId) - { - MagicNumber = magicNumber; - } - - public int MagicNumber { get; } + TExecutionResult Result { get; } + IReadOnlyCollection DomainEvents { get; } } } \ No newline at end of file diff --git a/Source/EventFlow.Tests/Documentation/GettingStarted/ExampleCommandHandler.cs b/Source/EventFlow/Aggregates/ExecutionResults/ExecutionResult.cs similarity index 57% rename from Source/EventFlow.Tests/Documentation/GettingStarted/ExampleCommandHandler.cs rename to Source/EventFlow/Aggregates/ExecutionResults/ExecutionResult.cs index 61dd8759b..6a9c7e38c 100644 --- a/Source/EventFlow.Tests/Documentation/GettingStarted/ExampleCommandHandler.cs +++ b/Source/EventFlow/Aggregates/ExecutionResults/ExecutionResult.cs @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// The MIT License (MIT) // // Copyright (c) 2015-2017 Rasmus Mikkelsen // Copyright (c) 2015-2017 eBay Software Foundation @@ -20,25 +20,27 @@ // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// -using System.Threading; -using System.Threading.Tasks; -using EventFlow.Commands; +using System.Collections.Generic; +using System.Linq; -namespace EventFlow.Tests.Documentation.GettingStarted +namespace EventFlow.Aggregates.ExecutionResults { - /// Command handler for our command - public class ExampleCommandHandler : - CommandHandler + public abstract class ExecutionResult : IExecutionResult { - public override Task ExecuteAsync( - ExampleAggregate aggregate, - ExampleCommand command, - CancellationToken cancellationToken) + private static readonly IExecutionResult SuccessResult = new SuccessExecutionResult(); + private static readonly IExecutionResult FailedResult = new FailedExecutionResult(Enumerable.Empty()); + + public static IExecutionResult Success() => SuccessResult; + public static IExecutionResult Failed() => FailedResult; + public static IExecutionResult Failed(IEnumerable errors) => new FailedExecutionResult(errors); + public static IExecutionResult Failed(params string[] errors) => new FailedExecutionResult(errors); + + public abstract bool IsSuccess { get; } + + public override string ToString() { - aggregate.SetMagicNumer(command.MagicNumber); - return Task.FromResult(0); + return $"ExecutionResult - IsSuccess:{IsSuccess}"; } } } \ No newline at end of file diff --git a/Source/EventFlow.Tests/Documentation/GettingStarted/ExampleReadModel.cs b/Source/EventFlow/Aggregates/ExecutionResults/FailedExecutionResult.cs similarity index 65% rename from Source/EventFlow.Tests/Documentation/GettingStarted/ExampleReadModel.cs rename to Source/EventFlow/Aggregates/ExecutionResults/FailedExecutionResult.cs index 60fde0774..ee58a403f 100644 --- a/Source/EventFlow.Tests/Documentation/GettingStarted/ExampleReadModel.cs +++ b/Source/EventFlow/Aggregates/ExecutionResults/FailedExecutionResult.cs @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// The MIT License (MIT) // // Copyright (c) 2015-2017 Rasmus Mikkelsen // Copyright (c) 2015-2017 eBay Software Foundation @@ -20,25 +20,29 @@ // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// -using EventFlow.Aggregates; -using EventFlow.ReadStores; +using System.Collections.Generic; +using System.Linq; -namespace EventFlow.Tests.Documentation.GettingStarted +namespace EventFlow.Aggregates.ExecutionResults { - /// Read model for our aggregate - public class ExampleReadModel : - IReadModel, - IAmReadModelFor + public class FailedExecutionResult : ExecutionResult { - public int MagicNumber { get; private set; } + public IReadOnlyCollection Errors { get; } + + public FailedExecutionResult( + IEnumerable errors) + { + Errors = (errors ?? Enumerable.Empty()).ToList(); + } + + public override bool IsSuccess { get; } = false; - public void Apply( - IReadModelContext context, - IDomainEvent domainEvent) + public override string ToString() { - MagicNumber = domainEvent.AggregateEvent.MagicNumber; + return Errors.Any() + ? $"Failed execution due to: {string.Join(", ", Errors)}" + : "Failed execution"; } } } \ No newline at end of file diff --git a/Source/EventFlow.Tests/Documentation/GettingStarted/ExampleId.cs b/Source/EventFlow/Aggregates/ExecutionResults/IExecutionResult.cs similarity index 82% rename from Source/EventFlow.Tests/Documentation/GettingStarted/ExampleId.cs rename to Source/EventFlow/Aggregates/ExecutionResults/IExecutionResult.cs index 82e44209f..ee9e05b5d 100644 --- a/Source/EventFlow.Tests/Documentation/GettingStarted/ExampleId.cs +++ b/Source/EventFlow/Aggregates/ExecutionResults/IExecutionResult.cs @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// The MIT License (MIT) // // Copyright (c) 2015-2017 Rasmus Mikkelsen // Copyright (c) 2015-2017 eBay Software Foundation @@ -20,16 +20,11 @@ // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// - -using EventFlow.Core; -namespace EventFlow.Tests.Documentation.GettingStarted +namespace EventFlow.Aggregates.ExecutionResults { - /// Represents the aggregate identity (ID) - public class ExampleId : - Identity + public interface IExecutionResult { - public ExampleId(string value) : base(value) { } + bool IsSuccess { get; } } } \ No newline at end of file diff --git a/Source/EventFlow.Tests/Documentation/GettingStarted/ExampleEvent.cs b/Source/EventFlow/Aggregates/ExecutionResults/SuccessExecutionResult.cs similarity index 74% rename from Source/EventFlow.Tests/Documentation/GettingStarted/ExampleEvent.cs rename to Source/EventFlow/Aggregates/ExecutionResults/SuccessExecutionResult.cs index 499d7bc40..2a16f6cdb 100644 --- a/Source/EventFlow.Tests/Documentation/GettingStarted/ExampleEvent.cs +++ b/Source/EventFlow/Aggregates/ExecutionResults/SuccessExecutionResult.cs @@ -1,4 +1,4 @@ -// The MIT License (MIT) +// The MIT License (MIT) // // Copyright (c) 2015-2017 Rasmus Mikkelsen // Copyright (c) 2015-2017 eBay Software Foundation @@ -20,23 +20,16 @@ // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// - -using EventFlow.Aggregates; -using EventFlow.EventStores; -namespace EventFlow.Tests.Documentation.GettingStarted +namespace EventFlow.Aggregates.ExecutionResults { - /// A basic event containing some information - [EventVersion("example", 1)] - public class ExampleEvent : - AggregateEvent + public class SuccessExecutionResult : ExecutionResult { - public ExampleEvent(int magicNumber) + public override bool IsSuccess { get; } = true; + + public override string ToString() { - MagicNumber = magicNumber; + return "Successful execution"; } - - public int MagicNumber { get; } } } \ No newline at end of file diff --git a/Source/EventFlow/Aggregates/IAggregateStore.cs b/Source/EventFlow/Aggregates/IAggregateStore.cs index e49292d54..bb1e2fb80 100644 --- a/Source/EventFlow/Aggregates/IAggregateStore.cs +++ b/Source/EventFlow/Aggregates/IAggregateStore.cs @@ -25,6 +25,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using EventFlow.Aggregates.ExecutionResults; using EventFlow.Core; namespace EventFlow.Aggregates @@ -45,6 +46,15 @@ Task> UpdateAsync( where TAggregate : IAggregateRoot where TIdentity : IIdentity; + Task> UpdateAsync( + TIdentity id, + ISourceId sourceId, + Func> updateAggregate, + CancellationToken cancellationToken) + where TAggregate : IAggregateRoot + where TIdentity : IIdentity + where TExecutionResult : IExecutionResult; + Task> StoreAsync( TAggregate aggregate, ISourceId sourceId, diff --git a/Source/EventFlow/CommandBus.cs b/Source/EventFlow/CommandBus.cs index 51d107a29..e0838cc88 100644 --- a/Source/EventFlow/CommandBus.cs +++ b/Source/EventFlow/CommandBus.cs @@ -22,12 +22,12 @@ // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. using System; -using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Threading; using System.Threading.Tasks; using EventFlow.Aggregates; +using EventFlow.Aggregates.ExecutionResults; using EventFlow.Commands; using EventFlow.Configuration; using EventFlow.Core; @@ -57,21 +57,21 @@ public CommandBus( _memoryCache = memoryCache; } - public async Task PublishAsync( - ICommand command, + public async Task PublishAsync( + ICommand command, CancellationToken cancellationToken) where TAggregate : IAggregateRoot where TIdentity : IIdentity - where TSourceIdentity : ISourceId + where TResult : IExecutionResult { if (command == null) throw new ArgumentNullException(nameof(command)); _log.Verbose(() => $"Executing command '{command.GetType().PrettyPrint()}' with ID '{command.SourceId}' on aggregate '{typeof(TAggregate).PrettyPrint()}'"); - IReadOnlyCollection domainEvents; + IAggregateUpdateResult aggregateUpdateResult; try { - domainEvents = await ExecuteCommandAsync(command, cancellationToken).ConfigureAwait(false); + aggregateUpdateResult = await ExecuteCommandAsync(command, cancellationToken).ConfigureAwait(false); } catch (Exception exception) { @@ -86,28 +86,30 @@ public async Task PublishAsync domainEvents.Any() + _log.Verbose(() => aggregateUpdateResult.DomainEvents.Any() ? string.Format( - "Execution command '{0}' with ID '{1}' on aggregate '{2}' did NOT result in any domain events", + "Execution command '{0}' with ID '{1}' on aggregate '{2}' did NOT result in any domain events, was success:{3}", command.GetType().PrettyPrint(), command.SourceId, - typeof(TAggregate).PrettyPrint()) + typeof(TAggregate).PrettyPrint(), + aggregateUpdateResult.Result?.IsSuccess) : string.Format( - "Execution command '{0}' with ID '{1}' on aggregate '{2}' resulted in these events: {3}", + "Execution command '{0}' with ID '{1}' on aggregate '{2}' resulted in these events: {3}, was success: {4}", command.GetType().PrettyPrint(), command.SourceId, typeof(TAggregate), - string.Join(", ", domainEvents.Select(d => d.EventType.PrettyPrint())))); + string.Join(", ", aggregateUpdateResult.DomainEvents.Select(d => d.EventType.PrettyPrint())), + aggregateUpdateResult.Result?.IsSuccess)); - return command.SourceId; + return aggregateUpdateResult.Result; } - private async Task> ExecuteCommandAsync( - ICommand command, + private async Task> ExecuteCommandAsync( + ICommand command, CancellationToken cancellationToken) where TAggregate : IAggregateRoot where TIdentity : IIdentity - where TSourceIdentity : ISourceId + where TResult : IExecutionResult { var commandType = command.GetType(); var commandExecutionDetails = await GetCommandExecutionDetailsAsync(commandType, cancellationToken).ConfigureAwait(false); @@ -133,10 +135,10 @@ private async Task> ExecuteCommandAsync( + return await _aggregateStore.UpdateAsync( command.AggregateId, command.SourceId, - (a, c) => commandExecutionDetails.Invoker(commandHandler, a, command, c), + (a, c) => (Task) commandExecutionDetails.Invoker(commandHandler, a, command, c), cancellationToken) .ConfigureAwait(false); } @@ -147,6 +149,13 @@ private class CommandExecutionDetails public Func Invoker { get; set; } } + private const string NameOfExecuteCommand = nameof( + ICommandHandler< + IAggregateRoot, + IIdentity, + IExecutionResult, + ICommand, IIdentity, IExecutionResult> + >.ExecuteCommandAsync); private Task GetCommandExecutionDetailsAsync(Type commandType, CancellationToken cancellationToken) { return _memoryCache.GetOrAddAsync( @@ -162,9 +171,11 @@ private Task GetCommandExecutionDetailsAsync(Type comma var commandHandlerType = typeof(ICommandHandler<,,,>) .MakeGenericType(commandTypes[0], commandTypes[1], commandTypes[2], commandType); + + _log.Verbose(() => $"Command '{commandType.PrettyPrint()}' is resolved by '{commandHandlerType.PrettyPrint()}'"); var invokeExecuteAsync = ReflectionHelper.CompileMethodInvocation>( - commandHandlerType, "ExecuteAsync"); + commandHandlerType, NameOfExecuteCommand); return Task.FromResult(new CommandExecutionDetails { diff --git a/Source/EventFlow/Commands/Command.cs b/Source/EventFlow/Commands/Command.cs index 5d4547cf9..018dc8aa6 100644 --- a/Source/EventFlow/Commands/Command.cs +++ b/Source/EventFlow/Commands/Command.cs @@ -25,22 +25,28 @@ using System.Threading; using System.Threading.Tasks; using EventFlow.Aggregates; +using EventFlow.Aggregates.ExecutionResults; using EventFlow.Core; using EventFlow.ValueObjects; namespace EventFlow.Commands { - public abstract class Command : + public abstract class Command : ValueObject, - ICommand + ICommand where TAggregate : IAggregateRoot where TIdentity : IIdentity - where TSourceIdentity : ISourceId + where TExecutionResult : IExecutionResult { - public TSourceIdentity SourceId { get; } + public ISourceId SourceId { get; } public TIdentity AggregateId { get; } - protected Command(TIdentity aggregateId, TSourceIdentity sourceId) + protected Command(TIdentity aggregateId) + : this(aggregateId, CommandId.New) + { + } + + protected Command(TIdentity aggregateId, ISourceId sourceId) { if (aggregateId == null) throw new ArgumentNullException(nameof(aggregateId)); if (sourceId == null) throw new ArgumentNullException(nameof(aggregateId)); @@ -49,9 +55,9 @@ protected Command(TIdentity aggregateId, TSourceIdentity sourceId) SourceId = sourceId; } - public Task PublishAsync(ICommandBus commandBus, CancellationToken cancellationToken) + public async Task PublishAsync(ICommandBus commandBus, CancellationToken cancellationToken) { - return commandBus.PublishAsync(this, cancellationToken); + return await commandBus.PublishAsync(this, cancellationToken).ConfigureAwait(false); } public ISourceId GetSourceId() @@ -61,8 +67,7 @@ public ISourceId GetSourceId() } public abstract class Command : - Command, - ICommand + Command where TAggregate : IAggregateRoot where TIdentity : IIdentity { diff --git a/Source/EventFlow/Commands/CommandHandler.cs b/Source/EventFlow/Commands/CommandHandler.cs index 38ed6fe9d..95ec5e62c 100644 --- a/Source/EventFlow/Commands/CommandHandler.cs +++ b/Source/EventFlow/Commands/CommandHandler.cs @@ -24,24 +24,42 @@ using System.Threading; using System.Threading.Tasks; using EventFlow.Aggregates; +using EventFlow.Aggregates.ExecutionResults; using EventFlow.Core; namespace EventFlow.Commands { - public abstract class CommandHandler : ICommandHandler + public abstract class CommandHandler : + ICommandHandler where TAggregate : IAggregateRoot where TIdentity : IIdentity - where TSourceIdentity : ISourceId - where TCommand : ICommand + where TResult : IExecutionResult + where TCommand : ICommand { - public abstract Task ExecuteAsync(TAggregate aggregate, TCommand command, CancellationToken cancellationToken); + public abstract Task ExecuteCommandAsync( + TAggregate aggregate, + TCommand command, + CancellationToken cancellationToken); } public abstract class CommandHandler : - CommandHandler + CommandHandler where TAggregate : IAggregateRoot where TIdentity : IIdentity - where TCommand : ICommand + where TCommand : ICommand { + public override async Task ExecuteCommandAsync( + TAggregate aggregate, + TCommand command, + CancellationToken cancellationToken) + { + await ExecuteAsync(aggregate, command, cancellationToken).ConfigureAwait(false); + return ExecutionResult.Success(); + } + + public abstract Task ExecuteAsync( + TAggregate aggregate, + TCommand command, + CancellationToken cancellationToken); } } \ No newline at end of file diff --git a/Source/EventFlow/Commands/DistinctCommand.cs b/Source/EventFlow/Commands/DistinctCommand.cs index c6de4fe14..3f49e6c52 100644 --- a/Source/EventFlow/Commands/DistinctCommand.cs +++ b/Source/EventFlow/Commands/DistinctCommand.cs @@ -27,17 +27,19 @@ using System.Threading; using System.Threading.Tasks; using EventFlow.Aggregates; +using EventFlow.Aggregates.ExecutionResults; using EventFlow.Core; namespace EventFlow.Commands { - public abstract class DistinctCommand : ICommand + public abstract class DistinctCommand : ICommand where TAggregate : IAggregateRoot where TIdentity : IIdentity + where TExecutionResult : IExecutionResult { - private readonly Lazy _lazySourceId; + private readonly Lazy _lazySourceId; - public CommandId SourceId => _lazySourceId.Value; + public ISourceId SourceId => _lazySourceId.Value; public TIdentity AggregateId { get; } protected DistinctCommand( @@ -45,7 +47,7 @@ protected DistinctCommand( { if (aggregateId == null) throw new ArgumentNullException(nameof(aggregateId)); - _lazySourceId = new Lazy(CalculateSourceId, LazyThreadSafetyMode.PublicationOnly); + _lazySourceId = new Lazy(CalculateSourceId, LazyThreadSafetyMode.PublicationOnly); AggregateId = aggregateId; } @@ -60,9 +62,9 @@ private CommandId CalculateSourceId() protected abstract IEnumerable GetSourceIdComponents(); - public Task PublishAsync(ICommandBus commandBus, CancellationToken cancellationToken) + public async Task PublishAsync(ICommandBus commandBus, CancellationToken cancellationToken) { - return commandBus.PublishAsync(this, cancellationToken); + return await commandBus.PublishAsync(this, cancellationToken).ConfigureAwait(false); } public ISourceId GetSourceId() diff --git a/Source/EventFlow/Commands/ICommand.cs b/Source/EventFlow/Commands/ICommand.cs index 511491b74..fe70967eb 100644 --- a/Source/EventFlow/Commands/ICommand.cs +++ b/Source/EventFlow/Commands/ICommand.cs @@ -24,6 +24,7 @@ using System.Threading; using System.Threading.Tasks; using EventFlow.Aggregates; +using EventFlow.Aggregates.ExecutionResults; using EventFlow.Core; using EventFlow.Core.VersionedTypes; @@ -31,22 +32,16 @@ namespace EventFlow.Commands { public interface ICommand : IVersionedType { - Task PublishAsync(ICommandBus commandBus, CancellationToken cancellationToken); + Task PublishAsync(ICommandBus commandBus, CancellationToken cancellationToken); ISourceId GetSourceId(); } - public interface ICommand : ICommand + public interface ICommand : ICommand where TAggregate : IAggregateRoot where TIdentity : IIdentity - where TSourceIdentity : ISourceId + where TResult : IExecutionResult { TIdentity AggregateId { get; } - TSourceIdentity SourceId { get; } - } - - public interface ICommand : ICommand - where TAggregate : IAggregateRoot - where TIdentity : IIdentity - { + ISourceId SourceId { get; } } } \ No newline at end of file diff --git a/Source/EventFlow/Commands/ICommandHandler.cs b/Source/EventFlow/Commands/ICommandHandler.cs index 79091b751..65ed5c896 100644 --- a/Source/EventFlow/Commands/ICommandHandler.cs +++ b/Source/EventFlow/Commands/ICommandHandler.cs @@ -24,6 +24,7 @@ using System.Threading; using System.Threading.Tasks; using EventFlow.Aggregates; +using EventFlow.Aggregates.ExecutionResults; using EventFlow.Core; namespace EventFlow.Commands @@ -32,19 +33,12 @@ public interface ICommandHandler { } - public interface ICommandHandler : ICommandHandler - where TAggregate : IAggregateRoot - where TIdentity : IIdentity - where TSourceIdentity : ISourceId - where TCommand : ICommand - { - Task ExecuteAsync(TAggregate aggregate, TCommand command, CancellationToken cancellationToken); - } - - public interface ICommandHandler : ICommandHandler + public interface ICommandHandler : ICommandHandler where TAggregate : IAggregateRoot where TIdentity : IIdentity - where TCommand : ICommand + where TResult : IExecutionResult + where TCommand : ICommand { + Task ExecuteCommandAsync(TAggregate aggregate, TCommand command, CancellationToken cancellationToken); } } \ No newline at end of file diff --git a/Source/EventFlow/Commands/SerializedCommandPublisher.cs b/Source/EventFlow/Commands/SerializedCommandPublisher.cs index 28940109a..855816aa9 100644 --- a/Source/EventFlow/Commands/SerializedCommandPublisher.cs +++ b/Source/EventFlow/Commands/SerializedCommandPublisher.cs @@ -76,7 +76,8 @@ public async Task PublishSerilizedCommandAsync( throw new ArgumentException($"Failed to deserilize command '{name}' v{version}: {e.Message}", e); } - return await command.PublishAsync(_commandBus, CancellationToken.None).ConfigureAwait(false); + await command.PublishAsync(_commandBus, CancellationToken.None).ConfigureAwait(false); + return command.GetSourceId(); } } } diff --git a/Source/EventFlow/EventFlow.csproj b/Source/EventFlow/EventFlow.csproj index 378ad892a..95cd6ef76 100644 --- a/Source/EventFlow/EventFlow.csproj +++ b/Source/EventFlow/EventFlow.csproj @@ -18,13 +18,13 @@ https://raw.githubusercontent.com/eventflow/EventFlow/develop/icon-128.png https://raw.githubusercontent.com/eventflow/EventFlow/develop/LICENSE en-US - UPDATED BY BUILD + UPDATED BY BUILD bin\$(Configuration)\$(TargetFramework)\EventFlow.xml 1701;1702;1705;1591 - + All diff --git a/Source/EventFlow/Extensions/CommandBusExtensions.cs b/Source/EventFlow/Extensions/CommandBusExtensions.cs index 7e458cf3c..dfb74d5a5 100644 --- a/Source/EventFlow/Extensions/CommandBusExtensions.cs +++ b/Source/EventFlow/Extensions/CommandBusExtensions.cs @@ -23,6 +23,7 @@ using System.Threading; using EventFlow.Aggregates; +using EventFlow.Aggregates.ExecutionResults; using EventFlow.Commands; using EventFlow.Core; @@ -30,32 +31,32 @@ namespace EventFlow.Extensions { public static class CommandBusExtensions { - public static ISourceId Publish( + public static TExecutionResult Publish( this ICommandBus commandBus, - ICommand command) + ICommand command) where TAggregate : IAggregateRoot where TIdentity : IIdentity - where TSourceIdentity : ISourceId + where TExecutionResult : IExecutionResult { return commandBus.Publish(command, CancellationToken.None); } - public static ISourceId Publish( + public static TExecutionResult Publish( this ICommandBus commandBus, - ICommand command, + ICommand command, CancellationToken cancellationToken) where TAggregate : IAggregateRoot where TIdentity : IIdentity - where TSourceIdentity : ISourceId + where TExecutionResult : IExecutionResult { - ISourceId sourceId = null; + var result = default(TExecutionResult); using (var a = AsyncHelper.Wait) { - a.Run(commandBus.PublishAsync(command, cancellationToken), id => sourceId = id); + a.Run(commandBus.PublishAsync(command, cancellationToken), id => result = id); } - return sourceId; + return result; } } } \ No newline at end of file diff --git a/Source/EventFlow/Extensions/SpecificationExtensions.cs b/Source/EventFlow/Extensions/SpecificationExtensions.cs index c430e1f58..6bd770ed8 100644 --- a/Source/EventFlow/Extensions/SpecificationExtensions.cs +++ b/Source/EventFlow/Extensions/SpecificationExtensions.cs @@ -25,6 +25,7 @@ using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; +using EventFlow.Aggregates.ExecutionResults; using EventFlow.Exceptions; using EventFlow.Provided.Specifications; using EventFlow.Specifications; @@ -47,6 +48,19 @@ public static void ThrowDomainErrorIfNotStatisfied( } } + public static IExecutionResult IsNotSatisfiedByAsExecutionResult( + this ISpecification specification, + T obj) + { + var whyIsNotStatisfiedBy = specification + .WhyIsNotSatisfiedBy(obj) + .ToList(); + + return whyIsNotStatisfiedBy.Any() + ? ExecutionResult.Failed(whyIsNotStatisfiedBy) + : ExecutionResult.Success(); + } + public static ISpecification All( this IEnumerable> specifications) { diff --git a/Source/EventFlow/ICommandBus.cs b/Source/EventFlow/ICommandBus.cs index c9ae4f277..16e99a26d 100644 --- a/Source/EventFlow/ICommandBus.cs +++ b/Source/EventFlow/ICommandBus.cs @@ -24,6 +24,7 @@ using System.Threading; using System.Threading.Tasks; using EventFlow.Aggregates; +using EventFlow.Aggregates.ExecutionResults; using EventFlow.Commands; using EventFlow.Core; @@ -31,11 +32,11 @@ namespace EventFlow { public interface ICommandBus { - Task PublishAsync( - ICommand command, + Task PublishAsync( + ICommand command, CancellationToken cancellationToken) where TAggregate : IAggregateRoot where TIdentity : IIdentity - where TSourceIdentity : ISourceId; + where TExecutionResult : IExecutionResult; } } \ No newline at end of file diff --git a/Source/EventFlow/Sagas/AggregateSagas/AggregateSaga.cs b/Source/EventFlow/Sagas/AggregateSagas/AggregateSaga.cs index 6fecba48c..0f303e536 100644 --- a/Source/EventFlow/Sagas/AggregateSagas/AggregateSaga.cs +++ b/Source/EventFlow/Sagas/AggregateSagas/AggregateSaga.cs @@ -27,6 +27,7 @@ using System.Threading; using System.Threading.Tasks; using EventFlow.Aggregates; +using EventFlow.Aggregates.ExecutionResults; using EventFlow.Commands; using EventFlow.Core; @@ -50,11 +51,11 @@ protected void Complete() _isCompleted = true; } - protected void Publish( - ICommand command) + protected void Publish( + ICommand command) where TCommandAggregate : IAggregateRoot where TCommandAggregateIdentity : IIdentity - where TCommandSourceIdentity : ISourceId + where TExecutionResult : IExecutionResult { _unpublishedCommands.Add((b, c) => b.PublishAsync(command, c)); } diff --git a/appveyor.yml b/appveyor.yml index 66c863f2e..539761649 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,11 +1,7 @@ init: - git config --global core.autocrlf input -version: 0.49.{build} - -install: - - cmd: pip install -U Sphinx - - cmd: pip install sphinx_rtd_theme +version: 0.50.{build} skip_tags: true @@ -22,7 +18,6 @@ test: off artifacts: - path: Build\Packages\*.nupkg - - path: Build\Documentation\*.zip services: - mssql2014 diff --git a/build.cake b/build.cake index bd8298466..28bbd2c15 100644 --- a/build.cake +++ b/build.cake @@ -35,27 +35,16 @@ using System.Xml; var VERSION = GetArgumentVersion(); var PROJECT_DIR = Context.Environment.WorkingDirectory.FullPath; var CONFIGURATION = "Release"; -var REGEX_NUGETPARSER = new System.Text.RegularExpressions.Regex( - @"(?[a-z]+)\s+(?[a-z\.0-9]+)\s+\-\s+(?[0-9\.]+)", - System.Text.RegularExpressions.RegexOptions.IgnoreCase | System.Text.RegularExpressions.RegexOptions.Compiled); // IMPORTANT DIRECTORIES var DIR_OUTPUT_PACKAGES = System.IO.Path.Combine(PROJECT_DIR, "Build", "Packages"); var DIR_OUTPUT_REPORTS = System.IO.Path.Combine(PROJECT_DIR, "Build", "Reports"); -var DIR_OUTPUT_DOCUMENTATION = System.IO.Path.Combine(PROJECT_DIR, "Build", "Documentation"); -var DIR_DOCUMENTATION = System.IO.Path.Combine(PROJECT_DIR, "Documentation"); -var DIR_BUILT_DOCUMENTATION = System.IO.Path.Combine(DIR_DOCUMENTATION, "_build"); -var DIR_BUILT_HTML_DOCUMENTATION = System.IO.Path.Combine(DIR_BUILT_DOCUMENTATION, "html"); // IMPORTANT FILES var FILE_OPENCOVER_REPORT = System.IO.Path.Combine(DIR_OUTPUT_REPORTS, "opencover-results.xml"); var FILE_NUNIT_XML_REPORT = System.IO.Path.Combine(DIR_OUTPUT_REPORTS, "nunit-results.xml"); var FILE_NUNIT_TXT_REPORT = System.IO.Path.Combine(DIR_OUTPUT_REPORTS, "nunit-output.txt"); -var FILE_DOCUMENTATION_MAKE = System.IO.Path.Combine(DIR_DOCUMENTATION, "make.bat"); var FILE_SOLUTION = System.IO.Path.Combine(PROJECT_DIR, "EventFlow.sln"); -var FILE_OUTPUT_DOCUMENTATION_ZIP = System.IO.Path.Combine( - DIR_OUTPUT_DOCUMENTATION, - string.Format("EventFlow-HtmlDocs-v{0}.zip", VERSION)); var RELEASE_NOTES = ParseReleaseNotes(System.IO.Path.Combine(PROJECT_DIR, "RELEASE_NOTES.md")); @@ -71,8 +60,6 @@ Task("Clean") { DIR_OUTPUT_PACKAGES, DIR_OUTPUT_REPORTS, - DIR_OUTPUT_DOCUMENTATION, - DIR_BUILT_DOCUMENTATION, }); DeleteDirectories(GetDirectories("**/bin"), true); @@ -116,7 +103,7 @@ Task("Test") { ExecuteTest("./Source/**/bin/" + CONFIGURATION + "/**/EventFlow*Tests.dll", FILE_NUNIT_XML_REPORT); }) - .Finally(() => + .Finally(() => { UploadArtifact(FILE_NUNIT_TXT_REPORT); UploadTestResults(FILE_NUNIT_XML_REPORT); @@ -155,20 +142,9 @@ Task("Package") } }); -// ===================================================================================================== -Task("Documentation") - .IsDependentOn("Clean") - .Does(() => - { - ExecuteCommand(FILE_DOCUMENTATION_MAKE, "html", DIR_DOCUMENTATION); - - ZipFile.CreateFromDirectory(DIR_BUILT_HTML_DOCUMENTATION, FILE_OUTPUT_DOCUMENTATION_ZIP); - }); - // ===================================================================================================== Task("All") .IsDependentOn("Package") - .IsDependentOn("Documentation") .Does(() => { @@ -334,7 +310,13 @@ void ExecuteTest(string files, string resultsFile) NoColor = true, DisposeRunners = true, OutputFile = FILE_NUNIT_TXT_REPORT, - Results = resultsFile + Results = new [] + { + new NUnit3Result + { + FileName = resultsFile, + } + } }); }, new FilePath(FILE_OPENCOVER_REPORT),