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.
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.
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.
To use the full CoreKernel library in your project:
dotnet add package CoreKernelOr, to use individual components:
dotnet add package CoreKernel.Primitives
dotnet add package CoreKernel.DomainMarkers
dotnet add package CoreKernel.Functional
dotnet add package CoreKernel.MessagingThe 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 IDAggregate 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 processingValue 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 valuesStrongly-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.
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;}}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.
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;}}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;}}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));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");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.
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);}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))}");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");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 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 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 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}}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;}}[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));}}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;}}- Use Value Objects for concepts defined by their attributes (Money, Address, PhoneNumber)
- Create meaningful Aggregates with clear boundaries and invariants
- Keep Entities focused on domain behavior rather than data persistence
- Use strongly-typed IDs to prevent mixing identifier types
- Prefer
Result<T>over exceptions for expected error cases - Use
Maybe<T>instead of null for optional values - Chain operations with
BindandMapinstead of using nested conditionals - Use
Matchfor exhaustive handling of all possible states
- Combine Domain Markers when an entity needs multiple cross-cutting concerns
- Centralize implementation logic in repositories or middleware
- Consider performance impacts of global filters for soft deletion and multi-tenancy
- Apply markers consistently across related entities
- Commands should represent intent and map to a single use case
- Queries should be idempotent and not modify state
- Use domain events for cross-aggregate communication
- Include correlation IDs for traceability across system boundaries
Contributions are welcome! Please feel free to submit a Pull Request.
This project is licensed under the Apache License 2.0. See the LICENSE file for details.
