Repository files navigation

CoreKernel Library

logo


The CoreKernel library provides a comprehensive set of foundational abstractions and utilities for building robust, modular, and maintainable .NET applications. It implements patterns from Domain-Driven Design (DDD), functional programming, and modern architectural approaches like CQRS and Event-Driven Architecture.

Table of Contents


Overview

The CoreKernel library is a comprehensive toolkit designed to simplify application development by providing:

  • DDD building blocks for modeling your business domain with clarity and precision
  • Functional programming constructs for safer, more expressive code
  • Cross-cutting concerns handled through domain markers
  • Messaging infrastructure for implementing CQRS and event-driven architecture
  • Consistent error handling through the Result pattern

By adopting these patterns early in your development process, you can ensure better code maintainability, domain integrity, and system scalability.

Components

The CoreKernel library consists of the following components:

  • CoreKernel.Primitives: Domain-Driven Design building blocks (entities, value objects, aggregates)
  • CoreKernel.DomainMarkers: Interfaces for cross-cutting concerns (auditing, multi-tenancy, etc.)
  • CoreKernel.Functional: Functional programming abstractions (Result, Maybe, Error types)
  • CoreKernel.Messaging: CQRS and event-driven architecture support (commands, queries, events)

Each component can be used independently or together for a complete development experience.

Installation

To use the full CoreKernel library in your project:

dotnet add package CoreKernel

Or, to use individual components:

dotnet add package CoreKernel.Primitives
dotnet add package CoreKernel.DomainMarkers
dotnet add package CoreKernel.Functional
dotnet add package CoreKernel.Messaging

Usage

Domain-Driven Design Primitives

Entities

The Entity<TId> base class provides identity-based equality comparison:

publicclassProduct:Entity<Guid>{publicstringName{get;}publicdecimalPrice{get;privateset;}publicProduct(Guidid,stringname,decimalprice):base(id){Name=name;Price=price;}publicvoidUpdatePrice(decimalnewPrice){Price=newPrice;}}

Entities are compared by ID and type:

varproduct1=newProduct(Guid.NewGuid(),"Widget",10.99m);varproduct2=newProduct(product1.Id,"Widget",10.99m);boolareEqual=product1==product2;// true, same ID

Aggregate Roots

Aggregate roots are the entry points to domain aggregates and can raise domain events:

publicclassOrder:AggregateRoot<Guid>{privatereadonlyList<OrderLine>_orderLines=new();publicIReadOnlyCollection<OrderLine>OrderLines=>_orderLines.AsReadOnly();publicdecimalTotalAmount{get;privateset;}publicOrder(Guidid):base(id){TotalAmount=0;}publicvoidAddOrderLine(Productproduct,intquantity){varorderLine=newOrderLine(Guid.NewGuid(),product.Id,product.Price,quantity);_orderLines.Add(orderLine);TotalAmount+=orderLine.LineTotal;RaiseDomainEvent(newOrderLineAddedEvent(Id,orderLine.Id));}publicvoidPlace(){// Business logic for placing an orderRaiseDomainEvent(newOrderPlacedEvent(Id));}}

Working with domain events:

// Create and manipulate the ordervarorder=newOrder(Guid.NewGuid());order.AddOrderLine(product,2);order.Place();// Retrieve domain events for processingvarevents=order.GetDomainEvents();// Process events...order.ClearDomainEvents();// Clear after processing

Value Objects

Value objects encapsulate concepts that are distinguished by their attributes rather than identity:

publicclassMoney:ValueObject{publicdecimalAmount{get;}publicstringCurrency{get;}publicMoney(decimalamount,stringcurrency){Amount=amount;Currency=currency;}protectedoverrideIEnumerable<object>GetAtomicValues(){yieldreturnAmount;yieldreturnCurrency;}publicMoneyAdd(Moneyother){if(Currency!=other.Currency)thrownewInvalidOperationException("Cannot add different currencies");returnnewMoney(Amount+other.Amount,Currency);}}

Value objects are compared by their structure, not reference:

varprice1=newMoney(100,"USD");varprice2=newMoney(100,"USD");boolareEqual=price1==price2;// true, same values

Strongly-Typed IDs

Strongly-typed IDs prevent accidental ID misuse between different entity types:

publicclassUserId:StronglyTypedId<Guid>{publicUserId(Guidvalue):base(value){}}publicclassOrderId:StronglyTypedId<Guid>{publicOrderId(Guidvalue):base(value){}}publicclassUser:Entity<UserId>{publicstringName{get;}publicUser(UserIdid,stringname):base(id){Name=name;}}

This prevents accidentally passing an OrderId to a method expecting a UserId.

Domain Markers

Auditing

Auditing interfaces track entity creation and modification:

// Basic time trackingpublicclassBlogPost:Entity<Guid>,ITimeStamped{publicstringTitle{get;set;}publicstringContent{get;set;}// ITimeStamped implementationpublicDateTimeOffsetCreatedOn{get;set;}publicDateTimeOffsetLastModifiedOn{get;set;}}// Complete auditing with user trackingpublicclassInvoice:Entity<Guid>,IAuditable{publicdecimalAmount{get;set;}publicstringCustomerName{get;set;}// IAuditable implementationpublicDateTimeOffsetCreatedOn{get;set;}publicstringCreatedBy{get;set;}publicDateTimeOffsetLastModifiedOn{get;set;}publicstringLastModifiedBy{get;set;}}

Multi-Tenancy

The ITenantScoped<TId> interface enables multi-tenant data isolation:

publicclassCustomerRecord:Entity<Guid>,ITenantScoped<Guid>{publicstringCustomerName{get;set;}publicstringContactEmail{get;set;}// ITenantScoped implementationpublicGuidTenantId{get;set;}}

This allows for automatic tenant filtering in queries.

Soft Deletion

The ISoftDeletable interface supports logical deletion of entities:

publicclassDocument:Entity<Guid>,ISoftDeletable{publicstringTitle{get;set;}publicstringContent{get;set;}// ISoftDeletable implementationpublicboolIsDeleted{get;set;}publicDateTimeOffset?DeletedOn{get;set;}publicstring?DeletedBy{get;set;}}

Tracing

The ITraceable interface enables distributed tracing:

publicclassPayment:Entity<Guid>,ITraceable{publicdecimalAmount{get;set;}publicstringPaymentMethod{get;set;}// ITraceable implementationpublicGuidCorrelationId{get;set;}publicstring?TraceSource{get;set;}publicstring?OperationName{get;set;}}

Functional Programming

Result Pattern

The Result pattern provides a way to represent operation outcomes that might fail:

// Creating resultsResultsuccessResult=Result.Success();Result<int>successWithValue=Result.Success(42);ResultfailureResult=Result.Failure(Error.ValidationError);Result<string>failureWithCustomError=Result.Failure<string>(Error.Failure("User.NotFound","The specified user was not found."));// Pattern matchingstringmessage=userResult.Match(onSuccess: user =>$"User {user.Name} was found",onFailure: error =>$"Error: {error.Message}");// Transforming resultsResult<UserDto>userDtoResult=userResult.Map(user =>newUserDto(user));// Chaining operationsResult<OrderConfirmation>confirmationResult=ValidateOrder(request).Bind(order =>CalculateTotals(order)).Bind(order =>CheckInventory(order)).Bind(order =>ProcessPayment(order)).Map(order =>GenerateConfirmation(order));

Maybe Pattern

The Maybe pattern provides type-safe handling of optional values:

// Creating Maybe valuesMaybe<string>someName=Maybe<string>.Some("John");Maybe<string>noName=Maybe<string>.None;// Using Match for handling both casesstringgreeting=someName.Match(onSome: name =>$"Hello, {name}!",onNone:()=>"Hello, stranger!");// Transforming Maybe valuesMaybe<int>nameLength=someName.Map(name =>name.Length);// Converting to ResultResult<string>nameResult=someName.ToResult("Name is required");

Validation

Validator Overview

The Validator class provides methods to validate input against single or multiple rules, returning structured results. It integrates seamlessly with the Result and ValidationResult patterns.

Validating Single Rule
usingCoreKernel.Functional.Validation;// Validate input against a single rulevarresult=Validator.Validate(input:"example@example.com",validationRule: value =>value.Contains("@"),errorMessage:"Input must contain '@'.");if(result.IsSuccess){Console.WriteLine("Validation succeeded: "+result.Value);}else{Console.WriteLine("Validation failed: "+result.Error.Message);}
Validating Multiple Rules
usingCoreKernel.Functional.Validation;// Define validation rulesvarvalidationRules=newList<(Func<string,bool>rule,stringerrorMessage)>{(value =>!string.IsNullOrWhiteSpace(value),"Input cannot be empty."),(value =>value.Contains("@"),"Input must contain '@'."),(value =>value.Length<=50,"Input must not exceed 50 characters.")};// Validate input against multiple rulesvarresult=Validator.Validate("example@example.com",validationRules);if(result.IsSuccess){Console.WriteLine("Validation succeeded: "+result.Value);}else{Console.WriteLine("Validation failed with errors:");foreach(varerrorin((IValidationResult)result).Errors){Console.WriteLine($"- {error.Message}");}}

Validation types allow collecting multiple validation errors:

// Creating validation resultsValidationResultvalidationResult=ValidationResult.WithErrors(new[]{Error.Validation("User.Email.Invalid","Email address is not in a valid format"),Error.Validation("User.Password.TooShort","Password must be at least 8 characters")});// Using Match with ValidationResultstringmessage=userValidationResult.Match(onSuccess: user =>$"User created: {user.Email}",onError: errors =>$"Validation failed: {string.Join(", ",errors.Select(e =>e.Message))}");

Error Handling

The Error record type provides structured error representation:

// Predefined errorsErrornullValueError=Error.NullValue;ErrorvalidationError=Error.ValidationError;// Custom errorsErrorcustomError=Error.Failure("Order.Processing.Failed","Failed to process the order due to payment issue");// Adding detailsErrordetailedError=customError.WithDetails("Transaction ID: 1234567");

Messaging

Commands

Commands represent intentions to change system state:

// Command definitionpublicclassCreateProductCommand:ICommand<Guid>{publicstringName{get;init;}publicdecimalPrice{get;init;}}// Command handlerpublicclassCreateProductCommandHandler:ICommandHandler<CreateProductCommand,Guid>{publicasyncTask<Result<Guid>>Handle(CreateProductCommandcommand,CancellationTokencancellationToken){// Implementation to create a productvarproductId=Guid.NewGuid();returnResult.Success(productId);}}

Queries

Queries retrieve data without changing system state:

// Query definitionpublicclassGetProductByIdQuery:IQuery<ProductDto>{publicGuidId{get;init;}}// Query handlerpublicclassGetProductByIdQueryHandler:IQueryHandler<GetProductByIdQuery,ProductDto>{publicasyncTask<Result<ProductDto>>Handle(GetProductByIdQueryquery,CancellationTokencancellationToken){// Implementation to retrieve product datareturnResult.Success(newProductDto{/* ... */});}}

Events

Events represent notifications about something that has happened:

// Event definitionpublicclassUserRegisteredEvent:IEvent{publicGuidId{get;init;}publicDateTimeTimeStamp{get;init;}publicGuidCorrelationId{get;init;}publicstringUsername{get;init;}publicstringEmail{get;init;}}// Event handlerpublicclassSendWelcomeEmailHandler:IEventHandler<UserRegisteredEvent>{publicasyncTaskHandle(UserRegisteredEventnotification,CancellationTokencancellationToken){// Implementation to send welcome email}}

Domain Events

Domain events are specialized events raised within the domain model:

// Domain event definitionpublicclassOrderPlacedEvent:IDomainEvent{publicGuidId{get;init;}publicDateTimeTimeStamp{get;init;}publicGuidCorrelationId{get;init;}publicGuidOrderId{get;init;}}// Domain event handlerpublicclassUpdateInventoryHandler:IDomainEventHandler<OrderPlacedEvent>{publicasyncTaskHandle(OrderPlacedEventnotification,CancellationTokencancellationToken){// Implementation to update inventory}}

Integration Examples

Repository with Domain Markers

publicclassGenericRepository<TEntity,TId>:IRepository<TEntity,TId>whereTEntity:Entity<TId>whereTId:notnull{privatereadonlyDbContext_dbContext;privatereadonlyICurrentUserService_currentUser;privatereadonlyITenantProvider_tenantProvider;publicasyncTask<Result<TEntity>>GetByIdAsync(TIdid){varentity=await_dbContext.Set<TEntity>().FindAsync(id);returnentity!=null?Result.Success(entity):Result.Failure<TEntity>(Error.NotFound($"{typeof(TEntity).Name} with ID {id} was not found."));}publicasyncTask<Result<TId>>SaveAsync(TEntityentity){// Handle audit informationif(entityisIAuditableauditable){varnow=DateTimeOffset.UtcNow;varuserId=_currentUser.UserId??"system";if(IsNew(entity)){auditable.CreatedOn=now;auditable.CreatedBy=userId;}auditable.LastModifiedOn=now;auditable.LastModifiedBy=userId;}// Handle multi-tenancyif(entityisITenantScoped<Guid>tenantScoped&&IsNew(entity)){tenantScoped.TenantId=_tenantProvider.GetCurrentTenantId();}// Handle tracing if applicableif(entityisITraceabletraceable){traceable.CorrelationId=_currentUser.CorrelationId;traceable.TraceSource=GetType().Name;traceable.OperationName=IsNew(entity)?"Create":"Update";}try{if(IsNew(entity))_dbContext.Add(entity);await_dbContext.SaveChangesAsync();returnResult.Success(entity.Id);}catch(Exceptionex){returnResult.Failure<TId>(Error.Failure("Database.SaveFailed",$"Failed to save {typeof(TEntity).Name}: {ex.Message}"));}}publicasyncTask<Result>DeleteAsync(TEntityentity){if(entityisISoftDeletablesoftDeletable){softDeletable.IsDeleted=true;softDeletable.DeletedOn=DateTimeOffset.UtcNow;softDeletable.DeletedBy=_currentUser.UserId??"system";await_dbContext.SaveChangesAsync();returnResult.Success();}_dbContext.Remove(entity);await_dbContext.SaveChangesAsync();returnResult.Success();}privateboolIsNew(TEntityentity){return!_dbContext.Entry(entity).IsKeySet;}}

ASP.NET Core Controller with CQRS

[ApiController][Route("api/[controller]")]publicclassProductsController:ControllerBase{privatereadonlyIMediator_mediator;publicProductsController(IMediatormediator){_mediator=mediator;}[HttpGet("{id}")]publicasyncTask<IActionResult>GetProduct(Guidid){varquery=newGetProductByIdQuery{Id=id};varresult=await_mediator.Send(query);returnresult.Match(onSuccess: product =>Ok(product),onFailure: error =>error.Typeswitch{ErrorType.NotFound=>NotFound(error.Message),ErrorType.Unauthorized=>Unauthorized(error.Message),
_ =>BadRequest(error.Message)});}[HttpPost]publicasyncTask<IActionResult>CreateProduct(CreateProductRequestrequest){varcommand=newCreateProductCommand{Name=request.Name,Price=request.Price};varresult=await_mediator.Send(command);returnresult.Match(onSuccess: id =>CreatedAtAction(nameof(GetProduct),new{id},null),onFailure: error =>BadRequest(error.Message));}}

Domain Event Handling Pipeline

publicclassDomainEventDispatcher:IDomainEventDispatcher{privatereadonlyIMediator_mediator;privatereadonlyILogger<DomainEventDispatcher>_logger;publicDomainEventDispatcher(IMediatormediator,ILogger<DomainEventDispatcher>logger){_mediator=mediator;_logger=logger;}publicasyncTaskDispatchEventsAsync(IEnumerable<IDomainEvent>domainEvents){foreach(vardomainEventindomainEvents){_logger.LogInformation("Dispatching domain event {EventType} with ID {EventId}",domainEvent.GetType().Name,domainEvent.Id);await_mediator.Publish(domainEvent);}}}// Usage in a command handlerpublicclassPlaceOrderCommandHandler:ICommandHandler<PlaceOrderCommand,Guid>{privatereadonlyIRepository<Order,Guid>_orderRepository;privatereadonlyIDomainEventDispatcher_eventDispatcher;publicasyncTask<Result<Guid>>Handle(PlaceOrderCommandcommand,CancellationTokencancellationToken){// Create and place ordervarorder=newOrder(Guid.NewGuid());foreach(varitemincommand.Items){order.AddOrderLine(item.ProductId,item.Quantity);}order.Place();// Save order and dispatch eventsvarsaveResult=await_orderRepository.SaveAsync(order);if(saveResult.IsSuccess){await_eventDispatcher.DispatchEventsAsync(order.GetDomainEvents());order.ClearDomainEvents();}returnsaveResult;}}

Best Practices

DDD Best Practices

  1. Use Value Objects for concepts defined by their attributes (Money, Address, PhoneNumber)
  2. Create meaningful Aggregates with clear boundaries and invariants
  3. Keep Entities focused on domain behavior rather than data persistence
  4. Use strongly-typed IDs to prevent mixing identifier types

Functional Programming Best Practices

  1. Prefer Result<T> over exceptions for expected error cases
  2. Use Maybe<T> instead of null for optional values
  3. Chain operations with Bind and Map instead of using nested conditionals
  4. Use Match for exhaustive handling of all possible states

Domain Markers Best Practices

  1. Combine Domain Markers when an entity needs multiple cross-cutting concerns
  2. Centralize implementation logic in repositories or middleware
  3. Consider performance impacts of global filters for soft deletion and multi-tenancy
  4. Apply markers consistently across related entities

Messaging Best Practices

  1. Commands should represent intent and map to a single use case
  2. Queries should be idempotent and not modify state
  3. Use domain events for cross-aggregate communication
  4. Include correlation IDs for traceability across system boundaries

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

This project is licensed under the Apache License 2.0. See the LICENSE file for details.

About

The CoreKernel library provides a set of foundational abstractions and utilities for building robust, modular, and maintainable applications. It includes support for functional programming constructs, messaging patterns, error handling, and domain-driven design (DDD) principles.

Topics

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

CoreKernel Library

logo


The CoreKernel library provides a comprehensive set of foundational abstractions and utilities for building robust, modular, and maintainable .NET applications. It implements patterns from Domain-Driven Design (DDD), functional programming, and modern architectural approaches like CQRS and Event-Driven Architecture.

Table of Contents


Overview

The CoreKernel library is a comprehensive toolkit designed to simplify application development by providing:

  • DDD building blocks for modeling your business domain with clarity and precision
  • Functional programming constructs for safer, more expressive code
  • Cross-cutting concerns handled through domain markers
  • Messaging infrastructure for implementing CQRS and event-driven architecture
  • Consistent error handling through the Result pattern

By adopting these patterns early in your development process, you can ensure better code maintainability, domain integrity, and system scalability.

Components

The CoreKernel library consists of the following components:

  • CoreKernel.Primitives: Domain-Driven Design building blocks (entities, value objects, aggregates)
  • CoreKernel.DomainMarkers: Interfaces for cross-cutting concerns (auditing, multi-tenancy, etc.)
  • CoreKernel.Functional: Functional programming abstractions (Result, Maybe, Error types)
  • CoreKernel.Messaging: CQRS and event-driven architecture support (commands, queries, events)

Each component can be used independently or together for a complete development experience.

Installation

To use the full CoreKernel library in your project:

dotnet add package CoreKernel

Or, to use individual components:

dotnet add package CoreKernel.Primitives
dotnet add package CoreKernel.DomainMarkers
dotnet add package CoreKernel.Functional
dotnet add package CoreKernel.Messaging

Usage

Domain-Driven Design Primitives

Entities

The Entity<TId> base class provides identity-based equality comparison:

publicclassProduct:Entity<Guid>{publicstringName{get;}publicdecimalPrice{get;privateset;}publicProduct(Guidid,stringname,decimalprice):base(id){Name=name;Price=price;}publicvoidUpdatePrice(decimalnewPrice){Price=newPrice;}}

Entities are compared by ID and type:

varproduct1=newProduct(Guid.NewGuid(),"Widget",10.99m);varproduct2=newProduct(product1.Id,"Widget",10.99m);boolareEqual=product1==product2;// true, same ID

Aggregate Roots

Aggregate roots are the entry points to domain aggregates and can raise domain events:

publicclassOrder:AggregateRoot<Guid>{privatereadonlyList<OrderLine>_orderLines=new();publicIReadOnlyCollection<OrderLine>OrderLines=>_orderLines.AsReadOnly();publicdecimalTotalAmount{get;privateset;}publicOrder(Guidid):base(id){TotalAmount=0;}publicvoidAddOrderLine(Productproduct,intquantity){varorderLine=newOrderLine(Guid.NewGuid(),product.Id,product.Price,quantity);_orderLines.Add(orderLine);TotalAmount+=orderLine.LineTotal;RaiseDomainEvent(newOrderLineAddedEvent(Id,orderLine.Id));}publicvoidPlace(){// Business logic for placing an orderRaiseDomainEvent(newOrderPlacedEvent(Id));}}

Working with domain events:

// Create and manipulate the ordervarorder=newOrder(Guid.NewGuid());order.AddOrderLine(product,2);order.Place();// Retrieve domain events for processingvarevents=order.GetDomainEvents();// Process events...order.ClearDomainEvents();// Clear after processing

Value Objects

Value objects encapsulate concepts that are distinguished by their attributes rather than identity:

publicclassMoney:ValueObject{publicdecimalAmount{get;}publicstringCurrency{get;}publicMoney(decimalamount,stringcurrency){Amount=amount;Currency=currency;}protectedoverrideIEnumerable<object>GetAtomicValues(){yieldreturnAmount;yieldreturnCurrency;}publicMoneyAdd(Moneyother){if(Currency!=other.Currency)thrownewInvalidOperationException("Cannot add different currencies");returnnewMoney(Amount+other.Amount,Currency);}}

Value objects are compared by their structure, not reference:

varprice1=newMoney(100,"USD");varprice2=newMoney(100,"USD");boolareEqual=price1==price2;// true, same values

Strongly-Typed IDs

Strongly-typed IDs prevent accidental ID misuse between different entity types:

publicclassUserId:StronglyTypedId<Guid>{publicUserId(Guidvalue):base(value){}}publicclassOrderId:StronglyTypedId<Guid>{publicOrderId(Guidvalue):base(value){}}publicclassUser:Entity<UserId>{publicstringName{get;}publicUser(UserIdid,stringname):base(id){Name=name;}}

This prevents accidentally passing an OrderId to a method expecting a UserId.

Domain Markers

Auditing

Auditing interfaces track entity creation and modification:

// Basic time trackingpublicclassBlogPost:Entity<Guid>,ITimeStamped{publicstringTitle{get;set;}publicstringContent{get;set;}// ITimeStamped implementationpublicDateTimeOffsetCreatedOn{get;set;}publicDateTimeOffsetLastModifiedOn{get;set;}}// Complete auditing with user trackingpublicclassInvoice:Entity<Guid>,IAuditable{publicdecimalAmount{get;set;}publicstringCustomerName{get;set;}// IAuditable implementationpublicDateTimeOffsetCreatedOn{get;set;}publicstringCreatedBy{get;set;}publicDateTimeOffsetLastModifiedOn{get;set;}publicstringLastModifiedBy{get;set;}}

Multi-Tenancy

The ITenantScoped<TId> interface enables multi-tenant data isolation:

publicclassCustomerRecord:Entity<Guid>,ITenantScoped<Guid>{publicstringCustomerName{get;set;}publicstringContactEmail{get;set;}// ITenantScoped implementationpublicGuidTenantId{get;set;}}

This allows for automatic tenant filtering in queries.

Soft Deletion

The ISoftDeletable interface supports logical deletion of entities:

publicclassDocument:Entity<Guid>,ISoftDeletable{publicstringTitle{get;set;}publicstringContent{get;set;}// ISoftDeletable implementationpublicboolIsDeleted{get;set;}publicDateTimeOffset?DeletedOn{get;set;}publicstring?DeletedBy{get;set;}}

Tracing

The ITraceable interface enables distributed tracing:

publicclassPayment:Entity<Guid>,ITraceable{publicdecimalAmount{get;set;}publicstringPaymentMethod{get;set;}// ITraceable implementationpublicGuidCorrelationId{get;set;}publicstring?TraceSource{get;set;}publicstring?OperationName{get;set;}}

Functional Programming

Result Pattern

The Result pattern provides a way to represent operation outcomes that might fail:

// Creating resultsResultsuccessResult=Result.Success();Result<int>successWithValue=Result.Success(42);ResultfailureResult=Result.Failure(Error.ValidationError);Result<string>failureWithCustomError=Result.Failure<string>(Error.Failure("User.NotFound","The specified user was not found."));// Pattern matchingstringmessage=userResult.Match(onSuccess: user =>$"User {user.Name} was found",onFailure: error =>$"Error: {error.Message}");// Transforming resultsResult<UserDto>userDtoResult=userResult.Map(user =>newUserDto(user));// Chaining operationsResult<OrderConfirmation>confirmationResult=ValidateOrder(request).Bind(order =>CalculateTotals(order)).Bind(order =>CheckInventory(order)).Bind(order =>ProcessPayment(order)).Map(order =>GenerateConfirmation(order));

Maybe Pattern

The Maybe pattern provides type-safe handling of optional values:

// Creating Maybe valuesMaybe<string>someName=Maybe<string>.Some("John");Maybe<string>noName=Maybe<string>.None;// Using Match for handling both casesstringgreeting=someName.Match(onSome: name =>$"Hello, {name}!",onNone:()=>"Hello, stranger!");// Transforming Maybe valuesMaybe<int>nameLength=someName.Map(name =>name.Length);// Converting to ResultResult<string>nameResult=someName.ToResult("Name is required");

Validation

Validator Overview

The Validator class provides methods to validate input against single or multiple rules, returning structured results. It integrates seamlessly with the Result and ValidationResult patterns.

Validating Single Rule
usingCoreKernel.Functional.Validation;// Validate input against a single rulevarresult=Validator.Validate(input:"example@example.com",validationRule: value =>value.Contains("@"),errorMessage:"Input must contain '@'.");if(result.IsSuccess){Console.WriteLine("Validation succeeded: "+result.Value);}else{Console.WriteLine("Validation failed: "+result.Error.Message);}
Validating Multiple Rules
usingCoreKernel.Functional.Validation;// Define validation rulesvarvalidationRules=newList<(Func<string,bool>rule,stringerrorMessage)>{(value =>!string.IsNullOrWhiteSpace(value),"Input cannot be empty."),(value =>value.Contains("@"),"Input must contain '@'."),(value =>value.Length<=50,"Input must not exceed 50 characters.")};// Validate input against multiple rulesvarresult=Validator.Validate("example@example.com",validationRules);if(result.IsSuccess){Console.WriteLine("Validation succeeded: "+result.Value);}else{Console.WriteLine("Validation failed with errors:");foreach(varerrorin((IValidationResult)result).Errors){Console.WriteLine($"- {error.Message}");}}

Validation types allow collecting multiple validation errors:

// Creating validation resultsValidationResultvalidationResult=ValidationResult.WithErrors(new[]{Error.Validation("User.Email.Invalid","Email address is not in a valid format"),Error.Validation("User.Password.TooShort","Password must be at least 8 characters")});// Using Match with ValidationResultstringmessage=userValidationResult.Match(onSuccess: user =>$"User created: {user.Email}",onError: errors =>$"Validation failed: {string.Join(", ",errors.Select(e =>e.Message))}");

Error Handling

The Error record type provides structured error representation:

// Predefined errorsErrornullValueError=Error.NullValue;ErrorvalidationError=Error.ValidationError;// Custom errorsErrorcustomError=Error.Failure("Order.Processing.Failed","Failed to process the order due to payment issue");// Adding detailsErrordetailedError=customError.WithDetails("Transaction ID: 1234567");

Messaging

Commands

Commands represent intentions to change system state:

// Command definitionpublicclassCreateProductCommand:ICommand<Guid>{publicstringName{get;init;}publicdecimalPrice{get;init;}}// Command handlerpublicclassCreateProductCommandHandler:ICommandHandler<CreateProductCommand,Guid>{publicasyncTask<Result<Guid>>Handle(CreateProductCommandcommand,CancellationTokencancellationToken){// Implementation to create a productvarproductId=Guid.NewGuid();returnResult.Success(productId);}}

Queries

Queries retrieve data without changing system state:

// Query definitionpublicclassGetProductByIdQuery:IQuery<ProductDto>{publicGuidId{get;init;}}// Query handlerpublicclassGetProductByIdQueryHandler:IQueryHandler<GetProductByIdQuery,ProductDto>{publicasyncTask<Result<ProductDto>>Handle(GetProductByIdQueryquery,CancellationTokencancellationToken){// Implementation to retrieve product datareturnResult.Success(newProductDto{/* ... */});}}

Events

Events represent notifications about something that has happened:

// Event definitionpublicclassUserRegisteredEvent:IEvent{publicGuidId{get;init;}publicDateTimeTimeStamp{get;init;}publicGuidCorrelationId{get;init;}publicstringUsername{get;init;}publicstringEmail{get;init;}}// Event handlerpublicclassSendWelcomeEmailHandler:IEventHandler<UserRegisteredEvent>{publicasyncTaskHandle(UserRegisteredEventnotification,CancellationTokencancellationToken){// Implementation to send welcome email}}

Domain Events

Domain events are specialized events raised within the domain model:

// Domain event definitionpublicclassOrderPlacedEvent:IDomainEvent{publicGuidId{get;init;}publicDateTimeTimeStamp{get;init;}publicGuidCorrelationId{get;init;}publicGuidOrderId{get;init;}}// Domain event handlerpublicclassUpdateInventoryHandler:IDomainEventHandler<OrderPlacedEvent>{publicasyncTaskHandle(OrderPlacedEventnotification,CancellationTokencancellationToken){// Implementation to update inventory}}

Integration Examples

Repository with Domain Markers

publicclassGenericRepository<TEntity,TId>:IRepository<TEntity,TId>whereTEntity:Entity<TId>whereTId:notnull{privatereadonlyDbContext_dbContext;privatereadonlyICurrentUserService_currentUser;privatereadonlyITenantProvider_tenantProvider;publicasyncTask<Result<TEntity>>GetByIdAsync(TIdid){varentity=await_dbContext.Set<TEntity>().FindAsync(id);returnentity!=null?Result.Success(entity):Result.Failure<TEntity>(Error.NotFound($"{typeof(TEntity).Name} with ID {id} was not found."));}publicasyncTask<Result<TId>>SaveAsync(TEntityentity){// Handle audit informationif(entityisIAuditableauditable){varnow=DateTimeOffset.UtcNow;varuserId=_currentUser.UserId??"system";if(IsNew(entity)){auditable.CreatedOn=now;auditable.CreatedBy=userId;}auditable.LastModifiedOn=now;auditable.LastModifiedBy=userId;}// Handle multi-tenancyif(entityisITenantScoped<Guid>tenantScoped&&IsNew(entity)){tenantScoped.TenantId=_tenantProvider.GetCurrentTenantId();}// Handle tracing if applicableif(entityisITraceabletraceable){traceable.CorrelationId=_currentUser.CorrelationId;traceable.TraceSource=GetType().Name;traceable.OperationName=IsNew(entity)?"Create":"Update";}try{if(IsNew(entity))_dbContext.Add(entity);await_dbContext.SaveChangesAsync();returnResult.Success(entity.Id);}catch(Exceptionex){returnResult.Failure<TId>(Error.Failure("Database.SaveFailed",$"Failed to save {typeof(TEntity).Name}: {ex.Message}"));}}publicasyncTask<Result>DeleteAsync(TEntityentity){if(entityisISoftDeletablesoftDeletable){softDeletable.IsDeleted=true;softDeletable.DeletedOn=DateTimeOffset.UtcNow;softDeletable.DeletedBy=_currentUser.UserId??"system";await_dbContext.SaveChangesAsync();returnResult.Success();}_dbContext.Remove(entity);await_dbContext.SaveChangesAsync();returnResult.Success();}privateboolIsNew(TEntityentity){return!_dbContext.Entry(entity).IsKeySet;}}

ASP.NET Core Controller with CQRS

[ApiController][Route("api/[controller]")]publicclassProductsController:ControllerBase{privatereadonlyIMediator_mediator;publicProductsController(IMediatormediator){_mediator=mediator;}[HttpGet("{id}")]publicasyncTask<IActionResult>GetProduct(Guidid){varquery=newGetProductByIdQuery{Id=id};varresult=await_mediator.Send(query);returnresult.Match(onSuccess: product =>Ok(product),onFailure: error =>error.Typeswitch{ErrorType.NotFound=>NotFound(error.Message),ErrorType.Unauthorized=>Unauthorized(error.Message),
_ =>BadRequest(error.Message)});}[HttpPost]publicasyncTask<IActionResult>CreateProduct(CreateProductRequestrequest){varcommand=newCreateProductCommand{Name=request.Name,Price=request.Price};varresult=await_mediator.Send(command);returnresult.Match(onSuccess: id =>CreatedAtAction(nameof(GetProduct),new{id},null),onFailure: error =>BadRequest(error.Message));}}

Domain Event Handling Pipeline

publicclassDomainEventDispatcher:IDomainEventDispatcher{privatereadonlyIMediator_mediator;privatereadonlyILogger<DomainEventDispatcher>_logger;publicDomainEventDispatcher(IMediatormediator,ILogger<DomainEventDispatcher>logger){_mediator=mediator;_logger=logger;}publicasyncTaskDispatchEventsAsync(IEnumerable<IDomainEvent>domainEvents){foreach(vardomainEventindomainEvents){_logger.LogInformation("Dispatching domain event {EventType} with ID {EventId}",domainEvent.GetType().Name,domainEvent.Id);await_mediator.Publish(domainEvent);}}}// Usage in a command handlerpublicclassPlaceOrderCommandHandler:ICommandHandler<PlaceOrderCommand,Guid>{privatereadonlyIRepository<Order,Guid>_orderRepository;privatereadonlyIDomainEventDispatcher_eventDispatcher;publicasyncTask<Result<Guid>>Handle(PlaceOrderCommandcommand,CancellationTokencancellationToken){// Create and place ordervarorder=newOrder(Guid.NewGuid());foreach(varitemincommand.Items){order.AddOrderLine(item.ProductId,item.Quantity);}order.Place();// Save order and dispatch eventsvarsaveResult=await_orderRepository.SaveAsync(order);if(saveResult.IsSuccess){await_eventDispatcher.DispatchEventsAsync(order.GetDomainEvents());order.ClearDomainEvents();}returnsaveResult;}}

Best Practices

DDD Best Practices

  1. Use Value Objects for concepts defined by their attributes (Money, Address, PhoneNumber)
  2. Create meaningful Aggregates with clear boundaries and invariants
  3. Keep Entities focused on domain behavior rather than data persistence
  4. Use strongly-typed IDs to prevent mixing identifier types

Functional Programming Best Practices

  1. Prefer Result<T> over exceptions for expected error cases
  2. Use Maybe<T> instead of null for optional values
  3. Chain operations with Bind and Map instead of using nested conditionals
  4. Use Match for exhaustive handling of all possible states

Domain Markers Best Practices

  1. Combine Domain Markers when an entity needs multiple cross-cutting concerns
  2. Centralize implementation logic in repositories or middleware
  3. Consider performance impacts of global filters for soft deletion and multi-tenancy
  4. Apply markers consistently across related entities

Messaging Best Practices

  1. Commands should represent intent and map to a single use case
  2. Queries should be idempotent and not modify state
  3. Use domain events for cross-aggregate communication
  4. Include correlation IDs for traceability across system boundaries

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

This project is licensed under the Apache License 2.0. See the LICENSE file for details.

About

The CoreKernel library provides a set of foundational abstractions and utilities for building robust, modular, and maintainable applications. It includes support for functional programming constructs, messaging patterns, error handling, and domain-driven design (DDD) principles.

Topics

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

CoreKernel Library

logo


The CoreKernel library provides a comprehensive set of foundational abstractions and utilities for building robust, modular, and maintainable .NET applications. It implements patterns from Domain-Driven Design (DDD), functional programming, and modern architectural approaches like CQRS and Event-Driven Architecture.

Table of Contents


Overview

The CoreKernel library is a comprehensive toolkit designed to simplify application development by providing:

  • DDD building blocks for modeling your business domain with clarity and precision
  • Functional programming constructs for safer, more expressive code
  • Cross-cutting concerns handled through domain markers
  • Messaging infrastructure for implementing CQRS and event-driven architecture
  • Consistent error handling through the Result pattern

By adopting these patterns early in your development process, you can ensure better code maintainability, domain integrity, and system scalability.

Components

The CoreKernel library consists of the following components:

  • CoreKernel.Primitives: Domain-Driven Design building blocks (entities, value objects, aggregates)
  • CoreKernel.DomainMarkers: Interfaces for cross-cutting concerns (auditing, multi-tenancy, etc.)
  • CoreKernel.Functional: Functional programming abstractions (Result, Maybe, Error types)
  • CoreKernel.Messaging: CQRS and event-driven architecture support (commands, queries, events)

Each component can be used independently or together for a complete development experience.

Installation

To use the full CoreKernel library in your project:

dotnet add package CoreKernel

Or, to use individual components:

dotnet add package CoreKernel.Primitives
dotnet add package CoreKernel.DomainMarkers
dotnet add package CoreKernel.Functional
dotnet add package CoreKernel.Messaging

Usage

Domain-Driven Design Primitives

Entities

The Entity<TId> base class provides identity-based equality comparison:

publicclassProduct:Entity<Guid>{publicstringName{get;}publicdecimalPrice{get;privateset;}publicProduct(Guidid,stringname,decimalprice):base(id){Name=name;Price=price;}publicvoidUpdatePrice(decimalnewPrice){Price=newPrice;}}

Entities are compared by ID and type:

varproduct1=newProduct(Guid.NewGuid(),"Widget",10.99m);varproduct2=newProduct(product1.Id,"Widget",10.99m);boolareEqual=product1==product2;// true, same ID

Aggregate Roots

Aggregate roots are the entry points to domain aggregates and can raise domain events:

publicclassOrder:AggregateRoot<Guid>{privatereadonlyList<OrderLine>_orderLines=new();publicIReadOnlyCollection<OrderLine>OrderLines=>_orderLines.AsReadOnly();publicdecimalTotalAmount{get;privateset;}publicOrder(Guidid):base(id){TotalAmount=0;}publicvoidAddOrderLine(Productproduct,intquantity){varorderLine=newOrderLine(Guid.NewGuid(),product.Id,product.Price,quantity);_orderLines.Add(orderLine);TotalAmount+=orderLine.LineTotal;RaiseDomainEvent(newOrderLineAddedEvent(Id,orderLine.Id));}publicvoidPlace(){// Business logic for placing an orderRaiseDomainEvent(newOrderPlacedEvent(Id));}}

Working with domain events:

// Create and manipulate the ordervarorder=newOrder(Guid.NewGuid());order.AddOrderLine(product,2);order.Place();// Retrieve domain events for processingvarevents=order.GetDomainEvents();// Process events...order.ClearDomainEvents();// Clear after processing

Value Objects

Value objects encapsulate concepts that are distinguished by their attributes rather than identity:

publicclassMoney:ValueObject{publicdecimalAmount{get;}publicstringCurrency{get;}publicMoney(decimalamount,stringcurrency){Amount=amount;Currency=currency;}protectedoverrideIEnumerable<object>GetAtomicValues(){yieldreturnAmount;yieldreturnCurrency;}publicMoneyAdd(Moneyother){if(Currency!=other.Currency)thrownewInvalidOperationException("Cannot add different currencies");returnnewMoney(Amount+other.Amount,Currency);}}

Value objects are compared by their structure, not reference:

varprice1=newMoney(100,"USD");varprice2=newMoney(100,"USD");boolareEqual=price1==price2;// true, same values

Strongly-Typed IDs

Strongly-typed IDs prevent accidental ID misuse between different entity types:

publicclassUserId:StronglyTypedId<Guid>{publicUserId(Guidvalue):base(value){}}publicclassOrderId:StronglyTypedId<Guid>{publicOrderId(Guidvalue):base(value){}}publicclassUser:Entity<UserId>{publicstringName{get;}publicUser(UserIdid,stringname):base(id){Name=name;}}

This prevents accidentally passing an OrderId to a method expecting a UserId.

Domain Markers

Auditing

Auditing interfaces track entity creation and modification:

// Basic time trackingpublicclassBlogPost:Entity<Guid>,ITimeStamped{publicstringTitle{get;set;}publicstringContent{get;set;}// ITimeStamped implementationpublicDateTimeOffsetCreatedOn{get;set;}publicDateTimeOffsetLastModifiedOn{get;set;}}// Complete auditing with user trackingpublicclassInvoice:Entity<Guid>,IAuditable{publicdecimalAmount{get;set;}publicstringCustomerName{get;set;}// IAuditable implementationpublicDateTimeOffsetCreatedOn{get;set;}publicstringCreatedBy{get;set;}publicDateTimeOffsetLastModifiedOn{get;set;}publicstringLastModifiedBy{get;set;}}

Multi-Tenancy

The ITenantScoped<TId> interface enables multi-tenant data isolation:

publicclassCustomerRecord:Entity<Guid>,ITenantScoped<Guid>{publicstringCustomerName{get;set;}publicstringContactEmail{get;set;}// ITenantScoped implementationpublicGuidTenantId{get;set;}}

This allows for automatic tenant filtering in queries.

Soft Deletion

The ISoftDeletable interface supports logical deletion of entities:

publicclassDocument:Entity<Guid>,ISoftDeletable{publicstringTitle{get;set;}publicstringContent{get;set;}// ISoftDeletable implementationpublicboolIsDeleted{get;set;}publicDateTimeOffset?DeletedOn{get;set;}publicstring?DeletedBy{get;set;}}

Tracing

The ITraceable interface enables distributed tracing:

publicclassPayment:Entity<Guid>,ITraceable{publicdecimalAmount{get;set;}publicstringPaymentMethod{get;set;}// ITraceable implementationpublicGuidCorrelationId{get;set;}publicstring?TraceSource{get;set;}publicstring?OperationName{get;set;}}

Functional Programming

Result Pattern

The Result pattern provides a way to represent operation outcomes that might fail:

// Creating resultsResultsuccessResult=Result.Success();Result<int>successWithValue=Result.Success(42);ResultfailureResult=Result.Failure(Error.ValidationError);Result<string>failureWithCustomError=Result.Failure<string>(Error.Failure("User.NotFound","The specified user was not found."));// Pattern matchingstringmessage=userResult.Match(onSuccess: user =>$"User {user.Name} was found",onFailure: error =>$"Error: {error.Message}");// Transforming resultsResult<UserDto>userDtoResult=userResult.Map(user =>newUserDto(user));// Chaining operationsResult<OrderConfirmation>confirmationResult=ValidateOrder(request).Bind(order =>CalculateTotals(order)).Bind(order =>CheckInventory(order)).Bind(order =>ProcessPayment(order)).Map(order =>GenerateConfirmation(order));

Maybe Pattern

The Maybe pattern provides type-safe handling of optional values:

// Creating Maybe valuesMaybe<string>someName=Maybe<string>.Some("John");Maybe<string>noName=Maybe<string>.None;// Using Match for handling both casesstringgreeting=someName.Match(onSome: name =>$"Hello, {name}!",onNone:()=>"Hello, stranger!");// Transforming Maybe valuesMaybe<int>nameLength=someName.Map(name =>name.Length);// Converting to ResultResult<string>nameResult=someName.ToResult("Name is required");

Validation

Validator Overview

The Validator class provides methods to validate input against single or multiple rules, returning structured results. It integrates seamlessly with the Result and ValidationResult patterns.

Validating Single Rule
usingCoreKernel.Functional.Validation;// Validate input against a single rulevarresult=Validator.Validate(input:"example@example.com",validationRule: value =>value.Contains("@"),errorMessage:"Input must contain '@'.");if(result.IsSuccess){Console.WriteLine("Validation succeeded: "+result.Value);}else{Console.WriteLine("Validation failed: "+result.Error.Message);}
Validating Multiple Rules
usingCoreKernel.Functional.Validation;// Define validation rulesvarvalidationRules=newList<(Func<string,bool>rule,stringerrorMessage)>{(value =>!string.IsNullOrWhiteSpace(value),"Input cannot be empty."),(value =>value.Contains("@"),"Input must contain '@'."),(value =>value.Length<=50,"Input must not exceed 50 characters.")};// Validate input against multiple rulesvarresult=Validator.Validate("example@example.com",validationRules);if(result.IsSuccess){Console.WriteLine("Validation succeeded: "+result.Value);}else{Console.WriteLine("Validation failed with errors:");foreach(varerrorin((IValidationResult)result).Errors){Console.WriteLine($"- {error.Message}");}}

Validation types allow collecting multiple validation errors:

// Creating validation resultsValidationResultvalidationResult=ValidationResult.WithErrors(new[]{Error.Validation("User.Email.Invalid","Email address is not in a valid format"),Error.Validation("User.Password.TooShort","Password must be at least 8 characters")});// Using Match with ValidationResultstringmessage=userValidationResult.Match(onSuccess: user =>$"User created: {user.Email}",onError: errors =>$"Validation failed: {string.Join(", ",errors.Select(e =>e.Message))}");

Error Handling

The Error record type provides structured error representation:

// Predefined errorsErrornullValueError=Error.NullValue;ErrorvalidationError=Error.ValidationError;// Custom errorsErrorcustomError=Error.Failure("Order.Processing.Failed","Failed to process the order due to payment issue");// Adding detailsErrordetailedError=customError.WithDetails("Transaction ID: 1234567");

Messaging

Commands

Commands represent intentions to change system state:

// Command definitionpublicclassCreateProductCommand:ICommand<Guid>{publicstringName{get;init;}publicdecimalPrice{get;init;}}// Command handlerpublicclassCreateProductCommandHandler:ICommandHandler<CreateProductCommand,Guid>{publicasyncTask<Result<Guid>>Handle(CreateProductCommandcommand,CancellationTokencancellationToken){// Implementation to create a productvarproductId=Guid.NewGuid();returnResult.Success(productId);}}

Queries

Queries retrieve data without changing system state:

// Query definitionpublicclassGetProductByIdQuery:IQuery<ProductDto>{publicGuidId{get;init;}}// Query handlerpublicclassGetProductByIdQueryHandler:IQueryHandler<GetProductByIdQuery,ProductDto>{publicasyncTask<Result<ProductDto>>Handle(GetProductByIdQueryquery,CancellationTokencancellationToken){// Implementation to retrieve product datareturnResult.Success(newProductDto{/* ... */});}}

Events

Events represent notifications about something that has happened:

// Event definitionpublicclassUserRegisteredEvent:IEvent{publicGuidId{get;init;}publicDateTimeTimeStamp{get;init;}publicGuidCorrelationId{get;init;}publicstringUsername{get;init;}publicstringEmail{get;init;}}// Event handlerpublicclassSendWelcomeEmailHandler:IEventHandler<UserRegisteredEvent>{publicasyncTaskHandle(UserRegisteredEventnotification,CancellationTokencancellationToken){// Implementation to send welcome email}}

Domain Events

Domain events are specialized events raised within the domain model:

// Domain event definitionpublicclassOrderPlacedEvent:IDomainEvent{publicGuidId{get;init;}publicDateTimeTimeStamp{get;init;}publicGuidCorrelationId{get;init;}publicGuidOrderId{get;init;}}// Domain event handlerpublicclassUpdateInventoryHandler:IDomainEventHandler<OrderPlacedEvent>{publicasyncTaskHandle(OrderPlacedEventnotification,CancellationTokencancellationToken){// Implementation to update inventory}}

Integration Examples

Repository with Domain Markers

publicclassGenericRepository<TEntity,TId>:IRepository<TEntity,TId>whereTEntity:Entity<TId>whereTId:notnull{privatereadonlyDbContext_dbContext;privatereadonlyICurrentUserService_currentUser;privatereadonlyITenantProvider_tenantProvider;publicasyncTask<Result<TEntity>>GetByIdAsync(TIdid){varentity=await_dbContext.Set<TEntity>().FindAsync(id);returnentity!=null?Result.Success(entity):Result.Failure<TEntity>(Error.NotFound($"{typeof(TEntity).Name} with ID {id} was not found."));}publicasyncTask<Result<TId>>SaveAsync(TEntityentity){// Handle audit informationif(entityisIAuditableauditable){varnow=DateTimeOffset.UtcNow;varuserId=_currentUser.UserId??"system";if(IsNew(entity)){auditable.CreatedOn=now;auditable.CreatedBy=userId;}auditable.LastModifiedOn=now;auditable.LastModifiedBy=userId;}// Handle multi-tenancyif(entityisITenantScoped<Guid>tenantScoped&&IsNew(entity)){tenantScoped.TenantId=_tenantProvider.GetCurrentTenantId();}// Handle tracing if applicableif(entityisITraceabletraceable){traceable.CorrelationId=_currentUser.CorrelationId;traceable.TraceSource=GetType().Name;traceable.OperationName=IsNew(entity)?"Create":"Update";}try{if(IsNew(entity))_dbContext.Add(entity);await_dbContext.SaveChangesAsync();returnResult.Success(entity.Id);}catch(Exceptionex){returnResult.Failure<TId>(Error.Failure("Database.SaveFailed",$"Failed to save {typeof(TEntity).Name}: {ex.Message}"));}}publicasyncTask<Result>DeleteAsync(TEntityentity){if(entityisISoftDeletablesoftDeletable){softDeletable.IsDeleted=true;softDeletable.DeletedOn=DateTimeOffset.UtcNow;softDeletable.DeletedBy=_currentUser.UserId??"system";await_dbContext.SaveChangesAsync();returnResult.Success();}_dbContext.Remove(entity);await_dbContext.SaveChangesAsync();returnResult.Success();}privateboolIsNew(TEntityentity){return!_dbContext.Entry(entity).IsKeySet;}}

ASP.NET Core Controller with CQRS

[ApiController][Route("api/[controller]")]publicclassProductsController:ControllerBase{privatereadonlyIMediator_mediator;publicProductsController(IMediatormediator){_mediator=mediator;}[HttpGet("{id}")]publicasyncTask<IActionResult>GetProduct(Guidid){varquery=newGetProductByIdQuery{Id=id};varresult=await_mediator.Send(query);returnresult.Match(onSuccess: product =>Ok(product),onFailure: error =>error.Typeswitch{ErrorType.NotFound=>NotFound(error.Message),ErrorType.Unauthorized=>Unauthorized(error.Message),
_ =>BadRequest(error.Message)});}[HttpPost]publicasyncTask<IActionResult>CreateProduct(CreateProductRequestrequest){varcommand=newCreateProductCommand{Name=request.Name,Price=request.Price};varresult=await_mediator.Send(command);returnresult.Match(onSuccess: id =>CreatedAtAction(nameof(GetProduct),new{id},null),onFailure: error =>BadRequest(error.Message));}}

Domain Event Handling Pipeline

publicclassDomainEventDispatcher:IDomainEventDispatcher{privatereadonlyIMediator_mediator;privatereadonlyILogger<DomainEventDispatcher>_logger;publicDomainEventDispatcher(IMediatormediator,ILogger<DomainEventDispatcher>logger){_mediator=mediator;_logger=logger;}publicasyncTaskDispatchEventsAsync(IEnumerable<IDomainEvent>domainEvents){foreach(vardomainEventindomainEvents){_logger.LogInformation("Dispatching domain event {EventType} with ID {EventId}",domainEvent.GetType().Name,domainEvent.Id);await_mediator.Publish(domainEvent);}}}// Usage in a command handlerpublicclassPlaceOrderCommandHandler:ICommandHandler<PlaceOrderCommand,Guid>{privatereadonlyIRepository<Order,Guid>_orderRepository;privatereadonlyIDomainEventDispatcher_eventDispatcher;publicasyncTask<Result<Guid>>Handle(PlaceOrderCommandcommand,CancellationTokencancellationToken){// Create and place ordervarorder=newOrder(Guid.NewGuid());foreach(varitemincommand.Items){order.AddOrderLine(item.ProductId,item.Quantity);}order.Place();// Save order and dispatch eventsvarsaveResult=await_orderRepository.SaveAsync(order);if(saveResult.IsSuccess){await_eventDispatcher.DispatchEventsAsync(order.GetDomainEvents());order.ClearDomainEvents();}returnsaveResult;}}

Best Practices

DDD Best Practices

  1. Use Value Objects for concepts defined by their attributes (Money, Address, PhoneNumber)
  2. Create meaningful Aggregates with clear boundaries and invariants
  3. Keep Entities focused on domain behavior rather than data persistence
  4. Use strongly-typed IDs to prevent mixing identifier types

Functional Programming Best Practices

  1. Prefer Result<T> over exceptions for expected error cases
  2. Use Maybe<T> instead of null for optional values
  3. Chain operations with Bind and Map instead of using nested conditionals
  4. Use Match for exhaustive handling of all possible states

Domain Markers Best Practices

  1. Combine Domain Markers when an entity needs multiple cross-cutting concerns
  2. Centralize implementation logic in repositories or middleware
  3. Consider performance impacts of global filters for soft deletion and multi-tenancy
  4. Apply markers consistently across related entities

Messaging Best Practices

  1. Commands should represent intent and map to a single use case
  2. Queries should be idempotent and not modify state
  3. Use domain events for cross-aggregate communication
  4. Include correlation IDs for traceability across system boundaries

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

This project is licensed under the Apache License 2.0. See the LICENSE file for details.

About

The CoreKernel library provides a set of foundational abstractions and utilities for building robust, modular, and maintainable applications. It includes support for functional programming constructs, messaging patterns, error handling, and domain-driven design (DDD) principles.

Topics

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

CoreKernel Library

logo


The CoreKernel library provides a comprehensive set of foundational abstractions and utilities for building robust, modular, and maintainable .NET applications. It implements patterns from Domain-Driven Design (DDD), functional programming, and modern architectural approaches like CQRS and Event-Driven Architecture.

Table of Contents


Overview

The CoreKernel library is a comprehensive toolkit designed to simplify application development by providing:

  • DDD building blocks for modeling your business domain with clarity and precision
  • Functional programming constructs for safer, more expressive code
  • Cross-cutting concerns handled through domain markers
  • Messaging infrastructure for implementing CQRS and event-driven architecture
  • Consistent error handling through the Result pattern

By adopting these patterns early in your development process, you can ensure better code maintainability, domain integrity, and system scalability.

Components

The CoreKernel library consists of the following components:

  • CoreKernel.Primitives: Domain-Driven Design building blocks (entities, value objects, aggregates)
  • CoreKernel.DomainMarkers: Interfaces for cross-cutting concerns (auditing, multi-tenancy, etc.)
  • CoreKernel.Functional: Functional programming abstractions (Result, Maybe, Error types)
  • CoreKernel.Messaging: CQRS and event-driven architecture support (commands, queries, events)

Each component can be used independently or together for a complete development experience.

Installation

To use the full CoreKernel library in your project:

dotnet add package CoreKernel

Or, to use individual components:

dotnet add package CoreKernel.Primitives
dotnet add package CoreKernel.DomainMarkers
dotnet add package CoreKernel.Functional
dotnet add package CoreKernel.Messaging

Usage

Domain-Driven Design Primitives

Entities

The Entity<TId> base class provides identity-based equality comparison:

publicclassProduct:Entity<Guid>{publicstringName{get;}publicdecimalPrice{get;privateset;}publicProduct(Guidid,stringname,decimalprice):base(id){Name=name;Price=price;}publicvoidUpdatePrice(decimalnewPrice){Price=newPrice;}}

Entities are compared by ID and type:

varproduct1=newProduct(Guid.NewGuid(),"Widget",10.99m);varproduct2=newProduct(product1.Id,"Widget",10.99m);boolareEqual=product1==product2;// true, same ID

Aggregate Roots

Aggregate roots are the entry points to domain aggregates and can raise domain events:

publicclassOrder:AggregateRoot<Guid>{privatereadonlyList<OrderLine>_orderLines=new();publicIReadOnlyCollection<OrderLine>OrderLines=>_orderLines.AsReadOnly();publicdecimalTotalAmount{get;privateset;}publicOrder(Guidid):base(id){TotalAmount=0;}publicvoidAddOrderLine(Productproduct,intquantity){varorderLine=newOrderLine(Guid.NewGuid(),product.Id,product.Price,quantity);_orderLines.Add(orderLine);TotalAmount+=orderLine.LineTotal;RaiseDomainEvent(newOrderLineAddedEvent(Id,orderLine.Id));}publicvoidPlace(){// Business logic for placing an orderRaiseDomainEvent(newOrderPlacedEvent(Id));}}

Working with domain events:

// Create and manipulate the ordervarorder=newOrder(Guid.NewGuid());order.AddOrderLine(product,2);order.Place();// Retrieve domain events for processingvarevents=order.GetDomainEvents();// Process events...order.ClearDomainEvents();// Clear after processing

Value Objects

Value objects encapsulate concepts that are distinguished by their attributes rather than identity:

publicclassMoney:ValueObject{publicdecimalAmount{get;}publicstringCurrency{get;}publicMoney(decimalamount,stringcurrency){Amount=amount;Currency=currency;}protectedoverrideIEnumerable<object>GetAtomicValues(){yieldreturnAmount;yieldreturnCurrency;}publicMoneyAdd(Moneyother){if(Currency!=other.Currency)thrownewInvalidOperationException("Cannot add different currencies");returnnewMoney(Amount+other.Amount,Currency);}}

Value objects are compared by their structure, not reference:

varprice1=newMoney(100,"USD");varprice2=newMoney(100,"USD");boolareEqual=price1==price2;// true, same values

Strongly-Typed IDs

Strongly-typed IDs prevent accidental ID misuse between different entity types:

publicclassUserId:StronglyTypedId<Guid>{publicUserId(Guidvalue):base(value){}}publicclassOrderId:StronglyTypedId<Guid>{publicOrderId(Guidvalue):base(value){}}publicclassUser:Entity<UserId>{publicstringName{get;}publicUser(UserIdid,stringname):base(id){Name=name;}}

This prevents accidentally passing an OrderId to a method expecting a UserId.

Domain Markers

Auditing

Auditing interfaces track entity creation and modification:

// Basic time trackingpublicclassBlogPost:Entity<Guid>,ITimeStamped{publicstringTitle{get;set;}publicstringContent{get;set;}// ITimeStamped implementationpublicDateTimeOffsetCreatedOn{get;set;}publicDateTimeOffsetLastModifiedOn{get;set;}}// Complete auditing with user trackingpublicclassInvoice:Entity<Guid>,IAuditable{publicdecimalAmount{get;set;}publicstringCustomerName{get;set;}// IAuditable implementationpublicDateTimeOffsetCreatedOn{get;set;}publicstringCreatedBy{get;set;}publicDateTimeOffsetLastModifiedOn{get;set;}publicstringLastModifiedBy{get;set;}}

Multi-Tenancy

The ITenantScoped<TId> interface enables multi-tenant data isolation:

publicclassCustomerRecord:Entity<Guid>,ITenantScoped<Guid>{publicstringCustomerName{get;set;}publicstringContactEmail{get;set;}// ITenantScoped implementationpublicGuidTenantId{get;set;}}

This allows for automatic tenant filtering in queries.

Soft Deletion

The ISoftDeletable interface supports logical deletion of entities:

publicclassDocument:Entity<Guid>,ISoftDeletable{publicstringTitle{get;set;}publicstringContent{get;set;}// ISoftDeletable implementationpublicboolIsDeleted{get;set;}publicDateTimeOffset?DeletedOn{get;set;}publicstring?DeletedBy{get;set;}}

Tracing

The ITraceable interface enables distributed tracing:

publicclassPayment:Entity<Guid>,ITraceable{publicdecimalAmount{get;set;}publicstringPaymentMethod{get;set;}// ITraceable implementationpublicGuidCorrelationId{get;set;}publicstring?TraceSource{get;set;}publicstring?OperationName{get;set;}}

Functional Programming

Result Pattern

The Result pattern provides a way to represent operation outcomes that might fail:

// Creating resultsResultsuccessResult=Result.Success();Result<int>successWithValue=Result.Success(42);ResultfailureResult=Result.Failure(Error.ValidationError);Result<string>failureWithCustomError=Result.Failure<string>(Error.Failure("User.NotFound","The specified user was not found."));// Pattern matchingstringmessage=userResult.Match(onSuccess: user =>$"User {user.Name} was found",onFailure: error =>$"Error: {error.Message}");// Transforming resultsResult<UserDto>userDtoResult=userResult.Map(user =>newUserDto(user));// Chaining operationsResult<OrderConfirmation>confirmationResult=ValidateOrder(request).Bind(order =>CalculateTotals(order)).Bind(order =>CheckInventory(order)).Bind(order =>ProcessPayment(order)).Map(order =>GenerateConfirmation(order));

Maybe Pattern

The Maybe pattern provides type-safe handling of optional values:

// Creating Maybe valuesMaybe<string>someName=Maybe<string>.Some("John");Maybe<string>noName=Maybe<string>.None;// Using Match for handling both casesstringgreeting=someName.Match(onSome: name =>$"Hello, {name}!",onNone:()=>"Hello, stranger!");// Transforming Maybe valuesMaybe<int>nameLength=someName.Map(name =>name.Length);// Converting to ResultResult<string>nameResult=someName.ToResult("Name is required");

Validation

Validator Overview

The Validator class provides methods to validate input against single or multiple rules, returning structured results. It integrates seamlessly with the Result and ValidationResult patterns.

Validating Single Rule
usingCoreKernel.Functional.Validation;// Validate input against a single rulevarresult=Validator.Validate(input:"example@example.com",validationRule: value =>value.Contains("@"),errorMessage:"Input must contain '@'.");if(result.IsSuccess){Console.WriteLine("Validation succeeded: "+result.Value);}else{Console.WriteLine("Validation failed: "+result.Error.Message);}
Validating Multiple Rules
usingCoreKernel.Functional.Validation;// Define validation rulesvarvalidationRules=newList<(Func<string,bool>rule,stringerrorMessage)>{(value =>!string.IsNullOrWhiteSpace(value),"Input cannot be empty."),(value =>value.Contains("@"),"Input must contain '@'."),(value =>value.Length<=50,"Input must not exceed 50 characters.")};// Validate input against multiple rulesvarresult=Validator.Validate("example@example.com",validationRules);if(result.IsSuccess){Console.WriteLine("Validation succeeded: "+result.Value);}else{Console.WriteLine("Validation failed with errors:");foreach(varerrorin((IValidationResult)result).Errors){Console.WriteLine($"- {error.Message}");}}

Validation types allow collecting multiple validation errors:

// Creating validation resultsValidationResultvalidationResult=ValidationResult.WithErrors(new[]{Error.Validation("User.Email.Invalid","Email address is not in a valid format"),Error.Validation("User.Password.TooShort","Password must be at least 8 characters")});// Using Match with ValidationResultstringmessage=userValidationResult.Match(onSuccess: user =>$"User created: {user.Email}",onError: errors =>$"Validation failed: {string.Join(", ",errors.Select(e =>e.Message))}");

Error Handling

The Error record type provides structured error representation:

// Predefined errorsErrornullValueError=Error.NullValue;ErrorvalidationError=Error.ValidationError;// Custom errorsErrorcustomError=Error.Failure("Order.Processing.Failed","Failed to process the order due to payment issue");// Adding detailsErrordetailedError=customError.WithDetails("Transaction ID: 1234567");

Messaging

Commands

Commands represent intentions to change system state:

// Command definitionpublicclassCreateProductCommand:ICommand<Guid>{publicstringName{get;init;}publicdecimalPrice{get;init;}}// Command handlerpublicclassCreateProductCommandHandler:ICommandHandler<CreateProductCommand,Guid>{publicasyncTask<Result<Guid>>Handle(CreateProductCommandcommand,CancellationTokencancellationToken){// Implementation to create a productvarproductId=Guid.NewGuid();returnResult.Success(productId);}}

Queries

Queries retrieve data without changing system state:

// Query definitionpublicclassGetProductByIdQuery:IQuery<ProductDto>{publicGuidId{get;init;}}// Query handlerpublicclassGetProductByIdQueryHandler:IQueryHandler<GetProductByIdQuery,ProductDto>{publicasyncTask<Result<ProductDto>>Handle(GetProductByIdQueryquery,CancellationTokencancellationToken){// Implementation to retrieve product datareturnResult.Success(newProductDto{/* ... */});}}

Events

Events represent notifications about something that has happened:

// Event definitionpublicclassUserRegisteredEvent:IEvent{publicGuidId{get;init;}publicDateTimeTimeStamp{get;init;}publicGuidCorrelationId{get;init;}publicstringUsername{get;init;}publicstringEmail{get;init;}}// Event handlerpublicclassSendWelcomeEmailHandler:IEventHandler<UserRegisteredEvent>{publicasyncTaskHandle(UserRegisteredEventnotification,CancellationTokencancellationToken){// Implementation to send welcome email}}

Domain Events

Domain events are specialized events raised within the domain model:

// Domain event definitionpublicclassOrderPlacedEvent:IDomainEvent{publicGuidId{get;init;}publicDateTimeTimeStamp{get;init;}publicGuidCorrelationId{get;init;}publicGuidOrderId{get;init;}}// Domain event handlerpublicclassUpdateInventoryHandler:IDomainEventHandler<OrderPlacedEvent>{publicasyncTaskHandle(OrderPlacedEventnotification,CancellationTokencancellationToken){// Implementation to update inventory}}

Integration Examples

Repository with Domain Markers

publicclassGenericRepository<TEntity,TId>:IRepository<TEntity,TId>whereTEntity:Entity<TId>whereTId:notnull{privatereadonlyDbContext_dbContext;privatereadonlyICurrentUserService_currentUser;privatereadonlyITenantProvider_tenantProvider;publicasyncTask<Result<TEntity>>GetByIdAsync(TIdid){varentity=await_dbContext.Set<TEntity>().FindAsync(id);returnentity!=null?Result.Success(entity):Result.Failure<TEntity>(Error.NotFound($"{typeof(TEntity).Name} with ID {id} was not found."));}publicasyncTask<Result<TId>>SaveAsync(TEntityentity){// Handle audit informationif(entityisIAuditableauditable){varnow=DateTimeOffset.UtcNow;varuserId=_currentUser.UserId??"system";if(IsNew(entity)){auditable.CreatedOn=now;auditable.CreatedBy=userId;}auditable.LastModifiedOn=now;auditable.LastModifiedBy=userId;}// Handle multi-tenancyif(entityisITenantScoped<Guid>tenantScoped&&IsNew(entity)){tenantScoped.TenantId=_tenantProvider.GetCurrentTenantId();}// Handle tracing if applicableif(entityisITraceabletraceable){traceable.CorrelationId=_currentUser.CorrelationId;traceable.TraceSource=GetType().Name;traceable.OperationName=IsNew(entity)?"Create":"Update";}try{if(IsNew(entity))_dbContext.Add(entity);await_dbContext.SaveChangesAsync();returnResult.Success(entity.Id);}catch(Exceptionex){returnResult.Failure<TId>(Error.Failure("Database.SaveFailed",$"Failed to save {typeof(TEntity).Name}: {ex.Message}"));}}publicasyncTask<Result>DeleteAsync(TEntityentity){if(entityisISoftDeletablesoftDeletable){softDeletable.IsDeleted=true;softDeletable.DeletedOn=DateTimeOffset.UtcNow;softDeletable.DeletedBy=_currentUser.UserId??"system";await_dbContext.SaveChangesAsync();returnResult.Success();}_dbContext.Remove(entity);await_dbContext.SaveChangesAsync();returnResult.Success();}privateboolIsNew(TEntityentity){return!_dbContext.Entry(entity).IsKeySet;}}

ASP.NET Core Controller with CQRS

[ApiController][Route("api/[controller]")]publicclassProductsController:ControllerBase{privatereadonlyIMediator_mediator;publicProductsController(IMediatormediator){_mediator=mediator;}[HttpGet("{id}")]publicasyncTask<IActionResult>GetProduct(Guidid){varquery=newGetProductByIdQuery{Id=id};varresult=await_mediator.Send(query);returnresult.Match(onSuccess: product =>Ok(product),onFailure: error =>error.Typeswitch{ErrorType.NotFound=>NotFound(error.Message),ErrorType.Unauthorized=>Unauthorized(error.Message),
_ =>BadRequest(error.Message)});}[HttpPost]publicasyncTask<IActionResult>CreateProduct(CreateProductRequestrequest){varcommand=newCreateProductCommand{Name=request.Name,Price=request.Price};varresult=await_mediator.Send(command);returnresult.Match(onSuccess: id =>CreatedAtAction(nameof(GetProduct),new{id},null),onFailure: error =>BadRequest(error.Message));}}

Domain Event Handling Pipeline

publicclassDomainEventDispatcher:IDomainEventDispatcher{privatereadonlyIMediator_mediator;privatereadonlyILogger<DomainEventDispatcher>_logger;publicDomainEventDispatcher(IMediatormediator,ILogger<DomainEventDispatcher>logger){_mediator=mediator;_logger=logger;}publicasyncTaskDispatchEventsAsync(IEnumerable<IDomainEvent>domainEvents){foreach(vardomainEventindomainEvents){_logger.LogInformation("Dispatching domain event {EventType} with ID {EventId}",domainEvent.GetType().Name,domainEvent.Id);await_mediator.Publish(domainEvent);}}}// Usage in a command handlerpublicclassPlaceOrderCommandHandler:ICommandHandler<PlaceOrderCommand,Guid>{privatereadonlyIRepository<Order,Guid>_orderRepository;privatereadonlyIDomainEventDispatcher_eventDispatcher;publicasyncTask<Result<Guid>>Handle(PlaceOrderCommandcommand,CancellationTokencancellationToken){// Create and place ordervarorder=newOrder(Guid.NewGuid());foreach(varitemincommand.Items){order.AddOrderLine(item.ProductId,item.Quantity);}order.Place();// Save order and dispatch eventsvarsaveResult=await_orderRepository.SaveAsync(order);if(saveResult.IsSuccess){await_eventDispatcher.DispatchEventsAsync(order.GetDomainEvents());order.ClearDomainEvents();}returnsaveResult;}}

Best Practices

DDD Best Practices

  1. Use Value Objects for concepts defined by their attributes (Money, Address, PhoneNumber)
  2. Create meaningful Aggregates with clear boundaries and invariants
  3. Keep Entities focused on domain behavior rather than data persistence
  4. Use strongly-typed IDs to prevent mixing identifier types

Functional Programming Best Practices

  1. Prefer Result<T> over exceptions for expected error cases
  2. Use Maybe<T> instead of null for optional values
  3. Chain operations with Bind and Map instead of using nested conditionals
  4. Use Match for exhaustive handling of all possible states

Domain Markers Best Practices

  1. Combine Domain Markers when an entity needs multiple cross-cutting concerns
  2. Centralize implementation logic in repositories or middleware
  3. Consider performance impacts of global filters for soft deletion and multi-tenancy
  4. Apply markers consistently across related entities

Messaging Best Practices

  1. Commands should represent intent and map to a single use case
  2. Queries should be idempotent and not modify state
  3. Use domain events for cross-aggregate communication
  4. Include correlation IDs for traceability across system boundaries

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

This project is licensed under the Apache License 2.0. See the LICENSE file for details.

About

The CoreKernel library provides a set of foundational abstractions and utilities for building robust, modular, and maintainable applications. It includes support for functional programming constructs, messaging patterns, error handling, and domain-driven design (DDD) principles.

Topics

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

CoreKernel Library

logo


The CoreKernel library provides a comprehensive set of foundational abstractions and utilities for building robust, modular, and maintainable .NET applications. It implements patterns from Domain-Driven Design (DDD), functional programming, and modern architectural approaches like CQRS and Event-Driven Architecture.

Table of Contents


Overview

The CoreKernel library is a comprehensive toolkit designed to simplify application development by providing:

  • DDD building blocks for modeling your business domain with clarity and precision
  • Functional programming constructs for safer, more expressive code
  • Cross-cutting concerns handled through domain markers
  • Messaging infrastructure for implementing CQRS and event-driven architecture
  • Consistent error handling through the Result pattern

By adopting these patterns early in your development process, you can ensure better code maintainability, domain integrity, and system scalability.

Components

The CoreKernel library consists of the following components:

  • CoreKernel.Primitives: Domain-Driven Design building blocks (entities, value objects, aggregates)
  • CoreKernel.DomainMarkers: Interfaces for cross-cutting concerns (auditing, multi-tenancy, etc.)
  • CoreKernel.Functional: Functional programming abstractions (Result, Maybe, Error types)
  • CoreKernel.Messaging: CQRS and event-driven architecture support (commands, queries, events)

Each component can be used independently or together for a complete development experience.

Installation

To use the full CoreKernel library in your project:

dotnet add package CoreKernel

Or, to use individual components:

dotnet add package CoreKernel.Primitives
dotnet add package CoreKernel.DomainMarkers
dotnet add package CoreKernel.Functional
dotnet add package CoreKernel.Messaging

Usage

Domain-Driven Design Primitives

Entities

The Entity<TId> base class provides identity-based equality comparison:

publicclassProduct:Entity<Guid>{publicstringName{get;}publicdecimalPrice{get;privateset;}publicProduct(Guidid,stringname,decimalprice):base(id){Name=name;Price=price;}publicvoidUpdatePrice(decimalnewPrice){Price=newPrice;}}

Entities are compared by ID and type:

varproduct1=newProduct(Guid.NewGuid(),"Widget",10.99m);varproduct2=newProduct(product1.Id,"Widget",10.99m);boolareEqual=product1==product2;// true, same ID

Aggregate Roots

Aggregate roots are the entry points to domain aggregates and can raise domain events:

publicclassOrder:AggregateRoot<Guid>{privatereadonlyList<OrderLine>_orderLines=new();publicIReadOnlyCollection<OrderLine>OrderLines=>_orderLines.AsReadOnly();publicdecimalTotalAmount{get;privateset;}publicOrder(Guidid):base(id){TotalAmount=0;}publicvoidAddOrderLine(Productproduct,intquantity){varorderLine=newOrderLine(Guid.NewGuid(),product.Id,product.Price,quantity);_orderLines.Add(orderLine);TotalAmount+=orderLine.LineTotal;RaiseDomainEvent(newOrderLineAddedEvent(Id,orderLine.Id));}publicvoidPlace(){// Business logic for placing an orderRaiseDomainEvent(newOrderPlacedEvent(Id));}}

Working with domain events:

// Create and manipulate the ordervarorder=newOrder(Guid.NewGuid());order.AddOrderLine(product,2);order.Place();// Retrieve domain events for processingvarevents=order.GetDomainEvents();// Process events...order.ClearDomainEvents();// Clear after processing

Value Objects

Value objects encapsulate concepts that are distinguished by their attributes rather than identity:

publicclassMoney:ValueObject{publicdecimalAmount{get;}publicstringCurrency{get;}publicMoney(decimalamount,stringcurrency){Amount=amount;Currency=currency;}protectedoverrideIEnumerable<object>GetAtomicValues(){yieldreturnAmount;yieldreturnCurrency;}publicMoneyAdd(Moneyother){if(Currency!=other.Currency)thrownewInvalidOperationException("Cannot add different currencies");returnnewMoney(Amount+other.Amount,Currency);}}

Value objects are compared by their structure, not reference:

varprice1=newMoney(100,"USD");varprice2=newMoney(100,"USD");boolareEqual=price1==price2;// true, same values

Strongly-Typed IDs

Strongly-typed IDs prevent accidental ID misuse between different entity types:

publicclassUserId:StronglyTypedId<Guid>{publicUserId(Guidvalue):base(value){}}publicclassOrderId:StronglyTypedId<Guid>{publicOrderId(Guidvalue):base(value){}}publicclassUser:Entity<UserId>{publicstringName{get;}publicUser(UserIdid,stringname):base(id){Name=name;}}

This prevents accidentally passing an OrderId to a method expecting a UserId.

Domain Markers

Auditing

Auditing interfaces track entity creation and modification:

// Basic time trackingpublicclassBlogPost:Entity<Guid>,ITimeStamped{publicstringTitle{get;set;}publicstringContent{get;set;}// ITimeStamped implementationpublicDateTimeOffsetCreatedOn{get;set;}publicDateTimeOffsetLastModifiedOn{get;set;}}// Complete auditing with user trackingpublicclassInvoice:Entity<Guid>,IAuditable{publicdecimalAmount{get;set;}publicstringCustomerName{get;set;}// IAuditable implementationpublicDateTimeOffsetCreatedOn{get;set;}publicstringCreatedBy{get;set;}publicDateTimeOffsetLastModifiedOn{get;set;}publicstringLastModifiedBy{get;set;}}

Multi-Tenancy

The ITenantScoped<TId> interface enables multi-tenant data isolation:

publicclassCustomerRecord:Entity<Guid>,ITenantScoped<Guid>{publicstringCustomerName{get;set;}publicstringContactEmail{get;set;}// ITenantScoped implementationpublicGuidTenantId{get;set;}}

This allows for automatic tenant filtering in queries.

Soft Deletion

The ISoftDeletable interface supports logical deletion of entities:

publicclassDocument:Entity<Guid>,ISoftDeletable{publicstringTitle{get;set;}publicstringContent{get;set;}// ISoftDeletable implementationpublicboolIsDeleted{get;set;}publicDateTimeOffset?DeletedOn{get;set;}publicstring?DeletedBy{get;set;}}

Tracing

The ITraceable interface enables distributed tracing:

publicclassPayment:Entity<Guid>,ITraceable{publicdecimalAmount{get;set;}publicstringPaymentMethod{get;set;}// ITraceable implementationpublicGuidCorrelationId{get;set;}publicstring?TraceSource{get;set;}publicstring?OperationName{get;set;}}

Functional Programming

Result Pattern

The Result pattern provides a way to represent operation outcomes that might fail:

// Creating resultsResultsuccessResult=Result.Success();Result<int>successWithValue=Result.Success(42);ResultfailureResult=Result.Failure(Error.ValidationError);Result<string>failureWithCustomError=Result.Failure<string>(Error.Failure("User.NotFound","The specified user was not found."));// Pattern matchingstringmessage=userResult.Match(onSuccess: user =>$"User {user.Name} was found",onFailure: error =>$"Error: {error.Message}");// Transforming resultsResult<UserDto>userDtoResult=userResult.Map(user =>newUserDto(user));// Chaining operationsResult<OrderConfirmation>confirmationResult=ValidateOrder(request).Bind(order =>CalculateTotals(order)).Bind(order =>CheckInventory(order)).Bind(order =>ProcessPayment(order)).Map(order =>GenerateConfirmation(order));

Maybe Pattern

The Maybe pattern provides type-safe handling of optional values:

// Creating Maybe valuesMaybe<string>someName=Maybe<string>.Some("John");Maybe<string>noName=Maybe<string>.None;// Using Match for handling both casesstringgreeting=someName.Match(onSome: name =>$"Hello, {name}!",onNone:()=>"Hello, stranger!");// Transforming Maybe valuesMaybe<int>nameLength=someName.Map(name =>name.Length);// Converting to ResultResult<string>nameResult=someName.ToResult("Name is required");

Validation

Validator Overview

The Validator class provides methods to validate input against single or multiple rules, returning structured results. It integrates seamlessly with the Result and ValidationResult patterns.

Validating Single Rule
usingCoreKernel.Functional.Validation;// Validate input against a single rulevarresult=Validator.Validate(input:"example@example.com",validationRule: value =>value.Contains("@"),errorMessage:"Input must contain '@'.");if(result.IsSuccess){Console.WriteLine("Validation succeeded: "+result.Value);}else{Console.WriteLine("Validation failed: "+result.Error.Message);}
Validating Multiple Rules
usingCoreKernel.Functional.Validation;// Define validation rulesvarvalidationRules=newList<(Func<string,bool>rule,stringerrorMessage)>{(value =>!string.IsNullOrWhiteSpace(value),"Input cannot be empty."),(value =>value.Contains("@"),"Input must contain '@'."),(value =>value.Length<=50,"Input must not exceed 50 characters.")};// Validate input against multiple rulesvarresult=Validator.Validate("example@example.com",validationRules);if(result.IsSuccess){Console.WriteLine("Validation succeeded: "+result.Value);}else{Console.WriteLine("Validation failed with errors:");foreach(varerrorin((IValidationResult)result).Errors){Console.WriteLine($"- {error.Message}");}}

Validation types allow collecting multiple validation errors:

// Creating validation resultsValidationResultvalidationResult=ValidationResult.WithErrors(new[]{Error.Validation("User.Email.Invalid","Email address is not in a valid format"),Error.Validation("User.Password.TooShort","Password must be at least 8 characters")});// Using Match with ValidationResultstringmessage=userValidationResult.Match(onSuccess: user =>$"User created: {user.Email}",onError: errors =>$"Validation failed: {string.Join(", ",errors.Select(e =>e.Message))}");

Error Handling

The Error record type provides structured error representation:

// Predefined errorsErrornullValueError=Error.NullValue;ErrorvalidationError=Error.ValidationError;// Custom errorsErrorcustomError=Error.Failure("Order.Processing.Failed","Failed to process the order due to payment issue");// Adding detailsErrordetailedError=customError.WithDetails("Transaction ID: 1234567");

Messaging

Commands

Commands represent intentions to change system state:

// Command definitionpublicclassCreateProductCommand:ICommand<Guid>{publicstringName{get;init;}publicdecimalPrice{get;init;}}// Command handlerpublicclassCreateProductCommandHandler:ICommandHandler<CreateProductCommand,Guid>{publicasyncTask<Result<Guid>>Handle(CreateProductCommandcommand,CancellationTokencancellationToken){// Implementation to create a productvarproductId=Guid.NewGuid();returnResult.Success(productId);}}

Queries

Queries retrieve data without changing system state:

// Query definitionpublicclassGetProductByIdQuery:IQuery<ProductDto>{publicGuidId{get;init;}}// Query handlerpublicclassGetProductByIdQueryHandler:IQueryHandler<GetProductByIdQuery,ProductDto>{publicasyncTask<Result<ProductDto>>Handle(GetProductByIdQueryquery,CancellationTokencancellationToken){// Implementation to retrieve product datareturnResult.Success(newProductDto{/* ... */});}}

Events

Events represent notifications about something that has happened:

// Event definitionpublicclassUserRegisteredEvent:IEvent{publicGuidId{get;init;}publicDateTimeTimeStamp{get;init;}publicGuidCorrelationId{get;init;}publicstringUsername{get;init;}publicstringEmail{get;init;}}// Event handlerpublicclassSendWelcomeEmailHandler:IEventHandler<UserRegisteredEvent>{publicasyncTaskHandle(UserRegisteredEventnotification,CancellationTokencancellationToken){// Implementation to send welcome email}}

Domain Events

Domain events are specialized events raised within the domain model:

// Domain event definitionpublicclassOrderPlacedEvent:IDomainEvent{publicGuidId{get;init;}publicDateTimeTimeStamp{get;init;}publicGuidCorrelationId{get;init;}publicGuidOrderId{get;init;}}// Domain event handlerpublicclassUpdateInventoryHandler:IDomainEventHandler<OrderPlacedEvent>{publicasyncTaskHandle(OrderPlacedEventnotification,CancellationTokencancellationToken){// Implementation to update inventory}}

Integration Examples

Repository with Domain Markers

publicclassGenericRepository<TEntity,TId>:IRepository<TEntity,TId>whereTEntity:Entity<TId>whereTId:notnull{privatereadonlyDbContext_dbContext;privatereadonlyICurrentUserService_currentUser;privatereadonlyITenantProvider_tenantProvider;publicasyncTask<Result<TEntity>>GetByIdAsync(TIdid){varentity=await_dbContext.Set<TEntity>().FindAsync(id);returnentity!=null?Result.Success(entity):Result.Failure<TEntity>(Error.NotFound($"{typeof(TEntity).Name} with ID {id} was not found."));}publicasyncTask<Result<TId>>SaveAsync(TEntityentity){// Handle audit informationif(entityisIAuditableauditable){varnow=DateTimeOffset.UtcNow;varuserId=_currentUser.UserId??"system";if(IsNew(entity)){auditable.CreatedOn=now;auditable.CreatedBy=userId;}auditable.LastModifiedOn=now;auditable.LastModifiedBy=userId;}// Handle multi-tenancyif(entityisITenantScoped<Guid>tenantScoped&&IsNew(entity)){tenantScoped.TenantId=_tenantProvider.GetCurrentTenantId();}// Handle tracing if applicableif(entityisITraceabletraceable){traceable.CorrelationId=_currentUser.CorrelationId;traceable.TraceSource=GetType().Name;traceable.OperationName=IsNew(entity)?"Create":"Update";}try{if(IsNew(entity))_dbContext.Add(entity);await_dbContext.SaveChangesAsync();returnResult.Success(entity.Id);}catch(Exceptionex){returnResult.Failure<TId>(Error.Failure("Database.SaveFailed",$"Failed to save {typeof(TEntity).Name}: {ex.Message}"));}}publicasyncTask<Result>DeleteAsync(TEntityentity){if(entityisISoftDeletablesoftDeletable){softDeletable.IsDeleted=true;softDeletable.DeletedOn=DateTimeOffset.UtcNow;softDeletable.DeletedBy=_currentUser.UserId??"system";await_dbContext.SaveChangesAsync();returnResult.Success();}_dbContext.Remove(entity);await_dbContext.SaveChangesAsync();returnResult.Success();}privateboolIsNew(TEntityentity){return!_dbContext.Entry(entity).IsKeySet;}}

ASP.NET Core Controller with CQRS

[ApiController][Route("api/[controller]")]publicclassProductsController:ControllerBase{privatereadonlyIMediator_mediator;publicProductsController(IMediatormediator){_mediator=mediator;}[HttpGet("{id}")]publicasyncTask<IActionResult>GetProduct(Guidid){varquery=newGetProductByIdQuery{Id=id};varresult=await_mediator.Send(query);returnresult.Match(onSuccess: product =>Ok(product),onFailure: error =>error.Typeswitch{ErrorType.NotFound=>NotFound(error.Message),ErrorType.Unauthorized=>Unauthorized(error.Message),
_ =>BadRequest(error.Message)});}[HttpPost]publicasyncTask<IActionResult>CreateProduct(CreateProductRequestrequest){varcommand=newCreateProductCommand{Name=request.Name,Price=request.Price};varresult=await_mediator.Send(command);returnresult.Match(onSuccess: id =>CreatedAtAction(nameof(GetProduct),new{id},null),onFailure: error =>BadRequest(error.Message));}}

Domain Event Handling Pipeline

publicclassDomainEventDispatcher:IDomainEventDispatcher{privatereadonlyIMediator_mediator;privatereadonlyILogger<DomainEventDispatcher>_logger;publicDomainEventDispatcher(IMediatormediator,ILogger<DomainEventDispatcher>logger){_mediator=mediator;_logger=logger;}publicasyncTaskDispatchEventsAsync(IEnumerable<IDomainEvent>domainEvents){foreach(vardomainEventindomainEvents){_logger.LogInformation("Dispatching domain event {EventType} with ID {EventId}",domainEvent.GetType().Name,domainEvent.Id);await_mediator.Publish(domainEvent);}}}// Usage in a command handlerpublicclassPlaceOrderCommandHandler:ICommandHandler<PlaceOrderCommand,Guid>{privatereadonlyIRepository<Order,Guid>_orderRepository;privatereadonlyIDomainEventDispatcher_eventDispatcher;publicasyncTask<Result<Guid>>Handle(PlaceOrderCommandcommand,CancellationTokencancellationToken){// Create and place ordervarorder=newOrder(Guid.NewGuid());foreach(varitemincommand.Items){order.AddOrderLine(item.ProductId,item.Quantity);}order.Place();// Save order and dispatch eventsvarsaveResult=await_orderRepository.SaveAsync(order);if(saveResult.IsSuccess){await_eventDispatcher.DispatchEventsAsync(order.GetDomainEvents());order.ClearDomainEvents();}returnsaveResult;}}

Best Practices

DDD Best Practices

  1. Use Value Objects for concepts defined by their attributes (Money, Address, PhoneNumber)
  2. Create meaningful Aggregates with clear boundaries and invariants
  3. Keep Entities focused on domain behavior rather than data persistence
  4. Use strongly-typed IDs to prevent mixing identifier types

Functional Programming Best Practices

  1. Prefer Result<T> over exceptions for expected error cases
  2. Use Maybe<T> instead of null for optional values
  3. Chain operations with Bind and Map instead of using nested conditionals
  4. Use Match for exhaustive handling of all possible states

Domain Markers Best Practices

  1. Combine Domain Markers when an entity needs multiple cross-cutting concerns
  2. Centralize implementation logic in repositories or middleware
  3. Consider performance impacts of global filters for soft deletion and multi-tenancy
  4. Apply markers consistently across related entities

Messaging Best Practices

  1. Commands should represent intent and map to a single use case
  2. Queries should be idempotent and not modify state
  3. Use domain events for cross-aggregate communication
  4. Include correlation IDs for traceability across system boundaries

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

This project is licensed under the Apache License 2.0. See the LICENSE file for details.

About

The CoreKernel library provides a set of foundational abstractions and utilities for building robust, modular, and maintainable applications. It includes support for functional programming constructs, messaging patterns, error handling, and domain-driven design (DDD) principles.

Topics

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

CoreKernel Library

logo


The CoreKernel library provides a comprehensive set of foundational abstractions and utilities for building robust, modular, and maintainable .NET applications. It implements patterns from Domain-Driven Design (DDD), functional programming, and modern architectural approaches like CQRS and Event-Driven Architecture.

Table of Contents


Overview

The CoreKernel library is a comprehensive toolkit designed to simplify application development by providing:

  • DDD building blocks for modeling your business domain with clarity and precision
  • Functional programming constructs for safer, more expressive code
  • Cross-cutting concerns handled through domain markers
  • Messaging infrastructure for implementing CQRS and event-driven architecture
  • Consistent error handling through the Result pattern

By adopting these patterns early in your development process, you can ensure better code maintainability, domain integrity, and system scalability.

Components

The CoreKernel library consists of the following components:

  • CoreKernel.Primitives: Domain-Driven Design building blocks (entities, value objects, aggregates)
  • CoreKernel.DomainMarkers: Interfaces for cross-cutting concerns (auditing, multi-tenancy, etc.)
  • CoreKernel.Functional: Functional programming abstractions (Result, Maybe, Error types)
  • CoreKernel.Messaging: CQRS and event-driven architecture support (commands, queries, events)

Each component can be used independently or together for a complete development experience.

Installation

To use the full CoreKernel library in your project:

dotnet add package CoreKernel

Or, to use individual components:

dotnet add package CoreKernel.Primitives
dotnet add package CoreKernel.DomainMarkers
dotnet add package CoreKernel.Functional
dotnet add package CoreKernel.Messaging

Usage

Domain-Driven Design Primitives

Entities

The Entity<TId> base class provides identity-based equality comparison:

publicclassProduct:Entity<Guid>{publicstringName{get;}publicdecimalPrice{get;privateset;}publicProduct(Guidid,stringname,decimalprice):base(id){Name=name;Price=price;}publicvoidUpdatePrice(decimalnewPrice){Price=newPrice;}}

Entities are compared by ID and type:

varproduct1=newProduct(Guid.NewGuid(),"Widget",10.99m);varproduct2=newProduct(product1.Id,"Widget",10.99m);boolareEqual=product1==product2;// true, same ID

Aggregate Roots

Aggregate roots are the entry points to domain aggregates and can raise domain events:

publicclassOrder:AggregateRoot<Guid>{privatereadonlyList<OrderLine>_orderLines=new();publicIReadOnlyCollection<OrderLine>OrderLines=>_orderLines.AsReadOnly();publicdecimalTotalAmount{get;privateset;}publicOrder(Guidid):base(id){TotalAmount=0;}publicvoidAddOrderLine(Productproduct,intquantity){varorderLine=newOrderLine(Guid.NewGuid(),product.Id,product.Price,quantity);_orderLines.Add(orderLine);TotalAmount+=orderLine.LineTotal;RaiseDomainEvent(newOrderLineAddedEvent(Id,orderLine.Id));}publicvoidPlace(){// Business logic for placing an orderRaiseDomainEvent(newOrderPlacedEvent(Id));}}

Working with domain events:

// Create and manipulate the ordervarorder=newOrder(Guid.NewGuid());order.AddOrderLine(product,2);order.Place();// Retrieve domain events for processingvarevents=order.GetDomainEvents();// Process events...order.ClearDomainEvents();// Clear after processing

Value Objects

Value objects encapsulate concepts that are distinguished by their attributes rather than identity:

publicclassMoney:ValueObject{publicdecimalAmount{get;}publicstringCurrency{get;}publicMoney(decimalamount,stringcurrency){Amount=amount;Currency=currency;}protectedoverrideIEnumerable<object>GetAtomicValues(){yieldreturnAmount;yieldreturnCurrency;}publicMoneyAdd(Moneyother){if(Currency!=other.Currency)thrownewInvalidOperationException("Cannot add different currencies");returnnewMoney(Amount+other.Amount,Currency);}}

Value objects are compared by their structure, not reference:

varprice1=newMoney(100,"USD");varprice2=newMoney(100,"USD");boolareEqual=price1==price2;// true, same values

Strongly-Typed IDs

Strongly-typed IDs prevent accidental ID misuse between different entity types:

publicclassUserId:StronglyTypedId<Guid>{publicUserId(Guidvalue):base(value){}}publicclassOrderId:StronglyTypedId<Guid>{publicOrderId(Guidvalue):base(value){}}publicclassUser:Entity<UserId>{publicstringName{get;}publicUser(UserIdid,stringname):base(id){Name=name;}}

This prevents accidentally passing an OrderId to a method expecting a UserId.

Domain Markers

Auditing

Auditing interfaces track entity creation and modification:

// Basic time trackingpublicclassBlogPost:Entity<Guid>,ITimeStamped{publicstringTitle{get;set;}publicstringContent{get;set;}// ITimeStamped implementationpublicDateTimeOffsetCreatedOn{get;set;}publicDateTimeOffsetLastModifiedOn{get;set;}}// Complete auditing with user trackingpublicclassInvoice:Entity<Guid>,IAuditable{publicdecimalAmount{get;set;}publicstringCustomerName{get;set;}// IAuditable implementationpublicDateTimeOffsetCreatedOn{get;set;}publicstringCreatedBy{get;set;}publicDateTimeOffsetLastModifiedOn{get;set;}publicstringLastModifiedBy{get;set;}}

Multi-Tenancy

The ITenantScoped<TId> interface enables multi-tenant data isolation:

publicclassCustomerRecord:Entity<Guid>,ITenantScoped<Guid>{publicstringCustomerName{get;set;}publicstringContactEmail{get;set;}// ITenantScoped implementationpublicGuidTenantId{get;set;}}

This allows for automatic tenant filtering in queries.

Soft Deletion

The ISoftDeletable interface supports logical deletion of entities:

publicclassDocument:Entity<Guid>,ISoftDeletable{publicstringTitle{get;set;}publicstringContent{get;set;}// ISoftDeletable implementationpublicboolIsDeleted{get;set;}publicDateTimeOffset?DeletedOn{get;set;}publicstring?DeletedBy{get;set;}}

Tracing

The ITraceable interface enables distributed tracing:

publicclassPayment:Entity<Guid>,ITraceable{publicdecimalAmount{get;set;}publicstringPaymentMethod{get;set;}// ITraceable implementationpublicGuidCorrelationId{get;set;}publicstring?TraceSource{get;set;}publicstring?OperationName{get;set;}}

Functional Programming

Result Pattern

The Result pattern provides a way to represent operation outcomes that might fail:

// Creating resultsResultsuccessResult=Result.Success();Result<int>successWithValue=Result.Success(42);ResultfailureResult=Result.Failure(Error.ValidationError);Result<string>failureWithCustomError=Result.Failure<string>(Error.Failure("User.NotFound","The specified user was not found."));// Pattern matchingstringmessage=userResult.Match(onSuccess: user =>$"User {user.Name} was found",onFailure: error =>$"Error: {error.Message}");// Transforming resultsResult<UserDto>userDtoResult=userResult.Map(user =>newUserDto(user));// Chaining operationsResult<OrderConfirmation>confirmationResult=ValidateOrder(request).Bind(order =>CalculateTotals(order)).Bind(order =>CheckInventory(order)).Bind(order =>ProcessPayment(order)).Map(order =>GenerateConfirmation(order));

Maybe Pattern

The Maybe pattern provides type-safe handling of optional values:

// Creating Maybe valuesMaybe<string>someName=Maybe<string>.Some("John");Maybe<string>noName=Maybe<string>.None;// Using Match for handling both casesstringgreeting=someName.Match(onSome: name =>$"Hello, {name}!",onNone:()=>"Hello, stranger!");// Transforming Maybe valuesMaybe<int>nameLength=someName.Map(name =>name.Length);// Converting to ResultResult<string>nameResult=someName.ToResult("Name is required");

Validation

Validator Overview

The Validator class provides methods to validate input against single or multiple rules, returning structured results. It integrates seamlessly with the Result and ValidationResult patterns.

Validating Single Rule
usingCoreKernel.Functional.Validation;// Validate input against a single rulevarresult=Validator.Validate(input:"example@example.com",validationRule: value =>value.Contains("@"),errorMessage:"Input must contain '@'.");if(result.IsSuccess){Console.WriteLine("Validation succeeded: "+result.Value);}else{Console.WriteLine("Validation failed: "+result.Error.Message);}
Validating Multiple Rules
usingCoreKernel.Functional.Validation;// Define validation rulesvarvalidationRules=newList<(Func<string,bool>rule,stringerrorMessage)>{(value =>!string.IsNullOrWhiteSpace(value),"Input cannot be empty."),(value =>value.Contains("@"),"Input must contain '@'."),(value =>value.Length<=50,"Input must not exceed 50 characters.")};// Validate input against multiple rulesvarresult=Validator.Validate("example@example.com",validationRules);if(result.IsSuccess){Console.WriteLine("Validation succeeded: "+result.Value);}else{Console.WriteLine("Validation failed with errors:");foreach(varerrorin((IValidationResult)result).Errors){Console.WriteLine($"- {error.Message}");}}

Validation types allow collecting multiple validation errors:

// Creating validation resultsValidationResultvalidationResult=ValidationResult.WithErrors(new[]{Error.Validation("User.Email.Invalid","Email address is not in a valid format"),Error.Validation("User.Password.TooShort","Password must be at least 8 characters")});// Using Match with ValidationResultstringmessage=userValidationResult.Match(onSuccess: user =>$"User created: {user.Email}",onError: errors =>$"Validation failed: {string.Join(", ",errors.Select(e =>e.Message))}");

Error Handling

The Error record type provides structured error representation:

// Predefined errorsErrornullValueError=Error.NullValue;ErrorvalidationError=Error.ValidationError;// Custom errorsErrorcustomError=Error.Failure("Order.Processing.Failed","Failed to process the order due to payment issue");// Adding detailsErrordetailedError=customError.WithDetails("Transaction ID: 1234567");

Messaging

Commands

Commands represent intentions to change system state:

// Command definitionpublicclassCreateProductCommand:ICommand<Guid>{publicstringName{get;init;}publicdecimalPrice{get;init;}}// Command handlerpublicclassCreateProductCommandHandler:ICommandHandler<CreateProductCommand,Guid>{publicasyncTask<Result<Guid>>Handle(CreateProductCommandcommand,CancellationTokencancellationToken){// Implementation to create a productvarproductId=Guid.NewGuid();returnResult.Success(productId);}}

Queries

Queries retrieve data without changing system state:

// Query definitionpublicclassGetProductByIdQuery:IQuery<ProductDto>{publicGuidId{get;init;}}// Query handlerpublicclassGetProductByIdQueryHandler:IQueryHandler<GetProductByIdQuery,ProductDto>{publicasyncTask<Result<ProductDto>>Handle(GetProductByIdQueryquery,CancellationTokencancellationToken){// Implementation to retrieve product datareturnResult.Success(newProductDto{/* ... */});}}

Events

Events represent notifications about something that has happened:

// Event definitionpublicclassUserRegisteredEvent:IEvent{publicGuidId{get;init;}publicDateTimeTimeStamp{get;init;}publicGuidCorrelationId{get;init;}publicstringUsername{get;init;}publicstringEmail{get;init;}}// Event handlerpublicclassSendWelcomeEmailHandler:IEventHandler<UserRegisteredEvent>{publicasyncTaskHandle(UserRegisteredEventnotification,CancellationTokencancellationToken){// Implementation to send welcome email}}

Domain Events

Domain events are specialized events raised within the domain model:

// Domain event definitionpublicclassOrderPlacedEvent:IDomainEvent{publicGuidId{get;init;}publicDateTimeTimeStamp{get;init;}publicGuidCorrelationId{get;init;}publicGuidOrderId{get;init;}}// Domain event handlerpublicclassUpdateInventoryHandler:IDomainEventHandler<OrderPlacedEvent>{publicasyncTaskHandle(OrderPlacedEventnotification,CancellationTokencancellationToken){// Implementation to update inventory}}

Integration Examples

Repository with Domain Markers

publicclassGenericRepository<TEntity,TId>:IRepository<TEntity,TId>whereTEntity:Entity<TId>whereTId:notnull{privatereadonlyDbContext_dbContext;privatereadonlyICurrentUserService_currentUser;privatereadonlyITenantProvider_tenantProvider;publicasyncTask<Result<TEntity>>GetByIdAsync(TIdid){varentity=await_dbContext.Set<TEntity>().FindAsync(id);returnentity!=null?Result.Success(entity):Result.Failure<TEntity>(Error.NotFound($"{typeof(TEntity).Name} with ID {id} was not found."));}publicasyncTask<Result<TId>>SaveAsync(TEntityentity){// Handle audit informationif(entityisIAuditableauditable){varnow=DateTimeOffset.UtcNow;varuserId=_currentUser.UserId??"system";if(IsNew(entity)){auditable.CreatedOn=now;auditable.CreatedBy=userId;}auditable.LastModifiedOn=now;auditable.LastModifiedBy=userId;}// Handle multi-tenancyif(entityisITenantScoped<Guid>tenantScoped&&IsNew(entity)){tenantScoped.TenantId=_tenantProvider.GetCurrentTenantId();}// Handle tracing if applicableif(entityisITraceabletraceable){traceable.CorrelationId=_currentUser.CorrelationId;traceable.TraceSource=GetType().Name;traceable.OperationName=IsNew(entity)?"Create":"Update";}try{if(IsNew(entity))_dbContext.Add(entity);await_dbContext.SaveChangesAsync();returnResult.Success(entity.Id);}catch(Exceptionex){returnResult.Failure<TId>(Error.Failure("Database.SaveFailed",$"Failed to save {typeof(TEntity).Name}: {ex.Message}"));}}publicasyncTask<Result>DeleteAsync(TEntityentity){if(entityisISoftDeletablesoftDeletable){softDeletable.IsDeleted=true;softDeletable.DeletedOn=DateTimeOffset.UtcNow;softDeletable.DeletedBy=_currentUser.UserId??"system";await_dbContext.SaveChangesAsync();returnResult.Success();}_dbContext.Remove(entity);await_dbContext.SaveChangesAsync();returnResult.Success();}privateboolIsNew(TEntityentity){return!_dbContext.Entry(entity).IsKeySet;}}

ASP.NET Core Controller with CQRS

[ApiController][Route("api/[controller]")]publicclassProductsController:ControllerBase{privatereadonlyIMediator_mediator;publicProductsController(IMediatormediator){_mediator=mediator;}[HttpGet("{id}")]publicasyncTask<IActionResult>GetProduct(Guidid){varquery=newGetProductByIdQuery{Id=id};varresult=await_mediator.Send(query);returnresult.Match(onSuccess: product =>Ok(product),onFailure: error =>error.Typeswitch{ErrorType.NotFound=>NotFound(error.Message),ErrorType.Unauthorized=>Unauthorized(error.Message),
_ =>BadRequest(error.Message)});}[HttpPost]publicasyncTask<IActionResult>CreateProduct(CreateProductRequestrequest){varcommand=newCreateProductCommand{Name=request.Name,Price=request.Price};varresult=await_mediator.Send(command);returnresult.Match(onSuccess: id =>CreatedAtAction(nameof(GetProduct),new{id},null),onFailure: error =>BadRequest(error.Message));}}

Domain Event Handling Pipeline

publicclassDomainEventDispatcher:IDomainEventDispatcher{privatereadonlyIMediator_mediator;privatereadonlyILogger<DomainEventDispatcher>_logger;publicDomainEventDispatcher(IMediatormediator,ILogger<DomainEventDispatcher>logger){_mediator=mediator;_logger=logger;}publicasyncTaskDispatchEventsAsync(IEnumerable<IDomainEvent>domainEvents){foreach(vardomainEventindomainEvents){_logger.LogInformation("Dispatching domain event {EventType} with ID {EventId}",domainEvent.GetType().Name,domainEvent.Id);await_mediator.Publish(domainEvent);}}}// Usage in a command handlerpublicclassPlaceOrderCommandHandler:ICommandHandler<PlaceOrderCommand,Guid>{privatereadonlyIRepository<Order,Guid>_orderRepository;privatereadonlyIDomainEventDispatcher_eventDispatcher;publicasyncTask<Result<Guid>>Handle(PlaceOrderCommandcommand,CancellationTokencancellationToken){// Create and place ordervarorder=newOrder(Guid.NewGuid());foreach(varitemincommand.Items){order.AddOrderLine(item.ProductId,item.Quantity);}order.Place();// Save order and dispatch eventsvarsaveResult=await_orderRepository.SaveAsync(order);if(saveResult.IsSuccess){await_eventDispatcher.DispatchEventsAsync(order.GetDomainEvents());order.ClearDomainEvents();}returnsaveResult;}}

Best Practices

DDD Best Practices

  1. Use Value Objects for concepts defined by their attributes (Money, Address, PhoneNumber)
  2. Create meaningful Aggregates with clear boundaries and invariants
  3. Keep Entities focused on domain behavior rather than data persistence
  4. Use strongly-typed IDs to prevent mixing identifier types

Functional Programming Best Practices

  1. Prefer Result<T> over exceptions for expected error cases
  2. Use Maybe<T> instead of null for optional values
  3. Chain operations with Bind and Map instead of using nested conditionals
  4. Use Match for exhaustive handling of all possible states

Domain Markers Best Practices

  1. Combine Domain Markers when an entity needs multiple cross-cutting concerns
  2. Centralize implementation logic in repositories or middleware
  3. Consider performance impacts of global filters for soft deletion and multi-tenancy
  4. Apply markers consistently across related entities

Messaging Best Practices

  1. Commands should represent intent and map to a single use case
  2. Queries should be idempotent and not modify state
  3. Use domain events for cross-aggregate communication
  4. Include correlation IDs for traceability across system boundaries

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

This project is licensed under the Apache License 2.0. See the LICENSE file for details.

About

The CoreKernel library provides a set of foundational abstractions and utilities for building robust, modular, and maintainable applications. It includes support for functional programming constructs, messaging patterns, error handling, and domain-driven design (DDD) principles.

Topics

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

CoreKernel Library

logo


The CoreKernel library provides a comprehensive set of foundational abstractions and utilities for building robust, modular, and maintainable .NET applications. It implements patterns from Domain-Driven Design (DDD), functional programming, and modern architectural approaches like CQRS and Event-Driven Architecture.

Table of Contents


Overview

The CoreKernel library is a comprehensive toolkit designed to simplify application development by providing:

  • DDD building blocks for modeling your business domain with clarity and precision
  • Functional programming constructs for safer, more expressive code
  • Cross-cutting concerns handled through domain markers
  • Messaging infrastructure for implementing CQRS and event-driven architecture
  • Consistent error handling through the Result pattern

By adopting these patterns early in your development process, you can ensure better code maintainability, domain integrity, and system scalability.

Components

The CoreKernel library consists of the following components:

  • CoreKernel.Primitives: Domain-Driven Design building blocks (entities, value objects, aggregates)
  • CoreKernel.DomainMarkers: Interfaces for cross-cutting concerns (auditing, multi-tenancy, etc.)
  • CoreKernel.Functional: Functional programming abstractions (Result, Maybe, Error types)
  • CoreKernel.Messaging: CQRS and event-driven architecture support (commands, queries, events)

Each component can be used independently or together for a complete development experience.

Installation

To use the full CoreKernel library in your project:

dotnet add package CoreKernel

Or, to use individual components:

dotnet add package CoreKernel.Primitives
dotnet add package CoreKernel.DomainMarkers
dotnet add package CoreKernel.Functional
dotnet add package CoreKernel.Messaging

Usage

Domain-Driven Design Primitives

Entities

The Entity<TId> base class provides identity-based equality comparison:

publicclassProduct:Entity<Guid>{publicstringName{get;}publicdecimalPrice{get;privateset;}publicProduct(Guidid,stringname,decimalprice):base(id){Name=name;Price=price;}publicvoidUpdatePrice(decimalnewPrice){Price=newPrice;}}

Entities are compared by ID and type:

varproduct1=newProduct(Guid.NewGuid(),"Widget",10.99m);varproduct2=newProduct(product1.Id,"Widget",10.99m);boolareEqual=product1==product2;// true, same ID

Aggregate Roots

Aggregate roots are the entry points to domain aggregates and can raise domain events:

publicclassOrder:AggregateRoot<Guid>{privatereadonlyList<OrderLine>_orderLines=new();publicIReadOnlyCollection<OrderLine>OrderLines=>_orderLines.AsReadOnly();publicdecimalTotalAmount{get;privateset;}publicOrder(Guidid):base(id){TotalAmount=0;}publicvoidAddOrderLine(Productproduct,intquantity){varorderLine=newOrderLine(Guid.NewGuid(),product.Id,product.Price,quantity);_orderLines.Add(orderLine);TotalAmount+=orderLine.LineTotal;RaiseDomainEvent(newOrderLineAddedEvent(Id,orderLine.Id));}publicvoidPlace(){// Business logic for placing an orderRaiseDomainEvent(newOrderPlacedEvent(Id));}}

Working with domain events:

// Create and manipulate the ordervarorder=newOrder(Guid.NewGuid());order.AddOrderLine(product,2);order.Place();// Retrieve domain events for processingvarevents=order.GetDomainEvents();// Process events...order.ClearDomainEvents();// Clear after processing

Value Objects

Value objects encapsulate concepts that are distinguished by their attributes rather than identity:

publicclassMoney:ValueObject{publicdecimalAmount{get;}publicstringCurrency{get;}publicMoney(decimalamount,stringcurrency){Amount=amount;Currency=currency;}protectedoverrideIEnumerable<object>GetAtomicValues(){yieldreturnAmount;yieldreturnCurrency;}publicMoneyAdd(Moneyother){if(Currency!=other.Currency)thrownewInvalidOperationException("Cannot add different currencies");returnnewMoney(Amount+other.Amount,Currency);}}

Value objects are compared by their structure, not reference:

varprice1=newMoney(100,"USD");varprice2=newMoney(100,"USD");boolareEqual=price1==price2;// true, same values

Strongly-Typed IDs

Strongly-typed IDs prevent accidental ID misuse between different entity types:

publicclassUserId:StronglyTypedId<Guid>{publicUserId(Guidvalue):base(value){}}publicclassOrderId:StronglyTypedId<Guid>{publicOrderId(Guidvalue):base(value){}}publicclassUser:Entity<UserId>{publicstringName{get;}publicUser(UserIdid,stringname):base(id){Name=name;}}

This prevents accidentally passing an OrderId to a method expecting a UserId.

Domain Markers

Auditing

Auditing interfaces track entity creation and modification:

// Basic time trackingpublicclassBlogPost:Entity<Guid>,ITimeStamped{publicstringTitle{get;set;}publicstringContent{get;set;}// ITimeStamped implementationpublicDateTimeOffsetCreatedOn{get;set;}publicDateTimeOffsetLastModifiedOn{get;set;}}// Complete auditing with user trackingpublicclassInvoice:Entity<Guid>,IAuditable{publicdecimalAmount{get;set;}publicstringCustomerName{get;set;}// IAuditable implementationpublicDateTimeOffsetCreatedOn{get;set;}publicstringCreatedBy{get;set;}publicDateTimeOffsetLastModifiedOn{get;set;}publicstringLastModifiedBy{get;set;}}

Multi-Tenancy

The ITenantScoped<TId> interface enables multi-tenant data isolation:

publicclassCustomerRecord:Entity<Guid>,ITenantScoped<Guid>{publicstringCustomerName{get;set;}publicstringContactEmail{get;set;}// ITenantScoped implementationpublicGuidTenantId{get;set;}}

This allows for automatic tenant filtering in queries.

Soft Deletion

The ISoftDeletable interface supports logical deletion of entities:

publicclassDocument:Entity<Guid>,ISoftDeletable{publicstringTitle{get;set;}publicstringContent{get;set;}// ISoftDeletable implementationpublicboolIsDeleted{get;set;}publicDateTimeOffset?DeletedOn{get;set;}publicstring?DeletedBy{get;set;}}

Tracing

The ITraceable interface enables distributed tracing:

publicclassPayment:Entity<Guid>,ITraceable{publicdecimalAmount{get;set;}publicstringPaymentMethod{get;set;}// ITraceable implementationpublicGuidCorrelationId{get;set;}publicstring?TraceSource{get;set;}publicstring?OperationName{get;set;}}

Functional Programming

Result Pattern

The Result pattern provides a way to represent operation outcomes that might fail:

// Creating resultsResultsuccessResult=Result.Success();Result<int>successWithValue=Result.Success(42);ResultfailureResult=Result.Failure(Error.ValidationError);Result<string>failureWithCustomError=Result.Failure<string>(Error.Failure("User.NotFound","The specified user was not found."));// Pattern matchingstringmessage=userResult.Match(onSuccess: user =>$"User {user.Name} was found",onFailure: error =>$"Error: {error.Message}");// Transforming resultsResult<UserDto>userDtoResult=userResult.Map(user =>newUserDto(user));// Chaining operationsResult<OrderConfirmation>confirmationResult=ValidateOrder(request).Bind(order =>CalculateTotals(order)).Bind(order =>CheckInventory(order)).Bind(order =>ProcessPayment(order)).Map(order =>GenerateConfirmation(order));

Maybe Pattern

The Maybe pattern provides type-safe handling of optional values:

// Creating Maybe valuesMaybe<string>someName=Maybe<string>.Some("John");Maybe<string>noName=Maybe<string>.None;// Using Match for handling both casesstringgreeting=someName.Match(onSome: name =>$"Hello, {name}!",onNone:()=>"Hello, stranger!");// Transforming Maybe valuesMaybe<int>nameLength=someName.Map(name =>name.Length);// Converting to ResultResult<string>nameResult=someName.ToResult("Name is required");

Validation

Validator Overview

The Validator class provides methods to validate input against single or multiple rules, returning structured results. It integrates seamlessly with the Result and ValidationResult patterns.

Validating Single Rule
usingCoreKernel.Functional.Validation;// Validate input against a single rulevarresult=Validator.Validate(input:"example@example.com",validationRule: value =>value.Contains("@"),errorMessage:"Input must contain '@'.");if(result.IsSuccess){Console.WriteLine("Validation succeeded: "+result.Value);}else{Console.WriteLine("Validation failed: "+result.Error.Message);}
Validating Multiple Rules
usingCoreKernel.Functional.Validation;// Define validation rulesvarvalidationRules=newList<(Func<string,bool>rule,stringerrorMessage)>{(value =>!string.IsNullOrWhiteSpace(value),"Input cannot be empty."),(value =>value.Contains("@"),"Input must contain '@'."),(value =>value.Length<=50,"Input must not exceed 50 characters.")};// Validate input against multiple rulesvarresult=Validator.Validate("example@example.com",validationRules);if(result.IsSuccess){Console.WriteLine("Validation succeeded: "+result.Value);}else{Console.WriteLine("Validation failed with errors:");foreach(varerrorin((IValidationResult)result).Errors){Console.WriteLine($"- {error.Message}");}}

Validation types allow collecting multiple validation errors:

// Creating validation resultsValidationResultvalidationResult=ValidationResult.WithErrors(new[]{Error.Validation("User.Email.Invalid","Email address is not in a valid format"),Error.Validation("User.Password.TooShort","Password must be at least 8 characters")});// Using Match with ValidationResultstringmessage=userValidationResult.Match(onSuccess: user =>$"User created: {user.Email}",onError: errors =>$"Validation failed: {string.Join(", ",errors.Select(e =>e.Message))}");

Error Handling

The Error record type provides structured error representation:

// Predefined errorsErrornullValueError=Error.NullValue;ErrorvalidationError=Error.ValidationError;// Custom errorsErrorcustomError=Error.Failure("Order.Processing.Failed","Failed to process the order due to payment issue");// Adding detailsErrordetailedError=customError.WithDetails("Transaction ID: 1234567");

Messaging

Commands

Commands represent intentions to change system state:

// Command definitionpublicclassCreateProductCommand:ICommand<Guid>{publicstringName{get;init;}publicdecimalPrice{get;init;}}// Command handlerpublicclassCreateProductCommandHandler:ICommandHandler<CreateProductCommand,Guid>{publicasyncTask<Result<Guid>>Handle(CreateProductCommandcommand,CancellationTokencancellationToken){// Implementation to create a productvarproductId=Guid.NewGuid();returnResult.Success(productId);}}

Queries

Queries retrieve data without changing system state:

// Query definitionpublicclassGetProductByIdQuery:IQuery<ProductDto>{publicGuidId{get;init;}}// Query handlerpublicclassGetProductByIdQueryHandler:IQueryHandler<GetProductByIdQuery,ProductDto>{publicasyncTask<Result<ProductDto>>Handle(GetProductByIdQueryquery,CancellationTokencancellationToken){// Implementation to retrieve product datareturnResult.Success(newProductDto{/* ... */});}}

Events

Events represent notifications about something that has happened:

// Event definitionpublicclassUserRegisteredEvent:IEvent{publicGuidId{get;init;}publicDateTimeTimeStamp{get;init;}publicGuidCorrelationId{get;init;}publicstringUsername{get;init;}publicstringEmail{get;init;}}// Event handlerpublicclassSendWelcomeEmailHandler:IEventHandler<UserRegisteredEvent>{publicasyncTaskHandle(UserRegisteredEventnotification,CancellationTokencancellationToken){// Implementation to send welcome email}}

Domain Events

Domain events are specialized events raised within the domain model:

// Domain event definitionpublicclassOrderPlacedEvent:IDomainEvent{publicGuidId{get;init;}publicDateTimeTimeStamp{get;init;}publicGuidCorrelationId{get;init;}publicGuidOrderId{get;init;}}// Domain event handlerpublicclassUpdateInventoryHandler:IDomainEventHandler<OrderPlacedEvent>{publicasyncTaskHandle(OrderPlacedEventnotification,CancellationTokencancellationToken){// Implementation to update inventory}}

Integration Examples

Repository with Domain Markers

publicclassGenericRepository<TEntity,TId>:IRepository<TEntity,TId>whereTEntity:Entity<TId>whereTId:notnull{privatereadonlyDbContext_dbContext;privatereadonlyICurrentUserService_currentUser;privatereadonlyITenantProvider_tenantProvider;publicasyncTask<Result<TEntity>>GetByIdAsync(TIdid){varentity=await_dbContext.Set<TEntity>().FindAsync(id);returnentity!=null?Result.Success(entity):Result.Failure<TEntity>(Error.NotFound($"{typeof(TEntity).Name} with ID {id} was not found."));}publicasyncTask<Result<TId>>SaveAsync(TEntityentity){// Handle audit informationif(entityisIAuditableauditable){varnow=DateTimeOffset.UtcNow;varuserId=_currentUser.UserId??"system";if(IsNew(entity)){auditable.CreatedOn=now;auditable.CreatedBy=userId;}auditable.LastModifiedOn=now;auditable.LastModifiedBy=userId;}// Handle multi-tenancyif(entityisITenantScoped<Guid>tenantScoped&&IsNew(entity)){tenantScoped.TenantId=_tenantProvider.GetCurrentTenantId();}// Handle tracing if applicableif(entityisITraceabletraceable){traceable.CorrelationId=_currentUser.CorrelationId;traceable.TraceSource=GetType().Name;traceable.OperationName=IsNew(entity)?"Create":"Update";}try{if(IsNew(entity))_dbContext.Add(entity);await_dbContext.SaveChangesAsync();returnResult.Success(entity.Id);}catch(Exceptionex){returnResult.Failure<TId>(Error.Failure("Database.SaveFailed",$"Failed to save {typeof(TEntity).Name}: {ex.Message}"));}}publicasyncTask<Result>DeleteAsync(TEntityentity){if(entityisISoftDeletablesoftDeletable){softDeletable.IsDeleted=true;softDeletable.DeletedOn=DateTimeOffset.UtcNow;softDeletable.DeletedBy=_currentUser.UserId??"system";await_dbContext.SaveChangesAsync();returnResult.Success();}_dbContext.Remove(entity);await_dbContext.SaveChangesAsync();returnResult.Success();}privateboolIsNew(TEntityentity){return!_dbContext.Entry(entity).IsKeySet;}}

ASP.NET Core Controller with CQRS

[ApiController][Route("api/[controller]")]publicclassProductsController:ControllerBase{privatereadonlyIMediator_mediator;publicProductsController(IMediatormediator){_mediator=mediator;}[HttpGet("{id}")]publicasyncTask<IActionResult>GetProduct(Guidid){varquery=newGetProductByIdQuery{Id=id};varresult=await_mediator.Send(query);returnresult.Match(onSuccess: product =>Ok(product),onFailure: error =>error.Typeswitch{ErrorType.NotFound=>NotFound(error.Message),ErrorType.Unauthorized=>Unauthorized(error.Message),
_ =>BadRequest(error.Message)});}[HttpPost]publicasyncTask<IActionResult>CreateProduct(CreateProductRequestrequest){varcommand=newCreateProductCommand{Name=request.Name,Price=request.Price};varresult=await_mediator.Send(command);returnresult.Match(onSuccess: id =>CreatedAtAction(nameof(GetProduct),new{id},null),onFailure: error =>BadRequest(error.Message));}}

Domain Event Handling Pipeline

publicclassDomainEventDispatcher:IDomainEventDispatcher{privatereadonlyIMediator_mediator;privatereadonlyILogger<DomainEventDispatcher>_logger;publicDomainEventDispatcher(IMediatormediator,ILogger<DomainEventDispatcher>logger){_mediator=mediator;_logger=logger;}publicasyncTaskDispatchEventsAsync(IEnumerable<IDomainEvent>domainEvents){foreach(vardomainEventindomainEvents){_logger.LogInformation("Dispatching domain event {EventType} with ID {EventId}",domainEvent.GetType().Name,domainEvent.Id);await_mediator.Publish(domainEvent);}}}// Usage in a command handlerpublicclassPlaceOrderCommandHandler:ICommandHandler<PlaceOrderCommand,Guid>{privatereadonlyIRepository<Order,Guid>_orderRepository;privatereadonlyIDomainEventDispatcher_eventDispatcher;publicasyncTask<Result<Guid>>Handle(PlaceOrderCommandcommand,CancellationTokencancellationToken){// Create and place ordervarorder=newOrder(Guid.NewGuid());foreach(varitemincommand.Items){order.AddOrderLine(item.ProductId,item.Quantity);}order.Place();// Save order and dispatch eventsvarsaveResult=await_orderRepository.SaveAsync(order);if(saveResult.IsSuccess){await_eventDispatcher.DispatchEventsAsync(order.GetDomainEvents());order.ClearDomainEvents();}returnsaveResult;}}

Best Practices

DDD Best Practices

  1. Use Value Objects for concepts defined by their attributes (Money, Address, PhoneNumber)
  2. Create meaningful Aggregates with clear boundaries and invariants
  3. Keep Entities focused on domain behavior rather than data persistence
  4. Use strongly-typed IDs to prevent mixing identifier types

Functional Programming Best Practices

  1. Prefer Result<T> over exceptions for expected error cases
  2. Use Maybe<T> instead of null for optional values
  3. Chain operations with Bind and Map instead of using nested conditionals
  4. Use Match for exhaustive handling of all possible states

Domain Markers Best Practices

  1. Combine Domain Markers when an entity needs multiple cross-cutting concerns
  2. Centralize implementation logic in repositories or middleware
  3. Consider performance impacts of global filters for soft deletion and multi-tenancy
  4. Apply markers consistently across related entities

Messaging Best Practices

  1. Commands should represent intent and map to a single use case
  2. Queries should be idempotent and not modify state
  3. Use domain events for cross-aggregate communication
  4. Include correlation IDs for traceability across system boundaries

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

This project is licensed under the Apache License 2.0. See the LICENSE file for details.

About

The CoreKernel library provides a set of foundational abstractions and utilities for building robust, modular, and maintainable applications. It includes support for functional programming constructs, messaging patterns, error handling, and domain-driven design (DDD) principles.

Topics

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

CoreKernel Library

logo


The CoreKernel library provides a comprehensive set of foundational abstractions and utilities for building robust, modular, and maintainable .NET applications. It implements patterns from Domain-Driven Design (DDD), functional programming, and modern architectural approaches like CQRS and Event-Driven Architecture.

Table of Contents


Overview

The CoreKernel library is a comprehensive toolkit designed to simplify application development by providing:

  • DDD building blocks for modeling your business domain with clarity and precision
  • Functional programming constructs for safer, more expressive code
  • Cross-cutting concerns handled through domain markers
  • Messaging infrastructure for implementing CQRS and event-driven architecture
  • Consistent error handling through the Result pattern

By adopting these patterns early in your development process, you can ensure better code maintainability, domain integrity, and system scalability.

Components

The CoreKernel library consists of the following components:

  • CoreKernel.Primitives: Domain-Driven Design building blocks (entities, value objects, aggregates)
  • CoreKernel.DomainMarkers: Interfaces for cross-cutting concerns (auditing, multi-tenancy, etc.)
  • CoreKernel.Functional: Functional programming abstractions (Result, Maybe, Error types)
  • CoreKernel.Messaging: CQRS and event-driven architecture support (commands, queries, events)

Each component can be used independently or together for a complete development experience.

Installation

To use the full CoreKernel library in your project:

dotnet add package CoreKernel

Or, to use individual components:

dotnet add package CoreKernel.Primitives
dotnet add package CoreKernel.DomainMarkers
dotnet add package CoreKernel.Functional
dotnet add package CoreKernel.Messaging

Usage

Domain-Driven Design Primitives

Entities

The Entity<TId> base class provides identity-based equality comparison:

publicclassProduct:Entity<Guid>{publicstringName{get;}publicdecimalPrice{get;privateset;}publicProduct(Guidid,stringname,decimalprice):base(id){Name=name;Price=price;}publicvoidUpdatePrice(decimalnewPrice){Price=newPrice;}}

Entities are compared by ID and type:

varproduct1=newProduct(Guid.NewGuid(),"Widget",10.99m);varproduct2=newProduct(product1.Id,"Widget",10.99m);boolareEqual=product1==product2;// true, same ID

Aggregate Roots

Aggregate roots are the entry points to domain aggregates and can raise domain events:

publicclassOrder:AggregateRoot<Guid>{privatereadonlyList<OrderLine>_orderLines=new();publicIReadOnlyCollection<OrderLine>OrderLines=>_orderLines.AsReadOnly();publicdecimalTotalAmount{get;privateset;}publicOrder(Guidid):base(id){TotalAmount=0;}publicvoidAddOrderLine(Productproduct,intquantity){varorderLine=newOrderLine(Guid.NewGuid(),product.Id,product.Price,quantity);_orderLines.Add(orderLine);TotalAmount+=orderLine.LineTotal;RaiseDomainEvent(newOrderLineAddedEvent(Id,orderLine.Id));}publicvoidPlace(){// Business logic for placing an orderRaiseDomainEvent(newOrderPlacedEvent(Id));}}

Working with domain events:

// Create and manipulate the ordervarorder=newOrder(Guid.NewGuid());order.AddOrderLine(product,2);order.Place();// Retrieve domain events for processingvarevents=order.GetDomainEvents();// Process events...order.ClearDomainEvents();// Clear after processing

Value Objects

Value objects encapsulate concepts that are distinguished by their attributes rather than identity:

publicclassMoney:ValueObject{publicdecimalAmount{get;}publicstringCurrency{get;}publicMoney(decimalamount,stringcurrency){Amount=amount;Currency=currency;}protectedoverrideIEnumerable<object>GetAtomicValues(){yieldreturnAmount;yieldreturnCurrency;}publicMoneyAdd(Moneyother){if(Currency!=other.Currency)thrownewInvalidOperationException("Cannot add different currencies");returnnewMoney(Amount+other.Amount,Currency);}}

Value objects are compared by their structure, not reference:

varprice1=newMoney(100,"USD");varprice2=newMoney(100,"USD");boolareEqual=price1==price2;// true, same values

Strongly-Typed IDs

Strongly-typed IDs prevent accidental ID misuse between different entity types:

publicclassUserId:StronglyTypedId<Guid>{publicUserId(Guidvalue):base(value){}}publicclassOrderId:StronglyTypedId<Guid>{publicOrderId(Guidvalue):base(value){}}publicclassUser:Entity<UserId>{publicstringName{get;}publicUser(UserIdid,stringname):base(id){Name=name;}}

This prevents accidentally passing an OrderId to a method expecting a UserId.

Domain Markers

Auditing

Auditing interfaces track entity creation and modification:

// Basic time trackingpublicclassBlogPost:Entity<Guid>,ITimeStamped{publicstringTitle{get;set;}publicstringContent{get;set;}// ITimeStamped implementationpublicDateTimeOffsetCreatedOn{get;set;}publicDateTimeOffsetLastModifiedOn{get;set;}}// Complete auditing with user trackingpublicclassInvoice:Entity<Guid>,IAuditable{publicdecimalAmount{get;set;}publicstringCustomerName{get;set;}// IAuditable implementationpublicDateTimeOffsetCreatedOn{get;set;}publicstringCreatedBy{get;set;}publicDateTimeOffsetLastModifiedOn{get;set;}publicstringLastModifiedBy{get;set;}}

Multi-Tenancy

The ITenantScoped<TId> interface enables multi-tenant data isolation:

publicclassCustomerRecord:Entity<Guid>,ITenantScoped<Guid>{publicstringCustomerName{get;set;}publicstringContactEmail{get;set;}// ITenantScoped implementationpublicGuidTenantId{get;set;}}

This allows for automatic tenant filtering in queries.

Soft Deletion

The ISoftDeletable interface supports logical deletion of entities:

publicclassDocument:Entity<Guid>,ISoftDeletable{publicstringTitle{get;set;}publicstringContent{get;set;}// ISoftDeletable implementationpublicboolIsDeleted{get;set;}publicDateTimeOffset?DeletedOn{get;set;}publicstring?DeletedBy{get;set;}}

Tracing

The ITraceable interface enables distributed tracing:

publicclassPayment:Entity<Guid>,ITraceable{publicdecimalAmount{get;set;}publicstringPaymentMethod{get;set;}// ITraceable implementationpublicGuidCorrelationId{get;set;}publicstring?TraceSource{get;set;}publicstring?OperationName{get;set;}}

Functional Programming

Result Pattern

The Result pattern provides a way to represent operation outcomes that might fail:

// Creating resultsResultsuccessResult=Result.Success();Result<int>successWithValue=Result.Success(42);ResultfailureResult=Result.Failure(Error.ValidationError);Result<string>failureWithCustomError=Result.Failure<string>(Error.Failure("User.NotFound","The specified user was not found."));// Pattern matchingstringmessage=userResult.Match(onSuccess: user =>$"User {user.Name} was found",onFailure: error =>$"Error: {error.Message}");// Transforming resultsResult<UserDto>userDtoResult=userResult.Map(user =>newUserDto(user));// Chaining operationsResult<OrderConfirmation>confirmationResult=ValidateOrder(request).Bind(order =>CalculateTotals(order)).Bind(order =>CheckInventory(order)).Bind(order =>ProcessPayment(order)).Map(order =>GenerateConfirmation(order));

Maybe Pattern

The Maybe pattern provides type-safe handling of optional values:

// Creating Maybe valuesMaybe<string>someName=Maybe<string>.Some("John");Maybe<string>noName=Maybe<string>.None;// Using Match for handling both casesstringgreeting=someName.Match(onSome: name =>$"Hello, {name}!",onNone:()=>"Hello, stranger!");// Transforming Maybe valuesMaybe<int>nameLength=someName.Map(name =>name.Length);// Converting to ResultResult<string>nameResult=someName.ToResult("Name is required");

Validation

Validator Overview

The Validator class provides methods to validate input against single or multiple rules, returning structured results. It integrates seamlessly with the Result and ValidationResult patterns.

Validating Single Rule
usingCoreKernel.Functional.Validation;// Validate input against a single rulevarresult=Validator.Validate(input:"example@example.com",validationRule: value =>value.Contains("@"),errorMessage:"Input must contain '@'.");if(result.IsSuccess){Console.WriteLine("Validation succeeded: "+result.Value);}else{Console.WriteLine("Validation failed: "+result.Error.Message);}
Validating Multiple Rules
usingCoreKernel.Functional.Validation;// Define validation rulesvarvalidationRules=newList<(Func<string,bool>rule,stringerrorMessage)>{(value =>!string.IsNullOrWhiteSpace(value),"Input cannot be empty."),(value =>value.Contains("@"),"Input must contain '@'."),(value =>value.Length<=50,"Input must not exceed 50 characters.")};// Validate input against multiple rulesvarresult=Validator.Validate("example@example.com",validationRules);if(result.IsSuccess){Console.WriteLine("Validation succeeded: "+result.Value);}else{Console.WriteLine("Validation failed with errors:");foreach(varerrorin((IValidationResult)result).Errors){Console.WriteLine($"- {error.Message}");}}

Validation types allow collecting multiple validation errors:

// Creating validation resultsValidationResultvalidationResult=ValidationResult.WithErrors(new[]{Error.Validation("User.Email.Invalid","Email address is not in a valid format"),Error.Validation("User.Password.TooShort","Password must be at least 8 characters")});// Using Match with ValidationResultstringmessage=userValidationResult.Match(onSuccess: user =>$"User created: {user.Email}",onError: errors =>$"Validation failed: {string.Join(", ",errors.Select(e =>e.Message))}");

Error Handling

The Error record type provides structured error representation:

// Predefined errorsErrornullValueError=Error.NullValue;ErrorvalidationError=Error.ValidationError;// Custom errorsErrorcustomError=Error.Failure("Order.Processing.Failed","Failed to process the order due to payment issue");// Adding detailsErrordetailedError=customError.WithDetails("Transaction ID: 1234567");

Messaging

Commands

Commands represent intentions to change system state:

// Command definitionpublicclassCreateProductCommand:ICommand<Guid>{publicstringName{get;init;}publicdecimalPrice{get;init;}}// Command handlerpublicclassCreateProductCommandHandler:ICommandHandler<CreateProductCommand,Guid>{publicasyncTask<Result<Guid>>Handle(CreateProductCommandcommand,CancellationTokencancellationToken){// Implementation to create a productvarproductId=Guid.NewGuid();returnResult.Success(productId);}}

Queries

Queries retrieve data without changing system state:

// Query definitionpublicclassGetProductByIdQuery:IQuery<ProductDto>{publicGuidId{get;init;}}// Query handlerpublicclassGetProductByIdQueryHandler:IQueryHandler<GetProductByIdQuery,ProductDto>{publicasyncTask<Result<ProductDto>>Handle(GetProductByIdQueryquery,CancellationTokencancellationToken){// Implementation to retrieve product datareturnResult.Success(newProductDto{/* ... */});}}

Events

Events represent notifications about something that has happened:

// Event definitionpublicclassUserRegisteredEvent:IEvent{publicGuidId{get;init;}publicDateTimeTimeStamp{get;init;}publicGuidCorrelationId{get;init;}publicstringUsername{get;init;}publicstringEmail{get;init;}}// Event handlerpublicclassSendWelcomeEmailHandler:IEventHandler<UserRegisteredEvent>{publicasyncTaskHandle(UserRegisteredEventnotification,CancellationTokencancellationToken){// Implementation to send welcome email}}

Domain Events

Domain events are specialized events raised within the domain model:

// Domain event definitionpublicclassOrderPlacedEvent:IDomainEvent{publicGuidId{get;init;}publicDateTimeTimeStamp{get;init;}publicGuidCorrelationId{get;init;}publicGuidOrderId{get;init;}}// Domain event handlerpublicclassUpdateInventoryHandler:IDomainEventHandler<OrderPlacedEvent>{publicasyncTaskHandle(OrderPlacedEventnotification,CancellationTokencancellationToken){// Implementation to update inventory}}

Integration Examples

Repository with Domain Markers

publicclassGenericRepository<TEntity,TId>:IRepository<TEntity,TId>whereTEntity:Entity<TId>whereTId:notnull{privatereadonlyDbContext_dbContext;privatereadonlyICurrentUserService_currentUser;privatereadonlyITenantProvider_tenantProvider;publicasyncTask<Result<TEntity>>GetByIdAsync(TIdid){varentity=await_dbContext.Set<TEntity>().FindAsync(id);returnentity!=null?Result.Success(entity):Result.Failure<TEntity>(Error.NotFound($"{typeof(TEntity).Name} with ID {id} was not found."));}publicasyncTask<Result<TId>>SaveAsync(TEntityentity){// Handle audit informationif(entityisIAuditableauditable){varnow=DateTimeOffset.UtcNow;varuserId=_currentUser.UserId??"system";if(IsNew(entity)){auditable.CreatedOn=now;auditable.CreatedBy=userId;}auditable.LastModifiedOn=now;auditable.LastModifiedBy=userId;}// Handle multi-tenancyif(entityisITenantScoped<Guid>tenantScoped&&IsNew(entity)){tenantScoped.TenantId=_tenantProvider.GetCurrentTenantId();}// Handle tracing if applicableif(entityisITraceabletraceable){traceable.CorrelationId=_currentUser.CorrelationId;traceable.TraceSource=GetType().Name;traceable.OperationName=IsNew(entity)?"Create":"Update";}try{if(IsNew(entity))_dbContext.Add(entity);await_dbContext.SaveChangesAsync();returnResult.Success(entity.Id);}catch(Exceptionex){returnResult.Failure<TId>(Error.Failure("Database.SaveFailed",$"Failed to save {typeof(TEntity).Name}: {ex.Message}"));}}publicasyncTask<Result>DeleteAsync(TEntityentity){if(entityisISoftDeletablesoftDeletable){softDeletable.IsDeleted=true;softDeletable.DeletedOn=DateTimeOffset.UtcNow;softDeletable.DeletedBy=_currentUser.UserId??"system";await_dbContext.SaveChangesAsync();returnResult.Success();}_dbContext.Remove(entity);await_dbContext.SaveChangesAsync();returnResult.Success();}privateboolIsNew(TEntityentity){return!_dbContext.Entry(entity).IsKeySet;}}

ASP.NET Core Controller with CQRS

[ApiController][Route("api/[controller]")]publicclassProductsController:ControllerBase{privatereadonlyIMediator_mediator;publicProductsController(IMediatormediator){_mediator=mediator;}[HttpGet("{id}")]publicasyncTask<IActionResult>GetProduct(Guidid){varquery=newGetProductByIdQuery{Id=id};varresult=await_mediator.Send(query);returnresult.Match(onSuccess: product =>Ok(product),onFailure: error =>error.Typeswitch{ErrorType.NotFound=>NotFound(error.Message),ErrorType.Unauthorized=>Unauthorized(error.Message),
_ =>BadRequest(error.Message)});}[HttpPost]publicasyncTask<IActionResult>CreateProduct(CreateProductRequestrequest){varcommand=newCreateProductCommand{Name=request.Name,Price=request.Price};varresult=await_mediator.Send(command);returnresult.Match(onSuccess: id =>CreatedAtAction(nameof(GetProduct),new{id},null),onFailure: error =>BadRequest(error.Message));}}

Domain Event Handling Pipeline

publicclassDomainEventDispatcher:IDomainEventDispatcher{privatereadonlyIMediator_mediator;privatereadonlyILogger<DomainEventDispatcher>_logger;publicDomainEventDispatcher(IMediatormediator,ILogger<DomainEventDispatcher>logger){_mediator=mediator;_logger=logger;}publicasyncTaskDispatchEventsAsync(IEnumerable<IDomainEvent>domainEvents){foreach(vardomainEventindomainEvents){_logger.LogInformation("Dispatching domain event {EventType} with ID {EventId}",domainEvent.GetType().Name,domainEvent.Id);await_mediator.Publish(domainEvent);}}}// Usage in a command handlerpublicclassPlaceOrderCommandHandler:ICommandHandler<PlaceOrderCommand,Guid>{privatereadonlyIRepository<Order,Guid>_orderRepository;privatereadonlyIDomainEventDispatcher_eventDispatcher;publicasyncTask<Result<Guid>>Handle(PlaceOrderCommandcommand,CancellationTokencancellationToken){// Create and place ordervarorder=newOrder(Guid.NewGuid());foreach(varitemincommand.Items){order.AddOrderLine(item.ProductId,item.Quantity);}order.Place();// Save order and dispatch eventsvarsaveResult=await_orderRepository.SaveAsync(order);if(saveResult.IsSuccess){await_eventDispatcher.DispatchEventsAsync(order.GetDomainEvents());order.ClearDomainEvents();}returnsaveResult;}}

Best Practices

DDD Best Practices

  1. Use Value Objects for concepts defined by their attributes (Money, Address, PhoneNumber)
  2. Create meaningful Aggregates with clear boundaries and invariants
  3. Keep Entities focused on domain behavior rather than data persistence
  4. Use strongly-typed IDs to prevent mixing identifier types

Functional Programming Best Practices

  1. Prefer Result<T> over exceptions for expected error cases
  2. Use Maybe<T> instead of null for optional values
  3. Chain operations with Bind and Map instead of using nested conditionals
  4. Use Match for exhaustive handling of all possible states

Domain Markers Best Practices

  1. Combine Domain Markers when an entity needs multiple cross-cutting concerns
  2. Centralize implementation logic in repositories or middleware
  3. Consider performance impacts of global filters for soft deletion and multi-tenancy
  4. Apply markers consistently across related entities

Messaging Best Practices

  1. Commands should represent intent and map to a single use case
  2. Queries should be idempotent and not modify state
  3. Use domain events for cross-aggregate communication
  4. Include correlation IDs for traceability across system boundaries

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

This project is licensed under the Apache License 2.0. See the LICENSE file for details.

About

The CoreKernel library provides a set of foundational abstractions and utilities for building robust, modular, and maintainable applications. It includes support for functional programming constructs, messaging patterns, error handling, and domain-driven design (DDD) principles.

Topics

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages