Code Monkey home page Code Monkey logo

be.cqrs's Introduction

CQRS

Common

This project represents a interpretation of the CQRS pattern, setting the focus on domainobjects, states and repositories. Although this project is already used in some real world projects like groupl I can't provide you any guarantues on functionality or support.

Currently a process of migration, documentation and samples creation is in progress. If you have any questions or wished please drop an issue here.

This project doesn't claim to be a prefect cqrs implementation. As most patterns CQRS has also many different real world interpretations.

Usage-Example

This is how a very basic domainobject looks like

public sealed class SessionDomainObject : DomainObjectBase
    {
        private readonly ILessonFactory lessonFactory;
        
        public SessionDomainObject(string id,ILessonFactory lessonFactory) : base(id,null)
        {
            Console.WriteLine("Creating object");

            this.lessonFactory = lessonFactory;
        }

        [Create]
        public void CreateSession(StartSessionForUserCommand cmd)
        {
            Precondition.For(cmd, nameof(cmd)).IsValidModel();
            
            ILesson lesson = lessonFactory.FromShortKey(cmd.LessonKey);

            IWorksheet worksheet = lesson.CreateWorksheet(new WorksheetArgument());
            
            
            RaiseEvent<SessionCreatedEvent>(@event =>
            {
                @event.Started = DateTimeOffset.Now;
                @event.UserId = cmd.UserId;
                @event.LessonKey = cmd.LessonKey;
                @event.WorksheetItems = worksheet.Items.ToEvent()
            });
        }
    }

Following an example for an api controler which triggers the creation process. The bus sends the command to a handler which creates the domain object, processes the events and persists the result

[HttpPost]
public async Task<IActionResult> Post([FromBody][Required] StartSessionModel model)
{
    Precondition.For(model, nameof(model)).NotNull().IsValidModel();

   await bus.EnqueueAsync(new StartSessionForUserCommand(Guid.NewGuid().ToString(), model.LessonKey, userId));

    return Accepted();
}

Persistance

Variant persistance implementations can be achieved by subclassing the domain object repository base. Currently two databases are implemented:

The EventStore implementation is already used in production scenarios , the mongodb is currentlyin a exeperimentell state.

Configuration-Examples

To get started have a look at the sample directory.

Adding the write part

MongoDb as EventStore, and asp.core serviceprovider for di

public static IServiceCollection AddCqrs(this IServiceCollection services, IConfiguration config)
    {
        string eventSecret = "0mDJVERJ34e4qLC6JYvT!$_d#+54d";
        var esconfig = new EventSourceConfiguration()
            .SetEventSecret(eventSecret)
            .SetDomainObjectAssemblies(typeof(DomainObjectSample).Assembly);

        string url = config["events:host"];
        string db = config["events:db"];
            
        services
            .AddServiceProviderDomainObjectAcitvator()
            .AddMongoDomainObjectRepository(()=>new MongoClient(url).GetDatabase(db))
            .AddConventionBasedInMemoryCommandBus(esconfig)
            .AddEventSource(esconfig);
        }

Adding the denormalizers

public static IServiceCollection AddDenormalizers(this IServiceCollection services, IConfiguration config)
{
    IMongoDatabase readDb = new MongoClient(readDburl).GetDatabase(readdb);

    var ctx = new DenormalizerContext(client, readDb);
    services.AddSingleton<IDenormalizerContext>(ctx);

   DenormalizerConfiguration deconfig = new DenormalizerConfiguration()
   .SetDenormalizerAssemblies(typeof(SampleDenormalizer).Assembly);

   services
       .AddServiceProviderDenormalizerActivator()
       .AddImmediateDenormalization()
       .AddDenormalization(deconfig);
   
   return services;
}
     

Ressources

To get started I strongly recommend to have a look at the awesome CQRS Webcasts by GregYoung.

Key-Concepts

DomainObject

The DomainObject is the center of each application logic. A very basic example is the Customer Domain Object in our first sample.

DomainObjectRepository

The DomainObjectRepository is responsible for persisting and reading the eventstreams of the domainobject. A very basic example is the usage of the MongoDomainObjectRepository which is registered first in the CQRSBooter in our first sample. Its usage is shown in the Program.cs of the same sample.

States

States are visitors that are iterating over stream of events to determine a desired state. The NameState for the Customer Domain Object determines the Name State for a customer. Accessing the NameState is also shown in the first sample's Program.cs.

Policies

Policies are specialized states which result in a boolean value. For example "IsCustomerActiveState"

CommandBus

CommandBus is used to send commands and in order to find their related Domain Objects and Processes that can handle the given Command. The CommandBus is registered in the CQRSBooter.cs of our second sample. After registration, it can be accessed in the CustomersController and enqueue Commmands.

EventSubscribers

EventSubscriber connect to eventstreams and provide notifications on new events. The [MongoDbEventSubscriber] is registered in the CQRSBooter.cs of our third sample.

Denormalizer

Denormalizers are working with eventsubscriber and are publishing new events to they registered eventhandlers e.g. for projecting informations in databases. By registering the CustomerDenormalizer in the CQRSBooter it can subscribe to the Notifications of the MongoDbEventSubscriber and create a CustomerReadModel.

be.cqrs's People

Contributors

alexzeitler avatar boase avatar cypressious avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

be.cqrs's Issues

Allow moving/renaming Domain Object

Currently, the full name of the domain object including namespace is persisted. Allow moving or renaming the domain object while keeping backward compatibility, e.g. by providing a mapping from old full name to new full name, either in the configuration or in an attribute.

No Exceptions logged in Denormalizer Repository

Given this Customer class:

 public sealed class Customer
    {
        [BsonId]
        public ObjectId Id { get; set; }
        public string Name { get; set; }
    }

And this Repository:

    {
        private readonly IDenormalizerContext context;

        public CustomerRepository(IDenormalizerContext context) : base(context.Db, "Customer")
        {
            this.context = context;
        }

        public Task AddCustomer(string customerId)
        {
            var dto = new Customer
            {
                Id = new ObjectId(customerId)
            };

            return Collection.InsertOneAsync(dto);
        }
    }

When debbuging in Rider, I get this exception:

System.FormatException: String should contain only hexadecimal digits.   at MongoDB.Bson.BsonUtils.ParseHexString(String s)   at MongoDB.Bson.ObjectId..ctor(String value)   at AspNetCoreSample.Denormalizer.Repositories.CustomerRepository.AddCustomer(String customerId) in /home/alexzeitler/src/github.com/BE.CQRS-15/Samples/3_ASPNET_Core_ReadModels/AspNetCoreSample.Denormalizer/Repositories/CustomerRepository.cs:line 18   at AspNetCoreSample.Denormalizer.CustomerDenormalizer.On(CustomerCreatedFromApiEvent event) in /home/alexzeitler/src/github.com/BE.CQRS-15/Samples/3_ASPNET_Core_ReadModels/AspNetCoreSample.Denormalizer/CustomerDenormalizer.cs:line 26

But the application does not log any errors on console.

Commit IDs are empty string

Issue appears repeatedly and with no recognisable pattern so far. The stacktrace is the following:

[11:14:09 ERR] Error handling command "Csi.Identities.Commands.AddPhoneCommand" - '' is not a valid 24 digit hex string.
System.FormatException: '' is not a valid 24 digit hex string.
   at MongoDB.Bson.ObjectId.Parse(String s)
   at MongoDB.Bson.BsonTypeMapper.Convert(Object value, Conversion conversion)
   at MongoDB.Bson.BsonTypeMapper.MapToBsonValue(Object value, BsonType bsonType)
   at MongoDB.Bson.BsonObjectId.Create(Object value)
   at BE.CQRS.Data.MongoDb.Commits.MongoCommitRepository.ByInternalId(String commitId)
   at BE.CQRS.Data.MongoDb.MongoDomainObjectRepository.ByAppendResult(AppendResult result)
   at BE.CQRS.Domain.DomainObjectRepositoryBase.SaveAsync[T](T domainObject, Boolean preventVersionCheck)
   at BE.CQRS.Domain.Conventions.ConventionCommandInvoker.InvokeAndSaveInternalAsync(Type domainObjectType, ICommand cmd, IEnumerable`1 commands)
   at BE.CQRS.Domain.Conventions.ConventionCommandInvoker.InvokeAndSaveAsync(Type domainObjectType, ICommand cmd, IEnumerable`1 commandMapping)
   at BE.CQRS.Domain.Commands.InMemoryCommandBus.HandleCommand(ICommand cmd)

Issues when duplicate domain object assembly calls

  new EventSourceConfiguration()
          .SetDomainObjectAssemblies(typeof(RealmDomainObject).Assembly)
          .SetDomainObjectAssemblies(typeof(LocalizationDomainObject).Assembly) // <= duplicate assembly
          .SetServiceProviderActivator()

Direct Denormalizer Call After Save

Create a possibility to run the denormalizers in a way so that they are called right after saving the event. This way the usage of a messagebus cam be omitted.

This will then make it required to have a simple possibility to run the creation of the read database

Di / DataContext Classes

to improve dependencies it would be helpfull to have Di-/ Data - Context Classes which can be recieved from the service provider.

this would also reduce the size of the ctor and so son

image

"Type" property in log causes conflict with Logstash

Description

When inserting events into the repository, the generated logs use the a Type property:
MongoCommitRepository
Line 194
logger.LogError(e, "Error when saving a commit for {type} {id}", commit.AggregateType, commit.AggregateId);
and Line 203
logger.LogDebug("{Count} events for {Type} - {Id} handled. Result: {Result}.", commit.Events.Count, commit.AggregateType, commit.AggregateId, result.CommitId);

When using structured logs, the Type property is usually already in use by some data processing pipelines, in this particular Logstash, which then can cause errors.

Proposal

Rename the Type property to AggregateType to prevent such conflicts

Support Logging Infrastructure

It should be possible to optin into logging messages i.g. to print them to the console

Idea:

  • Support a common LoggingInterface
  • Provide da Default Console implementation
  • Create Extension Methods for simple bootstrapping

Allow to pass serilog enricher for serilog logging config

This would allow to e.g. implement a Enricher (outside BE.CQRS) which handles correlation id and passing the Enricher to the config, the correlation id would also be used in BE.CQRS, thus it wouldn't be a black box correlation logging wise.

Fix broken DomainObjectTests

[xUnit.net 00:00:01.1304280]     BE.CQRS.Domain.Tests.DomainObjectTests.Visiting.WhenVisiting.ItProcessedAppliedAndUncommittedEvents [FAIL]
[xUnit.net 00:00:01.1365290]     BE.CQRS.Domain.Tests.ConventionEventhandlerTests.WhenEventIsRaised.ItCallsTheMethod [FAIL]
Failed   BE.CQRS.Domain.Tests.DomainObjectTests.Visiting.WhenVisiting.ItProcessedAppliedAndUncommittedEvents
Error Message:
 System.NullReferenceException : Object reference not set to an instance of an object.
Stack Trace:
   at BE.CQRS.Domain.DomainObjects.DomainObjectBase.StateInternal[T](Boolean excludeUncommitted) in /Users/alexzeitler/src/github.com/BE.CQRS/src/Domain/Domain/DomainObjects/DomainObjectBase.cs:line 195
   at BE.CQRS.Domain.DomainObjects.DomainObjectBase.State[T]() in /Users/alexzeitler/src/github.com/BE.CQRS/src/Domain/Domain/DomainObjects/DomainObjectBase.cs:line 185
   at BE.CQRS.Domain.Tests.DomainObjectTests.Visiting.WhenVisiting..ctor() in /Users/alexzeitler/src/github.com/BE.CQRS/src/Domain/Domain.Tests/DomainObjectTests/Visiting/WhenVisiting.cs:line 26
Failed   BE.CQRS.Domain.Tests.ConventionEventhandlerTests.WhenEventIsRaised.ItCallsTheMethod
Error Message:
 System.ArgumentNullException : Value cannot be null.
Parameter name: logger
Stack Trace:
   at Microsoft.Extensions.Logging.LoggerExtensions.Log(ILogger logger, LogLevel logLevel, EventId eventId, Exception exception, String message, Object[] args)
   at Microsoft.Extensions.Logging.LoggerExtensions.LogTrace(ILogger logger, String message, Object[] args)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.