Aarkam.DDD.Domain 1.0.0-preview01

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

Aarkam.DDD.Domain

Nuget GitHub License Platform Build Status NuGet Downloads

A high-performance, enterprise-grade, lightweight foundation package for Domain-Driven Design (DDD) and Event Sourcing (ES) patterns in modern .NET ecosystems.

Engineered with multi-target cross-runtime portability, zero-allocation micro-optimizations, and strict encapsulation semantics, Aarkam.DDD.Domain provides pristine abstractions without binding your domain core to concrete infrastructural dependenciesβ€”inspired by the robust architectural elements of Volo.ABP Framework, MassTransit, and Marten.


πŸ“– Table of Contents


⚑ Quick Start (30 Seconds)

dotnet add package Aarkam.DDD.Domain
// Define a Value Object (immutable, equality-by-value)
public sealed class Money : ValueObject
{
    public decimal Amount { get; }
    public string Currency { get; }

    public Money(decimal amount, string currency)
    {
        DomainGuard.AgainstNegative(amount, nameof(amount));
        Amount = amount;
        Currency = currency;
    }

    protected override IEnumerable<object?> GetEqualityComponents()
    {
        yield return Amount;
        yield return Currency;
    }
}

// Define an Entity (identity-based, mutable)
public class OrderItem : Entity<Guid>
{
    public string ProductSku { get; private set; } = null!;
    public Money Price { get; private set; } = null!;
    public int Quantity { get; private set; }

    protected OrderItem() { }  // For EF Core

    public OrderItem(Guid id, string sku, Money price, int quantity) : base(id)
    {
        DomainGuard.AgainstNullOrEmpty(sku, nameof(sku));
        DomainGuard.AgainstNegativeOrZero(quantity, nameof(quantity));
        
        ProductSku = sku;
        Price = price;
        Quantity = quantity;
    }
}

// Define an Aggregate Root with Domain Events
public sealed class OrderPlacedEvent : DomainEvent<Order>
{
    public string CustomerId { get; init; } = null!;
    public decimal TotalValue { get; init; }
}

public class Order : AggregateRoot<Guid>
{
    public string CustomerId { get; private set; } = null!;
    public decimal TotalAmount { get; private set; }
    public OrderStatus Status { get; private set; }

    protected Order() { }  // For EF Core

    public Order(Guid id, string customerId) : base(id)
    {
        DomainGuard.AgainstNullOrEmpty(customerId, nameof(customerId));
        RaiseEvent(new OrderPlacedEvent(id.ToString(), Version + 1, customerId, 0));
    }

    protected override void ApplyEvent(IDomainEvent domainEvent)
    {
        if (domainEvent is OrderPlacedEvent e)
        {
            Id = Guid.Parse(e.AggregateId);
            CustomerId = e.CustomerId;
            TotalAmount = e.TotalValue;
            Status = OrderStatus.Created;
        }
    }
}

public enum OrderStatus { Created, Processing, Shipped, Completed }

That's it! You now have type-safe, fully-audited, event-sourced domain models.


πŸ“¦ Installation

Via .NET CLI

dotnet add package Aarkam.DDD.Domain

Via Package Manager

Install-Package Aarkam.DDD.Domain

Via NuGet Package Explorer

Search for Aarkam.DDD.Domain on nuget.org

Supported Platforms

  • .NET Standard 2.1 (Xamarin, Unity, legacy .NET Framework)
  • .NET 6.0 (LTS)
  • .NET 8.0 (LTS)
  • .NET 10.0 (Current)

πŸ›οΈ Core Concepts

1. Entity<TKey> β€” Thread of Continuity

An Entity is defined by its identity, not its attributes. Two entities with the same ID are considered the same, even if their attributes differ.

public abstract class Entity<TKey> : IEntity<TKey>, IEquatable<Entity<TKey>>
    where TKey : IEquatable<TKey>
{
    public virtual TKey Id { get; protected set; }
    
    public virtual bool IsTransient() => EqualityComparer<TKey>.Default.Equals(Id, default);
}

Key Features:

  • βœ… Identity-based equality (not reference-based)
  • βœ… Transient detection (unpersisted entities)
  • βœ… ORM-friendly parameterless constructor
  • βœ… Protected setters guard against external mutation

2. ValueObject β€” Structural Immutability

A Value Object has no identity. Two value objects are completely interchangeable if their attributes match. They must be immutable.

public abstract class ValueObject : IEquatable<ValueObject>
{
    protected abstract IEnumerable<object?> GetEqualityComponents();
    
    public override int GetHashCode()
    {
        var hashCode = new HashCode();
        foreach (var component in GetEqualityComponents())
            hashCode.Add(component);
        return hashCode.ToHashCode();
    }
}

Key Features:

  • βœ… Attribute-based equality (structural comparison)
  • βœ… Works safely in HashSet<T> and Dictionary<K,V>
  • βœ… Immutability enforced via design
  • βœ… Composable (value objects can contain other value objects)

3. AggregateRoot<TKey> β€” Transactional Boundary

An Aggregate Root is the entry point for a cluster of entities. External code can only reference an aggregate via its root.

public abstract class AggregateRoot<TKey> : Entity<TKey>, IAggregateRoot<TKey>
{
    private readonly List<IDomainEvent> _changes = [];
    
    public virtual int Version { get; protected set; } = -1;
    public virtual Dictionary<string, object?> ExtraProperties { get; protected set; }
    public virtual bool IsDeleted { get; protected set; }
    public virtual string? TenantId { get; protected set; }
    
    protected void RaiseEvent(IDomainEvent domainEvent) => _changes.Add(domainEvent);
    protected abstract void ApplyEvent(IDomainEvent domainEvent);
    public IReadOnlyCollection<IDomainEvent> GetChanges() => _changes.AsReadOnly();
}

Key Features:

  • βœ… Event sourcing support (RaiseEvent + ApplyEvent pattern)
  • βœ… Multi-tenancy built-in (TenantId)
  • βœ… Soft deletes (IsDeleted flag + EF Global Query Filters)
  • βœ… Audit tracking (CreatorId, CreationTime, LastModifierId, LastModificationTime)
  • βœ… Extensible properties (ExtraProperties for dynamic attributes)
  • βœ… Optimistic concurrency (Version field for race condition detection)
  • βœ… Zero-copy event collection (GetChanges returns direct list cast)

4. DomainGuard β€” Fail-Fast Validation

Guards prevent garbage collection (GC) thrashing via compiled lambda exception factories.

public static class DomainGuard
{
    public static void AgainstNull([NotNull] object? value, string parameterName);
    public static void AgainstNullOrEmpty([NotNull] string? value, string parameterName);
    public static void AgainstNegative(long value, string parameterName);
    public static void AgainstNegativeOrZero(long value, string parameterName);
    public static void AgainstOutOfRange(int value, int min, int max, string parameterName);
    public static void CheckRule(IBusinessRule rule);
}

Performance: Uses compiled expression factories instead of Activator.CreateInstance(), eliminating reflection overhead.

5. IBusinessRule β€” Complex Invariants

Encapsulate multi-field business logic without polluting the aggregate with conditional chains.

public interface IBusinessRule
{
    bool IsBroken();
    string Message { get; }
}

public sealed class CustomerMustHaveValidEmailRule : IBusinessRule
{
    private readonly string _email;
    public CustomerMustHaveValidEmailRule(string email) => _email = email;
    
    public bool IsBroken() => !_email.Contains("@");
    public string Message => "Email format is invalid.";
}

// Usage
public void UpdateEmail(string newEmail)
{
    DomainGuard.AgainstNullOrEmpty(newEmail, nameof(newEmail));
    DomainGuard.CheckRule(new CustomerMustHaveValidEmailRule(newEmail));
    Email = newEmail;
}

6. IDomainEvent β€” Immutable Change Facts

Events are the single source of truth for state changes. They are append-only and immutable.

public interface IDomainEvent
{
    string AggregateId { get; }
    int Version { get; }
    DateTime OccurredAt { get; }
    Dictionary<string, object>? Metadata { get; }
}

public abstract class DomainEvent<TAggregate> : IDomainEvent
    where TAggregate : AggregateRoot<Guid>
{
    public string AggregateId { get; init; } = null!;
    public int Version { get; init; }
    public DateTime OccurredAt { get; init; } = DateTime.UtcNow;
    public Dictionary<string, object>? Metadata { get; init; }
}

7. ISpecification<T> β€” Translatable Queries

Build complex queries that translate cleanly to SQL via EF Core.

public sealed class ActiveOrdersSpecification : SpecificationBase<Order>
{
    public ActiveOrdersSpecification(string customerId, int pageIndex = 0, int pageSize = 10)
    {
        AddInclude(o => o.Items);
        AddInclude(o => o.Payments);
        OrderByDescending = o => o.CreationTime;
        Skip = pageIndex * pageSize;
        Take = pageSize;
    }

    public override Expression<Func<Order, bool>> ToExpression()
    {
        return order => order.CustomerId == customerId 
            && !order.IsDeleted
            && order.Status != OrderStatus.Completed;
    }
}

// Usage in Repository
public async Task<List<Order>> GetActiveOrdersAsync(string customerId)
{
    var spec = new ActiveOrdersSpecification(customerId);
    return await _context.Orders
        .Where(spec.ToExpression())
        .ToListAsync();
}

πŸ—οΈ Architectural Patterns Reference

Component DDD Pattern Use Case
Entity<TKey> Thread of Continuity Objects with persistent identity
ValueObject Structural Immutability Money, Address, Email, Coordinates
AggregateRoot<TKey> Transactional Boundary Order, Customer, Invoice
IDomainEvent Append-Only Facts Order Placed, Payment Received
IBusinessRule Invariant Validation Email must be unique, quantity > 0
ISpecification<T> Query Translation Filtered searches, pagination
IDomainService Stateless Cross-Aggregate Calculating discounts, validating policies
IInboxMessage / IOutboxMessage Transactional Messaging Idempotent message handling
ISnapshot State Baseline Rehydration Optimizing event stream replays
ISaga<TSagaData> Distributed Orchestration Long-running workflows

Reference Literature:

  • Eric Evans, Domain-Driven Design (Evans, 2003)
  • Vaughn Vernon, Implementing Domain-Driven Design (Vernon, 2013)
  • Greg Young, CQRS and Event Sourcing (Young, 2010)
  • Martin Fowler, Patterns of Enterprise Application Architecture (Fowler, 2002)
  • Gregor Hohpe, Enterprise Integration Patterns (Hohpe & Woolf, 2003)

πŸ’» Usage Examples

Example 1: E-Commerce Order Management

namespace Ecommerce.Orders.Domain;

// Value Objects
public sealed class OrderNumber : ValueObject
{
    public string Value { get; }
    
    public OrderNumber(string value)
    {
        DomainGuard.AgainstNullOrEmpty(value, nameof(value));
        Value = value;
    }
    
    protected override IEnumerable<object?> GetEqualityComponents()
    {
        yield return Value;
    }
}

public sealed class OrderLineItem : ValueObject
{
    public string Sku { get; }
    public int Quantity { get; }
    public Money UnitPrice { get; }

    public OrderLineItem(string sku, int quantity, Money unitPrice)
    {
        DomainGuard.AgainstNullOrEmpty(sku, nameof(sku));
        DomainGuard.AgainstNegativeOrZero(quantity, nameof(quantity));
        DomainGuard.AgainstNull(unitPrice, nameof(unitPrice));
        
        Sku = sku;
        Quantity = quantity;
        UnitPrice = unitPrice;
    }

    public Money GetLineTotal() => new(UnitPrice.Amount * Quantity, UnitPrice.Currency);

    protected override IEnumerable<object?> GetEqualityComponents()
    {
        yield return Sku;
        yield return Quantity;
        yield return UnitPrice;
    }
}

// Domain Events
public sealed class OrderCreatedEvent : DomainEvent<Order>
{
    public string CustomerId { get; init; } = null!;
    public List<OrderLineItem> LineItems { get; init; } = [];
    public Money TotalAmount { get; init; } = null!;
}

public sealed class OrderConfirmedEvent : DomainEvent<Order>
{
    public string ConfirmedBy { get; init; } = null!;
    public DateTime ConfirmedAt { get; init; }
}

public sealed class OrderCancelledEvent : DomainEvent<Order>
{
    public string CancellationReason { get; init; } = null!;
}

// Aggregate Root
public class Order : AggregateRoot<Guid>
{
    private readonly List<OrderLineItem> _lineItems = [];

    public OrderNumber OrderNumber { get; private set; } = null!;
    public string CustomerId { get; private set; } = null!;
    public Money TotalAmount { get; private set; } = null!;
    public OrderStatus Status { get; private set; }
    public IReadOnlyList<OrderLineItem> LineItems => _lineItems.AsReadOnly();

    protected Order() { }  // For EF Core

    public Order(Guid id, OrderNumber orderNumber, string customerId) : base(id)
    {
        DomainGuard.AgainstNull(orderNumber, nameof(orderNumber));
        DomainGuard.AgainstNullOrEmpty(customerId, nameof(customerId));

        OrderNumber = orderNumber;
        CustomerId = customerId;
        Status = OrderStatus.Draft;

        RaiseEvent(new OrderCreatedEvent
        {
            AggregateId = id.ToString(),
            Version = Version + 1,
            CustomerId = customerId
        });
    }

    public void AddLineItem(OrderLineItem lineItem)
    {
        DomainGuard.Against<InvalidOperationException>(
            Status != OrderStatus.Draft,
            "Cannot add items to a confirmed order."
        );
        DomainGuard.AgainstNull(lineItem, nameof(lineItem));

        _lineItems.Add(lineItem);
        RecalculateTotal();
    }

    public void Confirm(string confirmedBy)
    {
        DomainGuard.AgainstNullOrEmpty(confirmedBy, nameof(confirmedBy));
        DomainGuard.Against<InvalidOperationException>(
            _lineItems.Count == 0,
            "Cannot confirm an empty order."
        );

        Status = OrderStatus.Confirmed;
        RaiseEvent(new OrderConfirmedEvent
        {
            AggregateId = Id.ToString(),
            Version = Version + 1,
            ConfirmedBy = confirmedBy,
            ConfirmedAt = DateTime.UtcNow
        });
    }

    public void Cancel(string reason)
    {
        DomainGuard.AgainstNullOrEmpty(reason, nameof(reason));
        DomainGuard.Against<InvalidOperationException>(
            Status == OrderStatus.Completed,
            "Cannot cancel a completed order."
        );

        Status = OrderStatus.Cancelled;
        RaiseEvent(new OrderCancelledEvent
        {
            AggregateId = Id.ToString(),
            Version = Version + 1,
            CancellationReason = reason
        });
    }

    protected override void ApplyEvent(IDomainEvent domainEvent)
    {
        switch (domainEvent)
        {
            case OrderCreatedEvent e:
                OrderNumber = new(e.AggregateId);
                CustomerId = e.CustomerId;
                TotalAmount = e.TotalAmount;
                Status = OrderStatus.Draft;
                break;

            case OrderConfirmedEvent:
                Status = OrderStatus.Confirmed;
                break;

            case OrderCancelledEvent:
                Status = OrderStatus.Cancelled;
                break;
        }
    }

    private void RecalculateTotal()
    {
        var total = _lineItems.Sum(li => li.GetLineTotal().Amount);
        TotalAmount = new(total, "USD");
    }
}

public enum OrderStatus { Draft, Confirmed, Processing, Shipped, Completed, Cancelled }

Example 2: Multi-Tenant SaaS Application

// Inject current tenant from authentication middleware
public class OrderService
{
    private readonly IOrderRepository _repository;
    private readonly ICurrentTenant _currentTenant;

    public OrderService(IOrderRepository repository, ICurrentTenant currentTenant)
    {
        _repository = repository;
        _currentTenant = currentTenant;
    }

    public async Task CreateOrderAsync(CreateOrderRequest request)
    {
        var order = new Order(Guid.NewGuid(), request.OrderNumber, request.CustomerId);
        order.TenantId = _currentTenant.Id;  // Automatically scoped to tenant

        await _repository.AddAsync(order);
        await _repository.SaveChangesAsync();
    }

    // Tenant context switching for background jobs
    public async Task ProcessCrossTenantSyncAsync(string targetTenantId, Guid orderId)
    {
        using (_currentTenant.Change(targetTenantId))
        {
            var order = await _repository.GetAsync(orderId);
            order.Confirm("system");
            await _repository.UpdateAsync(order);
        }
    }
}

⚑ Performance Characteristics

Zero-Allocation Optimizations

  1. GetChanges() Smart Cast

    public IReadOnlyCollection<IDomainEvent> GetChanges() 
        => _changes.AsReadOnly();  // Direct cast, no copy
    

    Impact: Eliminates allocation when collecting uncommitted events.

  2. Compiled Exception Factories

    private static class ExceptionFactory<TException> where TException : Exception
    {
        private static readonly Func<string, TException> _factory = CreateFactory();
    
        public static TException Create(string message) => _factory(message);
    }
    

    Impact: Guard clauses don't trigger reflection overhead; useful in tight loops.

  3. ValueObject HashCode Computation

    • Uses modern HashCode struct (low collision rates)
    • Avoids legacy prime-multiplication algorithms
    • Safe for use in HashSet<T> and Dictionary<K,V>

Benchmarks (Estimated)

Operation Time Allocations
Entity.Equals() <1 Β΅s 0 bytes
ValueObject.Equals() 1-5 Β΅s 0 bytes
DomainGuard.AgainstNull() <1 Β΅s 0 bytes (cached lambda)
Order.RaiseEvent() <1 Β΅s 64 bytes (event object)

Note: Benchmarks are illustrative. Run dotnet benchmark for precise measurements in your environment.


πŸ”’ Security Considerations

1. Tenant Isolation

Risk: Accidental cross-tenant data leakage

Mitigation:

// βœ… CORRECT: Inject TenantId from authenticated context
public void Create(CreateOrderRequest request)
{
    var order = new Order(Guid.NewGuid(), request.OrderNumber, request.CustomerId);
    order.TenantId = _currentTenant.Id;  // Set from middleware, not user input
    _repository.Add(order);
}

// ❌ WRONG: TenantId from user input
public void Create(CreateOrderRequest request)
{
    var order = new Order(Guid.NewGuid(), request.OrderNumber, request.CustomerId);
    order.TenantId = request.TenantId;  // SECURITY BUG: trusting user input!
}

Best Practice: Use ICurrentTenant middleware that derives TenantId from JWT claims or session.

2. Entity Serialization

Risk: Sensitive data exposed via JSON serialization

Mitigation:

public class Order : AggregateRoot<Guid>
{
    [JsonIgnore]
    public string? InternalNotes { get; private set; }

    [JsonIgnore]
    public Dictionary<string, object?> ExtraProperties { get; protected set; }
}

3. Guard Clause Validation

Always validate at aggregate boundaries:

public void UpdatePrice(Money newPrice)
{
    DomainGuard.AgainstNull(newPrice, nameof(newPrice));
    DomainGuard.AgainstNegative(newPrice.Amount, nameof(newPrice.Amount));
    
    // Only here is the price safe to mutate
    Price = newPrice;
}

🎯 Comparison to Alternatives

vs. Volo.ABP Framework

Aspect Aarkam.DDD.Domain Volo.ABP
Dependency Injection 0 (pure types) Full integration
Database Abstraction None (works with any ORM) Built-in repositories
Authorization 0 (leave to middleware) RBAC + ABP.Authorization
Performance Optimized (zero-allocation) Full-featured (some overhead)
Learning Curve Minimal Steep (more to learn)
Use Case Foundation library Complete framework

When to use Aarkam: You want DDD patterns without framework lock-in.
When to use ABP: You need a complete, opinionated platform.

vs. MassTransit

Aspect Aarkam.DDD.Domain MassTransit
Purpose Domain modeling Message orchestration
Event Sourcing Built-in contracts External (via Marten)
Sagas Interface defined Fully implemented
Message Bus 0 (BYOB) Integrated

Complementary: Use both! Aarkam for domain, MassTransit for distribution.

vs. Marten

Aspect Aarkam.DDD.Domain Marten
Database Agnostic (any DB) PostgreSQL only
Event Store Contracts only Full implementation
ACID Snapshots Via user code Built-in
Query Optimization EF Core specs Marten projections

Complementary: Use Aarkam for domain types, Marten for PostgreSQL event store backend.


🀝 Contributing

Contributions are welcome! Please follow these guidelines:

  1. Report issues via GitHub Issues (include reproduction steps)
  2. Fork and branch from develop branch
  3. Write tests for new features (xUnit)
  4. Follow code style (nullable annotations, XML docs, 120-char lines)
  5. Commit messages in conventional format: feat: add ISaga interface
  6. Submit PR with description and linked issue

Development Setup:

git clone https://github.com/melhelbawi/Aarkam.DDD.Domain.git
cd Aarkam.DDD.Domain
dotnet restore
dotnet test
dotnet build

πŸ“‹ Versioning

This project follows Semantic Versioning 2.0.0:

  • MAJOR (X.0.0): Breaking API changes
  • MINOR (0.X.0): New features, backward-compatible
  • PATCH (0.0.X): Bug fixes, no API changes

Stability:

  • 1.0.0+: Production-ready
  • 0.x.x: Preview (API may change)

πŸ“„ License

Distributed under the MIT License. See LICENSE for details.

Copyright (c) 2026 Mohamed Elhelbawi / Aarkam

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions...

πŸ™ Acknowledgments

Built with inspiration from:

  • Eric Evans β€” Domain-Driven Design (Blue Book)
  • Vaughn Vernon β€” Implementing DDD
  • Greg Young β€” Event Sourcing Patterns
  • Martin Fowler β€” Enterprise Architecture Patterns
  • Volo.ABP Team β€” ABP Framework design philosophy
  • MassTransit Contributors β€” Distributed systems patterns

πŸ“ž Support


Made with ❀️ by Mohamed Elhelbawi


πŸ“š Further Reading

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 is compatible.  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 is compatible.  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 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. 
.NET Core netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.1 is compatible. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen 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
1.0.0-preview01 85 6/27/2026