Giter Site home page Giter Site logo

kgrzybek / sample-dotnet-core-cqrs-api Goto Github PK

View Code? Open in Web Editor NEW
2.8K 2.8K 624.0 388 KB

Sample .NET Core REST API CQRS implementation with raw SQL and DDD using Clean Architecture.

Home Page: https://www.kamilgrzybek.com/design/simple-cqrs-implementation-with-raw-sql-and-ddd/

License: MIT License

C# 97.53% TSQL 2.47%
clean-architecture clean-code cqrs cqrs-simple dapper ddd ddd-cqrs ddd-example design-patterns design-systems domain-driven-design dotnercore dotnet entity-framework entity-framework-core mediatr rest rest-api software-architecture

sample-dotnet-core-cqrs-api's Introduction

Sample .NET Core REST API CQRS implementation with raw SQL and DDD using Clean Architecture.

CI

Give a Star! โญ

If you like this project, learn something or you are using it in your applications, please give it a star. Thanks!

Description

Sample .NET Core REST API application implemented with basic CQRS approach and Domain Driven Design.

Architecture Clean Architecture

projects_dependencies

CQRS

Read Model - executing raw SQL scripts on database views objects (using Dapper).

Write Model - Domain Driven Design approach (using Entity Framework Core).

Commands/Queries/Domain Events handling using MediatR library.

Domain

projects_dependencies

Validation

Data validation using FluentValidation

Problem Details for HTTP APIs standard implementation using ProblemDetails

Caching

Using Cache-Aside pattern and in-memory cache.

Integration

Outbox Pattern implementation using Quartz.NET

Related blog articles

Simple CQRS implementation with raw SQL and DDD

Domain Model Encapsulation and PI with Entity Framework 2.2

REST API Data Validation

Domain Model Validation

How to publish and handle Domain Events

Handling Domain Events: Missing Part

Cache-Aside Pattern in .NET Core

The Outbox Pattern

How to run application

  1. Create empty database.
  2. Execute InitializeDatabase.sql script.
  3. Set connection string (in appsettings.json or by user secrets mechanism).
  4. Run!

How to run Integration Tests

  1. Create empty database.
  2. Execute InitializeDatabase.sql script.
  3. Set connection string using environment variable named ASPNETCORE_SampleProject_IntegrationTests_ConnectionString

sample-dotnet-core-cqrs-api's People

Contributors

ampr64 avatar barryyue avatar jalbrzym avatar jurajvt avatar kgrzybek avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

sample-dotnet-core-cqrs-api's Issues

Migrations

Thank you for an excellent framework.

Attempting to run: add-migrations init, I get the following error message in PM:

Unable to create an object of type 'OrdersContext'. For the different patterns supported at design time, see https://go.microsoft.com/fwlink/?linkid=851728

Any suggestions on how I can get through this issue?

Thank you.

Problem with context.SaveChangesAsync() after removing objects from List<>

Hi,
I've recently encountered a problem causing throwing an InvalidOperationException showed below.

System.InvalidOperationException: Failed to compare two elements in the array.
 ---> System.ArgumentException: At least one object must implement IComparable.
   at System.Collections.Comparer.Compare(Object a, Object b)
   at System.Collections.Generic.ObjectComparer`1.Compare(T x, T y)
   at Microsoft.EntityFrameworkCore.Update.Internal.ModificationCommandComparer.Compare(ModificationCommand x, ModificationCommand y)

The problem occurs when EntityFramework's SaveChangesAsync() method is called after removing a few objects from the property of type List of some Entity. I've tried Remove(object), RemoveAt(index) and RemoveAll(lambdaExpression) methods. The solution was to implement IComparable interface to TypedIdValueBase class.

"Light" OrderDto

Hi Kamil, thank you for the posts and examples!
I would like to put a scenario to understand your opinion about it.
Imagine that in UI you need a combobox of Orders and the only information needed is Id and Value.
The query is the same that is already done in "GetCustomerOrdersQueryHandler", the only change is that the object that the UI needs has less information.
How would you solve this scenario?
[]'s.

Customer as Aggregate

Why did you use a customer as aggregate root instead of the order?
Whenever I see the examples about DDD, they select the order as an aggregate.

Is it mandatory to use Guid for Id in entity or aggregate?

hello @kgrzybek ,

We have learned in the past that searching based on Guid primary key is less performant as compared to Sequential Integer/Long primary key.

Is there any work around so we can get the best of both worlds - leverage Outbox for gurantee and search friendly primary key?
Is it possible to do like this in command handler?

I am thinking to have 3 methods in UnitOfWork

  1. SaveChanges - to flush entity changes - explicit call in command handler and fetch Primary Key
  2. BeginTransaction - to start database transaction - from decorator
  3. Commit - to commit transaction - from decorator

REference: https://entityframeworkcore.com/saving-data-transaction
Commit method can Invoke DomainEventDispatcher which in turn can record DomainEventNotifications in Outbox table.

Open for other ideas?

StronglyTypedIdValueConverterSelector should be Singleton

Thank you for your work!

I think StronglyTypedIdValueConverterSelector should be registered as a singleton.

builder.RegisterType<StronglyTypedIdValueConverterSelector>()
                .As<IValueConverterSelector>()
                .InstancePerLifetimeScope();

Captured from EF internals.

[EntityFrameworkInternal]
public static readonly IDictionary<Type, ServiceCharacteristics> CoreServices
	= new Dictionary<Type, ServiceCharacteristics>
	{
		(...)
		{ typeof(IValueConverterSelector), new ServiceCharacteristics(ServiceLifetime.Singleton) },
		(...)

Raw SQL in application layer

Hi Kamil,
Thank you for your great sample,
I have a question. Why do you write the raw queries in the application layer?

Example: "GetCustomerDetailsQueryHandler"
the application layer knows the detailed structure of the database layer. Does this violate the separation of concerns principle?
`
public Task Handle(GetCustomerDetailsQuery request, CancellationToken cancellationToken)
{
const string sql = "SELECT " +
"[Customer].[Id], " +
"[Customer].[Name], " +
"[Customer].[Email], " +
"[Customer].[WelcomeEmailWasSent] " +
"FROM orders.v_Customers AS [Customer] " +
"WHERE [Customer].[Id] = @CustomerId ";

        var connection = _sqlConnectionFactory.GetOpenConnection();

        return connection.QuerySingleAsync<CustomerDetailsDto>(sql, new
        {
            request.CustomerId
        });
    }`

many thanks,
truonglx.

DDD EF Core Remove issue

Let's say we have a classes:

public class Order 
{
    public int Id { get; set; }
    public List<OrderItem> OrderItems { get; set; }
    // other properties
}
public class OrderItem
{
    public int Id { get; set; }
    public int OrderId { get; set; }
    public Order Order { get; set; }
    // other properties
}

Order has primary key on Id property, OrderItem as well. OrderItem has foreign key to Order with OrderId property. Simple as that. Both are autoincremented.

Is there any way to remove OrderItem from Order with RemoveOrderItem defined method in Order class with using Entity Framework Core and not setting primary key on two fields (Id, OrderId) in OrderItem class (or any different key breaking changes workarounds)?

SO

Decorator registration is not working with Autofac 6.x

Hi,
I've implemented CQRS by following your repo. However I noticed that the decorator registration is now working with Autofac 6.x

builder.RegisterGenericDecorator(
typeof(UnitOfWorkCommandHandlerWithResultDecorator<,>),
typeof(ICommandHandler<,>));

I found different links on github with same issue but none of them really helps
autofac/Autofac#1098
kgrzybek/modular-monolith-with-ddd#68

Can you explain how can we solve this issue with latest version of Autofac?

Product existence check / Asynchronous domain

Hi Kamil,

very great example and reading by the way!

How can you make sure that the products you are adding to an order (PlaceCustomerOrderCommand) really exists?
It may of course be that this topic has been left out for code brevity.
But it seems still important to me to check that the ProductDto with the given ProductId really exists, so that we do not generate any data garbage.

Where do you think this check would take place? In my opinion it is a domain concern. Adding an invalid item to the invoice would break a domain rule. In this case, an IProductExistenceChecker could help. This would be implemented in a similar way to the CustomerUniquessChecker and passed as a dependency / parameter. What do you think about it?

But that leads me to another question. CustomerUniquessChecker is using a synchronous database call to check if the email already exists. Since this check does not take place very often, it may be okay in this case. But when we check the existence of a product, this query is used far more frequently.
Would it make sense to design the domain asynchronously to prevent a bottleneck? For example:

public async Task<OrderId> PlaceOrderAsync(
    List<OrderProductData> orderProductsData,
    List<ProductPriceData> allProductPrices,
    string currency, 
    List<ConversionRate> conversionRates,
    IProductExistenceChecker productExistenceChecker)
{
    await CheckRuleAsync(new ProductsExistRule(orderProductsData, productExistenceChecker));
    CheckRule(new CustomerCannotOrderMoreThan2OrdersOnTheSameDayRule(_orders));
    CheckRule(new OrderMustHaveAtLeastOneProductRule(orderProductsData));

    var order = Order.CreateNew(orderProductsData, allProductPrices, currency, conversionRates);

    this._orders.Add(order);

    this.AddDomainEvent(new OrderPlacedEvent(order.Id, this.Id, order.GetValue()));

    return order.Id;
}

Or should caching be the way to go and stay synchronous?

Query Handlers and responsabilities

Hi, congratulations for the effort putting this up.

I have one question regarding the query handlers. Why are they in Application? Doesn't that mean that you are coupling your Infrastructure with business logic?
Just want to understand what weighted your decision and what you would suggest for a more strict decoupling.

Communication between contexts - Implement RemoveCustomer method that checks if customer has any orders.

Hi

First of all, thanks for all of the examples! I have one more question regarding the code, since I found that most of DDD examples show really simple scenario. I believe that for beginners the most difficult part is how to implement communication between contexts. I understand that this should be limited as much as possible, but it will be there and there are no real examples how to deal with it.

For example, let's assume that you add RemoveCustomer method to CustomerController and that we have business logic that spans across customer and orders that you can't remove customer if it has any orders. How something like that would be implemented, where and how would you obtain information about customer orders? Or maybe would you reverse dependency and track number of orders in customer context by subscribing to an event of order created / removed (that would be probably more work)?

Thanks again for all of the code examples!

Ordering of DomainEventNotifications in OutboxMessages table

Hi, I appreciate your work a lot ๐Ÿ’ฏ ๐Ÿ‘

I have one question.

Why in DomainEventsDispatcher do you decide firstly to publish DomainEvents to the specific Handlers and then only save DomainEventNotification into OutboxMessages?

image

In this case, the first handled commands will save DomainEventNotifications last, and the last handled commands first.
So the order is vice versa.

I saw that you also have OccurredOn but you do not use it during processing DomainEventNotifications in ProcessOutboxCommandHandler.

image

The only use of this property is in the Test OutboxMessagesHelper class.

image

In my case, after I used your code I can see that Messages in a Queue are in reverse order of how commands handlers and event handlers were processed.

Isn't it better to save DomainEventNotification into OutboxMessages first and then publish DomainEvents to the specific Handlers?

image

How to handle unique constraint with user friendly message

Sorry, my English is poor.

If the customer's email has unique constraint.
When call RegisterCustomer api with duplicate email,
the method in UnitOfWork.CommitAsync will throw DbUpdateException,
how to handle this and return friendly message to frontend, like this

{
   errorCode: 409001
   message: Duplicated email.
}

the errorCode is unique which defined by application,
and has a api document for frontend, like this

errorCode message
409001 Duplicated email
401001 Unauthorized
...... ......

frontend use errorCode to show user friendly message.

I found most solution is,

public class RegisterCustomerCommandHandler : IRequestHandler<RegisterCustomerCommand, CustomerDto>
{
        ......

        public async Task<CustomerDto> Handle(RegisterCustomerCommand request, CancellationToken cancellationToken)
        {
            var customer = new Customer(request.Email, request.Name, this._customerUniquenessChecker);

	    if (EmailAlreadyExist())
	    {
		throw new EmailAlreadyExistException(request.Email);
	    }

            await this._customerRepository.AddAsync(customer);

            await this._unitOfWork.CommitAsync(cancellationToken);

            return new CustomerDto { Id = customer.Id.Value };
        }
}

and use a ExceptionFilter to catch EmailAlreadyExistException.

But this can't handle concurrent insert,
if two requests send at same time with same email,
two requests will pass EmailAlreadyExist,
the method in UnitOfWork.CommitAsync will throw DbUpdateException,
this solution don't solve problem.

Maybe this happen rarely, I can return a response with 500(Internal Server Error), and let user try again.

Maybe I can use a lock in database, but most suggest don't do this, because of performance.

Maybe I can use Optimistic Concurrency Control, so I need write my code like this

public class RegisterCustomerCommandHandler : IRequestHandler<RegisterCustomerCommand, CustomerDto>
{
        ......

        public async Task<CustomerDto> Handle(RegisterCustomerCommand request, CancellationToken cancellationToken)
        {
            var customer = new Customer(request.Email, request.Name, this._customerUniquenessChecker);

	    if (EmailAlreadyExist())
	    {
		throw new EmailAlreadyExistException(request.Email);
	    }

            await this._customerRepository.AddAsync(customer);

	   try
	   {
		await this._unitOfWork.CommitAsync(cancellationToken);
	   }
	   catch(DbUpdateException)
	   {
		//do something
	   }

            return new CustomerDto { Id = customer.Id.Value };
        }
}

But it may has many change in a database transaction, I can't know who throw the DbUpdateException.

If I has a situation, user can update same record, and it happen frequently, user may complain.

How to handle this gracefully.

Allowed changes in DomainEventHandler - lack of example

Hello Kamil,

Thanks for that repo - a lot of valuable knowledge ๐Ÿ˜Š

I have a question regarding allowed changes in your DomainEventHandlers - there is no code in your OrderAddedDomainEventHandler.cs. It seems that changes in another aggregate in handler won't be published as events. For example:

  • In OrderAddedDomainEventHandler we get Buyer aggregate
  • We add order to the buyer
  • We have an event to publish but nothing will gather this event
    • If we run CommitAsync from context code will SaveChangesAsync twice
    • If not we won't run DispatchEventsAsync from the dispatcher

So we are not able to implement fully the scenario of triggering side effects across multiple aggregates (Microsoft docs)

What do you think of it? Maybe you have a different perspective on this topic ๐Ÿ˜‰

Should we persist this to the database?

Hey Kamil,
First thanks for the great example! I'm learning a lot from it!
While creating a new order I can't see where those are persisted on the database and consequently can't see any processing of outbox item.
Is it the case that we should persist the order on the db after the line bellow?

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.