IronAlpine.Framework.Kernel 1.0.2

There is a newer version of this package available.
See the version list below for details.
dotnet add package IronAlpine.Framework.Kernel --version 1.0.2
                    
NuGet\Install-Package IronAlpine.Framework.Kernel -Version 1.0.2
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="IronAlpine.Framework.Kernel" Version="1.0.2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="IronAlpine.Framework.Kernel" Version="1.0.2" />
                    
Directory.Packages.props
<PackageReference Include="IronAlpine.Framework.Kernel" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add IronAlpine.Framework.Kernel --version 1.0.2
                    
#r "nuget: IronAlpine.Framework.Kernel, 1.0.2"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package IronAlpine.Framework.Kernel@1.0.2
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=IronAlpine.Framework.Kernel&version=1.0.2
                    
Install as a Cake Addin
#tool nuget:?package=IronAlpine.Framework.Kernel&version=1.0.2
                    
Install as a Cake Tool

IronAlpine.Kernel v4.0.0

Enterprise-grade core domain primitives for DDD microservices — zero external dependencies, 100% source protected.

Overview

IronAlpine.Kernel is the foundational layer for all IronAlpine-based systems. It provides battle-tested domain primitives used across distributed microservices. Zero external dependencies means you can use Kernel in any project without adding transitive dependencies.

Why IronAlpine.Kernel?

  • ✅ Zero dependencies — use in UI, API, CLI, libraries without dependency bloat
  • ✅ Proven patterns — Entity, AggregateRoot, ValueObject, DomainEvent
  • ✅ Type safety — Result<T> pattern eliminates null reference exceptions
  • ✅ Testable — all types are mockable, no static methods
  • ✅ Source protected — all implementations internal, interfaces public
  • ✅ Performance — minimal overhead, no reflection, no serialization
  • ✅ Multi-framework — net9.0 & net10.0 support

Installation

dotnet add package IronAlpine.Kernel

Core Types Reference

1. Entity & Identity

What it is: Base class for objects with persistent identity (not value-based equality)

// Domain layer
public class TimeOff : Entity
{
    public TimeOffId Id { get; private set; }
    public UserId RequestedBy { get; private set; }
    public DateTime StartDate { get; private set; }
    public DateTime EndDate { get; private set; }
    public TimeOffStatus Status { get; private set; }

    // Constructor for creating new instance
    public static TimeOff Request(TimeOffId id, UserId userId, DateTime startDate, DateTime endDate)
    {
        if (startDate >= endDate)
            throw new DomainException("StartDate must be before EndDate");

        return new TimeOff
        {
            Id = id,
            RequestedBy = userId,
            StartDate = startDate,
            EndDate = endDate,
            Status = TimeOffStatus.Pending
        };
    }

    // Domain methods that change state
    public void Approve(UserId approver, string comment)
    {
        if (Status != TimeOffStatus.Pending)
            throw new DomainException($"Cannot approve {Status} request");

        Status = TimeOffStatus.Approved;
        // More logic...
    }

    public void Reject(UserId rejector, string reason)
    {
        if (Status != TimeOffStatus.Pending)
            throw new DomainException("Can only reject pending requests");

        Status = TimeOffStatus.Rejected;
        // More logic...
    }
}

// Identity value object
public class TimeOffId : ValueObject
{
    public Guid Value { get; }

    public TimeOffId(Guid value)
    {
        if (value == Guid.Empty)
            throw new ArgumentException("TimeOffId cannot be empty");
        Value = value;
    }

    protected override IEnumerable<object> GetAtomicValues()
    {
        yield return Value;
    }

    public override string ToString() => Value.ToString();
}

Key characteristics:

  • Identity-based equality (two entities with same Id are equal)
  • Can have mutable state (unlike value objects)
  • Lifecycle: created, modified, deleted
  • Used for business logic concentration

When to use:

  • Objects with persistent identity (User, Order, TimeOff)
  • Objects with changing state (Status, Amount, Name)
  • Objects that exist in a repository

When NOT to use:

  • Immutable data (use ValueObject instead)
  • Data without identity (use records/DTOs)

2. AggregateRoot

What it is: Special Entity that is a consistency boundary (contains related entities)

// An aggregate root owns related entities
public class Order : AggregateRoot
{
    private readonly List<OrderLine> _lines = new();

    public OrderId Id { get; private set; }
    public CustomerId CustomerId { get; private set; }
    public OrderStatus Status { get; private set; }
    public Money Total { get; private set; }
    public IReadOnlyList<OrderLine> Lines => _lines.AsReadOnly();

    // IMPORTANT: Private setters, exposed through methods only
    // This prevents bypassing business logic

    public static Order CreateForCustomer(OrderId id, CustomerId customerId)
    {
        var order = new Order
        {
            Id = id,
            CustomerId = customerId,
            Status = OrderStatus.Created,
            Total = Money.Zero
        };

        // Publish domain event when aggregate is created
        order.AddDomainEvent(new OrderCreatedEvent(id, customerId));

        return order;
    }

    // Business methods on aggregate
    public void AddLine(OrderLine line)
    {
        if (Status != OrderStatus.Created && Status != OrderStatus.Pending)
            throw new DomainException("Cannot add lines to finalized order");

        _lines.Add(line);
        RecalculateTotal();

        // Publish event
        this.AddDomainEvent(new LineAddedToOrderEvent(this.Id, line.ProductId, line.Quantity));
    }

    public void ConfirmOrder()
    {
        if (_lines.Count == 0)
            throw new DomainException("Order must have at least one line");

        Status = OrderStatus.Confirmed;
        this.AddDomainEvent(new OrderConfirmedEvent(this.Id, this.Total));
    }

    public void CancelOrder(string reason)
    {
        if (Status == OrderStatus.Cancelled || Status == OrderStatus.Shipped)
            throw new DomainException("Cannot cancel finalized order");

        Status = OrderStatus.Cancelled;
        this.AddDomainEvent(new OrderCancelledEvent(this.Id, reason));
    }

    private void RecalculateTotal()
    {
        Total = _lines.Aggregate(Money.Zero, (acc, line) => acc + line.Total);
    }

    // Related entity (part of aggregate, never exposed to external callers)
    public class OrderLine : Entity
    {
        public OrderLineId Id { get; private set; }
        public ProductId ProductId { get; private set; }
        public int Quantity { get; private set; }
        public Money UnitPrice { get; private set; }
        public Money Total => UnitPrice.MultiplyBy(Quantity);

        public void UpdateQuantity(int newQuantity)
        {
            if (newQuantity <= 0)
                throw new DomainException("Quantity must be greater than 0");
            Quantity = newQuantity;
        }
    }
}

Key characteristics:

  • Root entity that owns a cluster of related entities
  • Enforces invariants for entire cluster
  • External callers reference only the root
  • Internal entities not exposed (List<OrderLine> is private, accessed via methods)
  • Publishes domain events on state changes

Consistency boundaries example:

Order (Aggregate Root)
├── OrderLine (internal entity)
├── OrderLine (internal entity)
└── OrderLine (internal entity)

// Valid: reference Order
var order = repository.GetById(orderId);

// WRONG: never do this (orderId cannot reference OrderLine)
var line = repository.GetById(lineId);  // ❌ Breaks aggregate boundary

Rules for aggregates:

  1. ✅ Reference the root only
  2. ✅ Keep aggregates small (5-20 entities max)
  3. ✅ Communicate between aggregates via events
  4. ✅ One transaction per aggregate operation
  5. ❌ Don't fetch sub-entities independently
  6. ❌ Don't expose child collections directly

3. ValueObject

What it is: Immutable object compared by all its properties (not identity)

// ValueObject is compared by VALUE, not ID
public class Money : ValueObject
{
    public decimal Amount { get; }
    public Currency Currency { get; }

    public Money(decimal amount, Currency currency)
    {
        if (amount < 0)
            throw new ArgumentException("Amount cannot be negative");

        Amount = amount;
        Currency = currency;
    }

    public Money Add(Money other)
    {
        if (other.Currency != this.Currency)
            throw new DomainException($"Cannot add {other.Currency} to {this.Currency}");

        return new Money(this.Amount + other.Amount, this.Currency);
    }

    public Money Multiply(decimal factor)
    {
        if (factor < 0)
            throw new DomainException("Factor must be non-negative");

        return new Money(this.Amount * factor, this.Currency);
    }

    // CRITICAL: Implement GetAtomicValues for equality
    protected override IEnumerable<object> GetAtomicValues()
    {
        yield return Amount;
        yield return Currency;
    }

    public override string ToString() => $"{Amount:C} {Currency}";

    // Static factory methods for common values
    public static Money Zero => new(0, Currency.USD);
    public static Money FromUSD(decimal amount) => new(amount, Currency.USD);
}

public enum Currency
{
    USD = 1,
    EUR = 2,
    GBP = 3,
    TRY = 4
}

// Usage example
var price1 = new Money(100, Currency.USD);
var price2 = new Money(100, Currency.USD);

// TRUE: Same value means equality
Assert.AreEqual(price1, price2);  // ✓

// TRUE: Creating a new instance still equals the same value
var price3 = new Money(100, Currency.USD);
Assert.AreEqual(price1, price3);  // ✓

// Different currencies are NOT equal
var price4 = new Money(100, Currency.EUR);
Assert.AreNotEqual(price1, price4);  // ✓

Characteristics:

  • Immutable (properties readonly after construction)
  • Compared by value (100 USD = 100 USD always)
  • No identity (no ID needed)
  • Reusable across aggregates
  • Type-safe compared to primitives

ValueObject best practices:

// ✅ GOOD: Type-safe, business meaning
public Money UnitPrice { get; }

// ❌ BAD: Loose typing, unclear meaning
public decimal UnitPrice { get; }
// ✅ GOOD: Immutable, predictable
var newPrice = price.Multiply(1.1);

// ❌ BAD: Mutation, hard to track
price.Amount *= 1.1;

4. IDomainEvent

What it is: Immutable record of something important that happened in the domain

// Domain event - records WHAT happened
public class OrderConfirmedEvent : IDomainEvent
{
    public OrderId OrderId { get; init; }
    public Money TotalAmount { get; init; }
    public UserId ConfirmedBy { get; init; }
    public DateTime ConfirmedAt { get; init; }

    public OrderConfirmedEvent(OrderId orderId, Money totalAmount, UserId confirmedBy = null)
    {
        OrderId = orderId ?? throw new ArgumentNullException(nameof(orderId));
        TotalAmount = totalAmount ?? throw new ArgumentNullException(nameof(totalAmount));
        ConfirmedBy = confirmedBy;
        ConfirmedAt = DateTime.UtcNow;
    }
}

// Another event
public class OrderShippedEvent : IDomainEvent
{
    public OrderId OrderId { get; init; }
    public string TrackingNumber { get; init; }
    public DateTime ShippedAt { get; init; }
}

// Aggregate publishes events
public class Order : AggregateRoot
{
    public void Confirm()
    {
        if (Status != OrderStatus.Pending)
            throw new DomainException("Can only confirm pending orders");

        Status = OrderStatus.Confirmed;

        // Publish domain event - notify other aggregates/services
        this.AddDomainEvent(new OrderConfirmedEvent(this.Id, this.Total));
    }

    public void Ship(string trackingNumber)
    {
        Status = OrderStatus.Shipped;
        this.AddDomainEvent(new OrderShippedEvent(this.Id, trackingNumber));
    }
}

// Accessing domain events
var order = await repository.GetByIdAsync(orderId);
var events = order.DomainEvents;  // Get published events

// Events are persisted & published to message bus (Kafka, RabbitMQ)
// Other services subscribe:
//   - Notification service sends email when order confirmed
//   - Fulfillment service prepares shipment
//   - Accounting service records revenue
//   - Analytics service tracks order metrics

Event naming convention:

  • ✅ Past tense: OrderConfirmed, PaymentProcessed, UserRegistered
  • ✅ Business terminology: TimeOffApproved, InventoryReserved
  • ❌ Technical: EntityChanged, DataUpdated

Domain events solve:

  • ✅ Loose coupling between services
  • ✅ Audit trail (what happened and when)
  • ✅ Side effects (send email, update cache, etc.)
  • ✅ Event sourcing patterns
  • ✅ Saga patterns for distributed transactions

5. Guard Clauses

What it is: Defensive programming helpers for preconditions

// Guard usage in domain methods
public class TimeOff : AggregateRoot
{
    public void Request(UserId requestedBy, DateTime startDate, DateTime endDate)
    {
        // Guard against null/invalid inputs
        Guard.Against.Null(requestedBy, nameof(requestedBy));
        Guard.Against.InvalidData(startDate < DateTime.Today, "Start date must be in future");
        Guard.Against.InvalidData(startDate >= endDate, "Start must be before end");
        Guard.Against.InvalidData((endDate - startDate).Days > 30, "Cannot request more than 30 days");

        RequestedBy = requestedBy;
        StartDate = startDate;
        EndDate = endDate;
        Status = TimeOffStatus.Pending;
    }

    public void Approve(UserId approver, string comment)
    {
        Guard.Against.Null(approver, nameof(approver));
        Guard.Against.InvalidState(Status != TimeOffStatus.Pending, "Can only approve pending requests");
        Guard.Against.NullOrEmpty(comment, nameof(comment));
        Guard.Against.InvalidData(comment.Length > 500, "Comment too long");

        Status = TimeOffStatus.Approved;
        ApprovedBy = approver;
        ApprovedAt = DateTime.UtcNow;
        Comment = comment;
    }
}

Guard methods available:

  • Guard.Against.Null(value, paramName) — throw if null
  • Guard.Against.NullOrEmpty(string) — throw if null/empty
  • Guard.Against.InvalidData(condition, message) — throw if condition true
  • Guard.Against.InvalidState(condition, message) — throw if invariant violated
  • Guard.Against.OutOfRange(value, min, max) — throw if out of range

6. Result<T> Pattern

What it is: Railway-oriented programming - explicit success/failure path

// Instead of throwing exceptions, return Result
public class TimeOffService
{
    public Result<TimeOff> RequestTimeOff(UserId userId, DateTime startDate, DateTime endDate)
    {
        // Validation failures return Result.Failure
        if (startDate < DateTime.Today)
            return Result.Failure<TimeOff>("Start date must be in future");

        if (startDate >= endDate)
            return Result.Failure<TimeOff>("Start date must be before end date");

        // Business rule checks
        var existingRequests = _repository.FindByUserAndDatesAsync(userId, startDate, endDate);
        if (existingRequests.Any())
            return Result.Failure<TimeOff>("Overlap with existing request");

        // Success case returns Result.Success
        var timeOff = TimeOff.Request(TimeOffId.New(), userId, startDate, endDate);
        return Result.Success(timeOff);
    }
}

// Usage in controller
[HttpPost("request")]
public async Task<IActionResult> Request(RequestTimeOffDto dto)
{
    var result = _service.RequestTimeOff(
        new UserId(dto.UserId),
        dto.StartDate,
        dto.EndDate);

    // Explicit success/failure handling
    if (result.IsSuccess)
    {
        await _repository.AddAsync(result.Value);
        return CreatedAtAction(nameof(Get), new { id = result.Value.Id });
    }
    else
    {
        return BadRequest(new { error = result.Error });
    }
}

// Can chain operations
var result = _service.ValidateRequest(dto)
    .Then(validated => _service.ProcessRequest(validated))
    .Then(processed => _repository.SaveAsync(processed))
    .Tap(saved => _logger.LogInformation("Request saved: {Id}", saved.Id));

if (result.IsSuccess)
    return Ok(result.Value);
else
    return BadRequest(result.Error);

Result benefits:

  • ✅ No exception overhead for business failures
  • ✅ Explicit success/failure types
  • ✅ Composable pipelines
  • ✅ Clear intent (not an error if validation fails)
  • ✅ Better performance (no stack trace)

7. Specification Pattern

What it is: Encapsulates query logic in reusable, composable specifications

// Define specification for complex queries
public class TimeOffPendingApprovalSpec : Specification<TimeOff>
{
    public TimeOffPendingApprovalSpec(UserId approverUserId)
    {
        // Define query
        Query
            .Where(t => t.Status == TimeOffStatus.Pending)
            .Where(t => t.ApproverId == approverUserId)
            .OrderByDescending(t => t.RequestedAt)
            .Take(50);

        // Include related data
        Query.Include(t => t.Requester)
             .Include(t => t.Approver);
    }
}

// Use in repository
public class TimeOffRepository : Repository<TimeOff>
{
    public async Task<IEnumerable<TimeOff>> GetPendingForApprovalAsync(
        UserId approverId,
        CancellationToken ct)
    {
        var spec = new TimeOffPendingApprovalSpec(approverId);
        return await ListAsync(spec, ct);
    }
}

// Reuse across services
var pendingForMgr = await timeOffRepo.GetPendingForApprovalAsync(managerUserId, ct);
var pendingForHR = await timeOffRepo.GetPendingForApprovalAsync(hrUserId, ct);

// Create complex specifications
public class TimeOffReportSpec : Specification<TimeOff>
{
    public TimeOffReportSpec(int year, int month)
    {
        Query
            .Where(t => t.StartDate.Year == year && t.StartDate.Month == month)
            .Where(t => t.Status == TimeOffStatus.Approved)
            .OrderBy(t => t.RequestedBy)
            .ThenBy(t => t.StartDate);

        Query.Include(t => t.Requester);
    }
}

Architecture Patterns

Domain-Driven Design (DDD)

┌─────────────────────────────────────────────────┐
│           Domain Layer (IronAlpine.Kernel)      │
│  ┌───────────────────────────────────────────┐  │
│  │ Aggregates: Order, TimeOff, User          │  │
│  │ Entities: OrderLine, TimeOffHistory       │  │
│  │ ValueObjects: Money, DateRange, UserId    │  │
│  │ Domain Events: OrderConfirmed, Approved   │  │
│  │ Domain Services: PricingService, Rules    │  │
│  └───────────────────────────────────────────┘  │
│                     ↑                             │
│  No dependencies on infrastructure               │
└─────────────────────────────────────────────────┘
        ↑                                  ↑
        │                                  │
   ┌────────────────┐          ┌──────────────────┐
   │  Application   │          │ Infrastructure   │
   │  (Mediator)    │          │ (Data, EventBus) │
   └────────────────┘          └──────────────────┘

Complete Examples

Example 1: TimeOff Domain

// src/Domain/TimeOff/TimeOffId.cs
public class TimeOffId : ValueObject
{
    public Guid Value { get; }

    public TimeOffId(Guid value)
    {
        Guard.Against.InvalidData(value == Guid.Empty, "TimeOffId cannot be empty");
        Value = value;
    }

    protected override IEnumerable<object> GetAtomicValues()
    {
        yield return Value;
    }

    public static TimeOffId New() => new(Guid.NewGuid());
}

// src/Domain/TimeOff/TimeOffStatus.cs
public enum TimeOffStatus
{
    Pending = 1,
    Approved = 2,
    Rejected = 3,
    Cancelled = 4,
    Completed = 5
}

// src/Domain/TimeOff/TimeOff.cs
public class TimeOff : AggregateRoot
{
    public TimeOffId Id { get; private set; }
    public UserId RequestedBy { get; private set; }
    public DateTime StartDate { get; private set; }
    public DateTime EndDate { get; private set; }
    public TimeOffStatus Status { get; private set; }
    public UserId ApprovedBy { get; private set; }
    public string ApprovalComment { get; private set; }
    public DateTime RequestedAt { get; private set; }

    // Factory method
    public static TimeOff Request(TimeOffId id, UserId userId, DateTime startDate, DateTime endDate)
    {
        Guard.Against.Null(id, nameof(id));
        Guard.Against.Null(userId, nameof(userId));
        Guard.Against.InvalidData(startDate < DateTime.Today, "Start date must be in future");
        Guard.Against.InvalidData(startDate >= endDate, "Start must be before end");
        Guard.Against.InvalidData((endDate - startDate).Days > 30, "Max 30 days");

        var timeOff = new TimeOff
        {
            Id = id,
            RequestedBy = userId,
            StartDate = startDate,
            EndDate = endDate,
            Status = TimeOffStatus.Pending,
            RequestedAt = DateTime.UtcNow
        };

        timeOff.AddDomainEvent(new TimeOffRequestedEvent(
            id.Value,
            userId.Value,
            startDate,
            endDate));

        return timeOff;
    }

    // Domain method
    public void Approve(UserId approver, string comment)
    {
        Guard.Against.Null(approver, nameof(approver));
        Guard.Against.NullOrEmpty(comment, nameof(comment));
        Guard.Against.InvalidState(Status != TimeOffStatus.Pending, "Only pending requests can be approved");
        Guard.Against.InvalidData(comment.Length > 500, "Comment too long");

        ApprovedBy = approver;
        ApprovalComment = comment;
        Status = TimeOffStatus.Approved;

        this.AddDomainEvent(new TimeOffApprovedEvent(this.Id.Value, approver.Value, comment));
    }

    public void Reject(UserId rejector, string reason)
    {
        Guard.Against.Null(rejector, nameof(rejector));
        Guard.Against.NullOrEmpty(reason, nameof(reason));
        Guard.Against.InvalidState(Status != TimeOffStatus.Pending, "Only pending requests can be rejected");

        Status = TimeOffStatus.Rejected;
        ApprovedBy = rejector;
        ApprovalComment = reason;

        this.AddDomainEvent(new TimeOffRejectedEvent(this.Id.Value, rejector.Value, reason));
    }
}

// Domain events
public class TimeOffRequestedEvent : IDomainEvent
{
    public Guid TimeOffId { get; init; }
    public Guid UserId { get; init; }
    public DateTime StartDate { get; init; }
    public DateTime EndDate { get; init; }

    public TimeOffRequestedEvent(Guid timeOffId, Guid userId, DateTime start, DateTime end)
    {
        TimeOffId = timeOffId;
        UserId = userId;
        StartDate = start;
        EndDate = end;
    }
}

public class TimeOffApprovedEvent : IDomainEvent
{
    public Guid TimeOffId { get; init; }
    public Guid ApprovedBy { get; init; }
    public string Comment { get; init; }

    public TimeOffApprovedEvent(Guid timeOffId, Guid approvedBy, string comment)
    {
        TimeOffId = timeOffId;
        ApprovedBy = approvedBy;
        Comment = comment;
    }
}

Best Practices

✅ DO

// 1. Use Entities for objects with identity
public class User : Entity { public UserId Id { get; } }

// 2. Use ValueObjects for business concepts
public class EmailAddress : ValueObject { }

// 3. Publish events from aggregates
public void Approve() { AddDomainEvent(new ApprovedEvent(...)); }

// 4. Use Guard for preconditions
Guard.Against.Null(value, nameof(value));

// 5. Keep aggregates small and focused
public class Order : AggregateRoot { /* ~100-200 lines */ }

// 6. Factory methods for creation
public static Order Create(...) { }

// 7. Use private setters
public UserId RequestedBy { get; private set; }

❌ DON'T

// 1. Don't expose internal collections
public List<OrderLine> Lines { get; set; }  // ❌

// 2. Don't mutate from outside
order.Status = OrderStatus.Approved;  // ❌

// 3. Don't put business logic in DTOs
public class OrderDto { public void Approve() { } }  // ❌

// 4. Don't use entities as DTOs
public class Order : Entity { /* returned to API */ }  // ❌

// 5. Don't forget domain events
var order = Create(...);  // No events published ❌

// 6. Don't use primitives
public decimal Amount { get; }  // Use Money instead

// 7. Don't mix domains
public User { public Order LastOrder { get; } }  // Coupling

Testing

[TestClass]
public class TimeOffTests
{
    [TestMethod]
    public void Request_ValidData_CreatesTimeOff()
    {
        // Arrange
        var id = TimeOffId.New();
        var userId = new UserId(Guid.NewGuid());
        var startDate = DateTime.Today.AddDays(7);
        var endDate = startDate.AddDays(5);

        // Act
        var timeOff = TimeOff.Request(id, userId, startDate, endDate);

        // Assert
        Assert.AreEqual(TimeOffStatus.Pending, timeOff.Status);
        Assert.AreEqual(userId, timeOff.RequestedBy);
        Assert.AreEqual(1, timeOff.DomainEvents.Count);
        Assert.IsInstanceOfType(timeOff.DomainEvents[0], typeof(TimeOffRequestedEvent));
    }

    [TestMethod]
    [ExpectedException(typeof(DomainException))]
    public void Request_InvalidDates_Throws()
    {
        var id = TimeOffId.New();
        var userId = new UserId(Guid.NewGuid());

        // Should fail: start >= end
        TimeOff.Request(id, userId, DateTime.Today, DateTime.Today);
    }

    [TestMethod]
    public void Approve_PendingStatus_Approves()
    {
        // Arrange
        var timeOff = TimeOff.Request(TimeOffId.New(), new UserId(Guid.NewGuid()),
            DateTime.Today.AddDays(7), DateTime.Today.AddDays(10));
        var approverId = new UserId(Guid.NewGuid());

        // Act
        timeOff.Approve(approverId, "Approved");

        // Assert
        Assert.AreEqual(TimeOffStatus.Approved, timeOff.Status);
        Assert.AreEqual(approverId, timeOff.ApprovedBy);
        Assert.AreEqual(2, timeOff.DomainEvents.Count);
    }
}

Performance Considerations

  • Zero allocation for value objects: ValueObject comparison is fast (no reference equality)
  • Domain events are in-memory: No I/O until SaveChangesAsync
  • Entity identity is Guid: No database lookup for equality
  • No reflection: All types are AOT-friendly
  • Minimal overhead: Guard clauses compiled to IL efficiently

Integration with Other IronAlpine Packages

Package Integration Purpose
Mediator Handlers work with domain types Commands/queries use Entity, ValueObject
Data Repository<Entity> Persistence of aggregates
EventBus IDomainEvent published to Kafka Event-driven architecture
Observability Logging domain state Audit trails of domain changes
Security IAuditUser provides context Who changed what

Troubleshooting

Q: How do I compare entities? A: Entities use identity-based equality. user1.Id == user2.Id ✓

Q: Can ValueObjects be null? A: No, ValueObjects must be non-null. Use Money? amount for optional values.

Q: How do I update an entity? A: Through domain methods only: entity.Method(), never direct property assignment.

Q: Where do I put business logic? A: In domain types (entities, aggregates), not in services or controllers.

Version History

Version Changes
4.0.0 Current release
3.0.0 Initial v3 release
2.x v2 packages (deprecated)

Made with ❤️ for DDD-based microservices
MIT License | https://github.com/KerimDemirer/IronAlpine

Product Compatible and additional computed target framework versions.
.NET net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net10.0

    • No dependencies.
  • net9.0

    • No dependencies.

NuGet packages (7)

Showing the top 5 NuGet packages that depend on IronAlpine.Framework.Kernel:

Package Downloads
IronAlpine.Framework.Mediator

High-performance mediator runtime with request/response, notifications, streaming, and configurable behavior pipeline. Includes validation (FluentValidation) support. Caching behavior lives in IronAlpine.Framework.Caching.

IronAlpine.Framework.Data

EFCore data stack for IronAlpine: repositories, interceptors (audit, soft-delete, versioning), transaction and auditing behaviors, and domain event dispatching. Unified entry point: AddIronAlpineData<TDbContext>. Repository base classes remain public extension points for custom repositories.

IronAlpine.Framework.Web

ASP.NET Core defaults, Swagger, health checks, correlation middleware, and YARP gateway transform for IronAlpine microservices. Single entry point: AddIronAlpineWebDefaults + UseIronAlpineWebDefaults. All implementations internal.

IronAlpine.Framework.EventBus

Kafka-backed event bus with outbox/inbox pattern, dead letter support, and replay functionality. Unified namespace: IronAlpine.EventBus. Kafka implementation is internal; only contracts and topology builder are public. SSL cert parameters fully configurable (SslCaLocation, SslCertLocation, SslKeyLocation, SslKeyPassword).

IronAlpine.Framework.Security

JWT authentication, claim-based authorization, and EFCore permission store for IronAlpine microservices. Provides ICurrentUser, permission policy provider, security builder, and public authorization store contracts.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
3.1.1 293 10/21/2026
3.0.9 273 10/15/2026
3.0.8 175 10/13/2026
3.0.7 155 10/13/2026
3.0.6 166 10/9/2026
3.0.5 242 10/5/2026
3.0.4 258 10/2/2026
3.0.3 235 9/30/2026
3.0.2 1,139 7/22/2026
3.0.1 201 7/22/2026
3.0.0 417 7/20/2026
2.0.3 219 7/20/2026
2.0.2 253 7/13/2026
2.0.1 258 7/13/2026
2.0.0 237 7/13/2026
2.0.0-preview.2 103 7/13/2026
2.0.0-preview.1 108 7/6/2026
1.0.2 212 7/3/2026
1.0.1 199 7/3/2026
Loading failed

Stable mediator release with request/response, notification publish strategies, streaming, and dependency injection integration.