Zooper.Lion 2.1.0

dotnet add package Zooper.Lion --version 2.1.0
                    
NuGet\Install-Package Zooper.Lion -Version 2.1.0
                    
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="Zooper.Lion" Version="2.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Zooper.Lion" Version="2.1.0" />
                    
Directory.Packages.props
<PackageReference Include="Zooper.Lion" />
                    
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 Zooper.Lion --version 2.1.0
                    
#r "nuget: Zooper.Lion, 2.1.0"
                    
#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 Zooper.Lion@2.1.0
                    
#: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=Zooper.Lion&version=2.1.0
                    
Install as a Cake Addin
#tool nuget:?package=Zooper.Lion&version=2.1.0
                    
Install as a Cake Tool

Zooper.Lion

<img src="icon.png" alt="drawing" width="256"/>

Domain-Driven Design (DDD) Library

This library provides a unified and minimalist approach to implementing Domain-Driven Design (DDD) patterns in .NET applications. The architecture uses interfaces and extension methods to support both class-based and record-based implementations, allowing you to choose the most appropriate approach for your specific use case without the need for duplicate implementations.

Core Concepts

Entities and Aggregate Roots

Entities are objects with a unique identity. The library provides:

  • IEntity<TId> interface: The base contract for all entities
  • IAggregateRoot<TId> interface: A marker interface for aggregate roots
  • Extension methods that provide common implementation logic for both classes and records

Value Objects

Value objects are immutable objects identified by their properties, not by identity. The library provides:

  • IValueObject interface: The base contract for all value objects
  • IValueObjectWithComponents interface: Enables equality comparison across implementation styles
  • Extension methods that provide common implementation logic for both classes and records

Events

The library provides interfaces for event-driven architectures:

  • IEvent interface: The base contract for all events
  • IDomainEvent interface: Events that represent business changes within a domain
  • IIntegrationEvent interface: Events for communication between services/microservices

Namespace Organization

The library is organized into logical namespaces for better code organization:

  • Zooper.Lion.Common - Shared interfaces and utilities
  • Zooper.Lion.Domain.Entities - Entity and aggregate root interfaces
  • Zooper.Lion.Domain.ValueObjects - Value object interfaces and extensions
  • Zooper.Lion.Domain.Events - Domain event interfaces
  • Zooper.Lion.Integration.Events - Integration event interfaces
  • Zooper.Lion.Extensions.Records - Extension methods for record implementations

Interface-Based Design

This library uses a minimal interface-based design that gives you the freedom to implement your domain objects however you want. Instead of providing bulky abstract base classes, it offers:

  1. Simple interfaces that define the contracts
  2. Extension methods that provide shared implementation logic
  3. Example implementations for both class and record-based approaches

This approach gives you maximum flexibility while maintaining the semantic consistency of domain concepts across implementation styles.

Implementation Examples

Class-Based Approach

using Zooper.Lion.Domain.Entities;
using Zooper.Lion.Domain.ValueObjects;
using Zooper.Lion.Extensions.Records;

// Entity
public class Product : IEntity<Guid>
{
    // For classes, we can control mutability with access modifiers
    public Guid Id { get; protected set; }
    public string Name { get; private set; }

    protected Product() { }

    public Product(Guid id, string name)
    {
        Id = id;
        Name = name;
    }

    // Use the extension methods for equality
    public override bool Equals(object? obj) => this.EntityEquals(obj);
    public override int GetHashCode() => this.EntityGetHashCode();
}

// Aggregate Root
public class Order : IEntity<Guid>, IAggregateRoot<Guid>
{
    public Guid Id { get; protected set; }
    private readonly List<OrderItem> _items = new();

    public IReadOnlyList<OrderItem> Items => _items.AsReadOnly();

    public Order(Guid id)
    {
        Id = id;
    }

    public void AddItem(Product product, int quantity)
    {
        // Domain logic here
    }

    // Use the extension methods for equality
    public override bool Equals(object? obj) => this.EntityEquals(obj);
    public override int GetHashCode() => this.EntityGetHashCode();
}

// Value Object
public class Address : IValueObject, IValueObjectWithComponents
{
    public string Street { get; }
    public string City { get; }

    public Address(string street, string city)
    {
        Street = street;
        City = city;
    }

    public IEnumerable<object?> GetEqualityComponents()
    {
        yield return Street;
        yield return City;
    }

    public void Validate()
    {
        if (string.IsNullOrWhiteSpace(Street))
            throw new InvalidOperationException("Street cannot be empty");
    }

    // Use the extension methods for equality
    public override bool Equals(object? obj) =>
        this.ValueObjectEquals(obj, GetEqualityComponents);
    public override int GetHashCode() =>
        this.ValueObjectGetHashCode(GetEqualityComponents);
}

Record-Based Approach

using Zooper.Lion.Domain.Entities;
using Zooper.Lion.Domain.ValueObjects;
using Zooper.Lion.Extensions.Records;

// Entity
public record ProductRecord : IEntity<Guid>
{
    // For records, we use init-only properties for immutability
    public Guid Id { get; init; }
    public string Name { get; init; }

    public ProductRecord(Guid id, string name)
    {
        Id = id;
        Name = name;
    }

    // Use the extension methods for equality
    public bool Equals(object? obj) => this.EntityEquals(obj);
    public override int GetHashCode() => this.EntityGetHashCode();
}

// Aggregate Root
public record OrderRecord : IEntity<Guid>, IAggregateRoot<Guid>
{
    public Guid Id { get; init; }
    public IReadOnlyList<OrderItemRecord> Items { get; init; }

    public OrderRecord(Guid id, IReadOnlyList<OrderItemRecord> items)
    {
        Id = id;
        Items = items;
    }

    // With records, we create new instances for mutations
    public OrderRecord AddItem(ProductRecord product, int quantity)
    {
        var newItems = Items.ToList();
        newItems.Add(new OrderItemRecord(product, quantity));
        return this with { Items = newItems };
    }

    // Use the extension methods for equality
    public bool Equals(object? obj) => this.EntityEquals(obj);
    public override int GetHashCode() => this.EntityGetHashCode();
}

// Value Object
public record AddressRecord : IValueObject, IValueObjectWithComponents
{
    public string Street { get; init; }
    public string City { get; init; }

    public AddressRecord(string street, string city)
    {
        Street = street;
        City = city;
    }

    public IEnumerable<object?> GetEqualityComponents()
    {
        yield return Street;
        yield return City;
    }

    public void Validate()
    {
        if (string.IsNullOrWhiteSpace(Street))
            throw new InvalidOperationException("Street cannot be empty");
    }

    // Use the extension methods for equality
    public bool Equals(object? obj) =>
        this.ValueObjectEquals(obj, GetEqualityComponents);
    public override int GetHashCode() =>
        this.ValueObjectGetHashCode(GetEqualityComponents);
}

Mixing Implementation Styles

One of the benefits of this approach is that you can mix and match class-based and record-based implementations as needed:

using Zooper.Lion.Domain.Entities;
using Zooper.Lion.Domain.ValueObjects;
using Zooper.Lion.Extensions.Records;

// Use records for immutable reference data
public record ProductRecord : IEntity<Guid>, IValueObjectWithComponents
{
    public Guid Id { get; init; }
    public string Name { get; init; }
    public decimal Price { get; init; }

    // Implementation details...
}

// Use classes for mutable entities with behavior
public class ShoppingCart : IEntity<Guid>, IAggregateRoot<Guid>
{
    public Guid Id { get; protected set; }
    private readonly List<CartItem> _items = new();

    public void AddItem(ProductRecord product, int quantity)
    {
        // Domain logic...
    }

    // Implementation details...
}

Event Mapping Framework

The library includes a comprehensive event mapping framework that enables clean conversion from domain events to integration events, with support for additional context and one-to-many mappings.

Core Concepts

Domain Event Notifications

Domain event notifications wrap domain events with additional context needed for integration:

using Zooper.Lion.Domain.Events;

// Your domain event
public class UserCreatedDomainEvent : IDomainEvent
{
    public string UserId { get; }
    public string Email { get; }
    public string FirstName { get; }
    public string LastName { get; }

    public UserCreatedDomainEvent(string userId, string email, string firstName, string lastName)
    {
        UserId = userId ?? throw new ArgumentNullException(nameof(userId));
        Email = email ?? throw new ArgumentNullException(nameof(email));
        FirstName = firstName ?? throw new ArgumentNullException(nameof(firstName));
        LastName = lastName ?? throw new ArgumentNullException(nameof(lastName));
    }
}

// Notification with additional context
public class UserCreatedNotification : DomainEventNotification<UserCreatedDomainEvent>
{
    public string PlaintextPassword { get; }  // Additional context
    public string ActivationToken { get; }    // Additional context

    public UserCreatedNotification(
        UserCreatedDomainEvent domainEvent,
        string plaintextPassword,
        string activationToken) : base(domainEvent)
    {
        PlaintextPassword = plaintextPassword ?? throw new ArgumentNullException(nameof(plaintextPassword));
        ActivationToken = activationToken ?? throw new ArgumentNullException(nameof(activationToken));
    }
}
Integration Events

Integration events represent the external contract for cross-service communication:

using Zooper.Lion.Integration.Events;

public class UserRegisteredIntegrationEvent : IIntegrationEvent
{
    public string UserId { get; }
    public string Email { get; }
    public string FullName { get; }

    public UserRegisteredIntegrationEvent(string userId, string email, string fullName)
    {
        UserId = userId ?? throw new ArgumentNullException(nameof(userId));
        Email = email ?? throw new ArgumentNullException(nameof(email));
        FullName = fullName ?? throw new ArgumentNullException(nameof(fullName));
    }
}

public class WelcomeEmailIntegrationEvent : IIntegrationEvent
{
    public string Email { get; }
    public string FirstName { get; }
    public string ActivationToken { get; }

    public WelcomeEmailIntegrationEvent(string email, string firstName, string activationToken)
    {
        Email = email ?? throw new ArgumentNullException(nameof(email));
        FirstName = firstName ?? throw new ArgumentNullException(nameof(firstName));
        ActivationToken = activationToken ?? throw new ArgumentNullException(nameof(activationToken));
    }
}

Event Mappers

Typed Event Mapper

For type-safe mapping with compile-time checking:

using Zooper.Lion.Integration.Events;

public class UserCreatedEventMapper : IEventMapper<UserCreatedNotification>
{
    public IEnumerable<IIntegrationEvent> MapToIntegrationEvents(UserCreatedNotification notification)
    {
        var domainEvent = notification.DomainEvent;
        
        // Map to multiple integration events
        yield return new UserRegisteredIntegrationEvent(
            domainEvent.UserId,
            domainEvent.Email,
            $"{domainEvent.FirstName} {domainEvent.LastName}");

        yield return new WelcomeEmailIntegrationEvent(
            domainEvent.Email,
            domainEvent.FirstName,
            notification.ActivationToken);
    }
}
Flexible Event Mapper

For framework compatibility (returns objects instead of strongly typed events):

using Zooper.Lion.Integration.Events;

public class FlexibleUserCreatedEventMapper : IFlexibleEventMapper<UserCreatedNotification>
{
    public IEnumerable<object> MapToIntegrationEvents(UserCreatedNotification notification)
    {
        var domainEvent = notification.DomainEvent;
        
        yield return new UserRegisteredIntegrationEvent(
            domainEvent.UserId,
            domainEvent.Email,
            $"{domainEvent.FirstName} {domainEvent.LastName}");

        yield return new WelcomeEmailIntegrationEvent(
            domainEvent.Email,
            domainEvent.FirstName,
            notification.ActivationToken);
    }
}

Dependency Injection Setup

The library provides extension methods for automatic registration of event mappers:

using Microsoft.Extensions.DependencyInjection;
using Zooper.Lion.Extensions.DependencyInjection;

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // Register all event mappers from the calling assembly
        services.AddEventMappers();
        
        // Register event mappers from specific assemblies
        services.AddEventMappers(typeof(UserCreatedEventMapper).Assembly);
        
        // Register event mappers from assembly containing a specific type
        services.AddEventMappersFromAssemblyOf<UserCreatedEventMapper>();
    }
}

Usage in Application Services

public class UserApplicationService
{
    private readonly IEventMapper<UserCreatedNotification> _eventMapper;
    private readonly IServiceProvider _serviceProvider;

    public UserApplicationService(
        IEventMapper<UserCreatedNotification> eventMapper,
        IServiceProvider serviceProvider)
    {
        _eventMapper = eventMapper;
        _serviceProvider = serviceProvider;
    }

    public async Task CreateUserAsync(CreateUserCommand command)
    {
        // Create user and raise domain event
        var domainEvent = new UserCreatedDomainEvent(
            command.UserId, 
            command.Email, 
            command.FirstName, 
            command.LastName);

        // Create notification with additional context
        var notification = new UserCreatedNotification(
            domainEvent,
            command.PlaintextPassword,
            GenerateActivationToken());

        // Map to integration events
        var integrationEvents = _eventMapper.MapToIntegrationEvents(notification);

        // Publish integration events
        foreach (var integrationEvent in integrationEvents)
        {
            await PublishIntegrationEventAsync(integrationEvent);
        }
    }
}

Event Mapping Benefits

  • Separation of Concerns: Domain events focus on business changes, integration events focus on external contracts
  • Additional Context: Notifications can include context not available in the original domain event
  • One-to-Many Mapping: Single domain events can trigger multiple integration events
  • Type Safety: Strongly typed mappers provide compile-time checking
  • Framework Compatibility: Flexible mappers work with any event publishing framework
  • Automatic Registration: Dependency injection extensions simplify setup

Benefits of This Approach

  • Simplicity: Minimal interfaces with no bulky abstract classes
  • Flexibility: Choose between classes or records based on your needs
  • DRY: Share implementation logic through extension methods
  • Immutability: Records provide immutability by default when desired
  • Encapsulation: Proper access control across implementation styles
  • Low Coupling: Your domain model doesn't depend on base classes
  • Event-Driven Architecture: Comprehensive support for domain and integration events
Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  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 was computed.  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. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
2.1.0 404 5/29/2025
2.0.0 147 5/29/2025
1.0.0 174 4/19/2025