DKNet

DKNet.Fw.Extensions

Framework-agnostic reflection, type, string, enum and DI-inspection helpers shared by every layer of a DKNet solution.

✨ Why use it?

Reach for it whenever you catch yourself hand-rolling reflection-based property access, a “does this type implement X” check, enum-to-Display mapping, digit extraction from a formatted string, or an assembly scan for types matching a shape.

🚀 Quick Start

dotnet add package DKNet.Fw.Extensions
using DKNet.Fw.Extensions.Reflection;

var product = new Product { Name = "Laptop", Price = 999.99m };

var name = product.GetPropertyValue("name");                  // "Laptop" — lookup is case-insensitive
product.SetPropertyValue("Price", 1099.99m);                  // converted to the property's type

typeof(List<string>).IsImplementOf(typeof(IEnumerable<>));    // true — open generic match

No DI registration, no configuration and no startup wiring is required for the extension methods — they are static/extension methods, so referencing the package and adding the right using is the entire setup. The one exception is the ServiceCollectionExtensions feature described below, which you call explicitly wherever you build up an IServiceCollection.

Members are grouped into per-area namespaces rather than one flat DKNet.Fw.Extensions namespace — see the using line on each example below.

🧩 Features

String extensions (StringExtensions)

using DKNet.Fw.Extensions.Primitives;

"Price: $123.45".ExtractDigits();   // "123.45"
"99.99".IsNumber();                 // true
"123-456-7890".IsNumber();          // false — more than one '-', not a leading sign

Type extensions (TypeExtensions)

using DKNet.Fw.Extensions.Reflection;

typeof(int?).GetNonNullableType();          // typeof(int)
typeof(List<string>).IsImplementOf(typeof(IEnumerable<>)); // true (open generic match)
typeof(MyRepo<User>).IsImplementOf<IRepository<User>>();   // true
typeof(decimal).IsNumericType();            // true
typeof(MyEnum).TryConvertToEnum(1, out var value); // true, value = (MyEnum)1

Enum extensions with Display attribute info (EnumExtensions, EnumInfo)

using System.ComponentModel.DataAnnotations;
using DKNet.Fw.Extensions.Enums;

public enum OrderStatus
{
    [Display(Name = "Pending", Description = "Waiting for processing")]
    Pending,
    Processing,
}

OrderStatus.Pending.GetAttribute<DisplayAttribute>()?.Name; // "Pending"

var info = OrderStatus.Pending.GetEnumInfo();
// info!.Key = "Pending", info.Name = "Pending", info.Description = "Waiting for processing"

foreach (var i in EnumExtensions.GetEnumInfos<OrderStatus>())
    Console.WriteLine($"{i.Key}: {i.Name}");
// Pending: Pending
// Processing: Processing   (Name falls back to the field name when there's no [Display])

EnumInfo.Name is declared required string Name (non-nullable), but GetEnumInfo() assigns it via att?.Name! with no fallback — if the enum value has no [Display] attribute (or the attribute has no Name), Name comes back null at runtime despite the non-nullable declaration. GetEnumInfos<T>() doesn’t have this problem because it falls back to the field name. Null-check Name after calling GetEnumInfo().

DateTime extensions (DateTimeExtensions)

using DKNet.Fw.Extensions.Primitives;

DateTime.Today.InQuarter();          // 1, 2, 3, or 4
DateTime.Today.LastDayOfMonth();     // e.g. 2026-08-31, Kind = Local
((DateTime?)null).LastDayOfMonth();  // null

Async enumerable extensions — removed

AsyncEnumerableExtensions.ToListAsync(this IAsyncEnumerable<T>) has been removed. It used to live in the ambient System.Collections.Generic namespace precisely so it would show up without an extra using — but that same ambient placement is what killed it: .NET 10 ships its own System.Linq.AsyncEnumerable.ToListAsync extension in that same reachable surface, and having both in scope made every call site ambiguous. Use the BCL method instead:

using System.Linq; // System.Linq.AsyncEnumerable.ToListAsync

IAsyncEnumerable<int> source = GetAsyncNumbers();
List<int> all = await source.ToListAsync(cancellationToken);

The BCL version also takes a CancellationToken (DKNet’s did not) and returns List<T> rather than IList<T>.

Property extensions (PropertyExtensions)

using DKNet.Fw.Extensions.Reflection;

var product = new Product { Name = "Laptop", Price = 999.99m };

var prop = product.GetProperty("name");          // case-insensitive, any access level
var value = product.GetPropertyValue("Owner.Address.City"); // dotted path, nested properties

product.SetPropertyValue("Price", 1099.99m);     // by name, converts to the property's type
product.SetPropertyValue(prop!, 42);             // by PropertyInfo
product.TrySetPropertyValue("DoesNotExist", 1);  // swallows the failure instead of throwing

Attribute extensions (AttributeExtensions)

using DKNet.Fw.Extensions.Reflection;

typeof(Product).HasAttribute<ObsoleteAttribute>();                 // Type overload
productType.GetProperty("Price").HasAttribute<RequiredAttribute>(); // PropertyInfo overload
product.HasAttributeOnProperty<RequiredAttribute>("Price");         // by property name, via reflection

Collection extensions (CollectionExtensions)

using DKNet.Fw.Extensions.Collections;

ICollection<int> target = [1, 2, 3];
target.AddRange([4, 5, 6]); // target now has 6 items

Service-collection / DI extensions (ServiceCollectionExtensions, ServiceCollectionRegistrationExtensions)

These are inspection/guard helpers you call while assembling an IServiceCollection — they don’t register anything themselves.

using Microsoft.Extensions.DependencyInjection; // ServiceCollectionExtensions lives in this namespace

// Guard a single-active-implementation contract from being registered twice.
if (!services.IsRegistered<IIdempotencyKeyStore>())
    services.AddSingleton<IIdempotencyKeyStore, SqlIdempotencyKeyStore>();

// Guard a multi-implementation contract from registering the *same* implementation twice,
// while still allowing a second, different implementation to coexist.
if (!services.IsRegisteredWithImplementation<IEventHandler>(typeof(OrderCreatedHandler)))
    services.AddScoped<IEventHandler, OrderCreatedHandler>();

// Inspect a ServiceDescriptor, including keyed registrations.
ServiceDescriptor descriptor = services[0];
bool isFoo = descriptor.IsImplementationOf<IFoo>();
bool isKeyedFoo = descriptor.IsKeyedImplementationOf<IFoo>("fooKey");

TypeExtractors — fluent assembly/type scanning

using System.Reflection;
using DKNet.Fw.Extensions.TypeExtractors;

Assembly[] assemblies = [typeof(Program).Assembly];

var handlerTypes = assemblies
    .Extract()          // ITypeExtractor over every type in the given assemblies
    .Classes()
    .NotAbstract()
    .IsInstanceOf<IEventHandler>()
    .Where(t => t.Namespace!.EndsWith("Handlers", StringComparison.Ordinal))
    .ToList(); // ITypeExtractor : IEnumerable<Type>, so LINQ works directly on it

⚙️ Configuration reference

There is no options object, no IOptions<T> and no environment-specific behavior — every member above is a static or extension method with fixed behavior. The only configurable surface is a single optional parameter:

Option Type Default Effect
GetProperty(propertyName, flags) – flags BindingFlags IgnoreCase \| Public \| NonPublic \| Instance Which properties the reflection lookup considers. GetPropertyValue, SetPropertyValue, TrySetPropertyValue and HasAttributeOnProperty all resolve through this default and do not expose the parameter themselves.
HasAttribute<TAttribute>(inherit) / HasAttributeOnProperty<TAttribute>(propertyName, inherit) – inherit bool true Whether attributes inherited from a base type/property count as present.

🧱 Where it fits

TypeExtractors is the piece other DKNet packages lean on hardest — it is how they discover your entity configurations, seeders, and global model builders without you registering each one by hand:

Data-flow diagram of TypeExtractor: assemblies enter through Extract(), chain through shape filters (Classes, Interfaces, Enums, Abstract) and relationship filters (IsInstanceOf, HasAttribute, Where), and stay lazy until enumerated into the type list that DKNet's entity configuration, seeding, and model-builder discovery consume.

DKNet.Fw.Extensions sits at the bottom of the dependency graph and is referenced directly by:

DKNet.SlimBus.Extensions does not reference this package directly; it picks it up transitively through DKNet.EfCore.Events → DKNet.EfCore.Hooks.

⚠️ Gotchas & limits