KNM.Core 1.3.1
KNM.Core
KNM.Core is a shared NuGet infrastructure library for KoNiMa enterprise .NET applications. It eliminates code duplication across projects by providing battle-tested, configurable core services.
Features
| Feature | Description |
|---|---|
| Email Sending | SMTP email with 4 responsive HTML templates + plain-text fallback, configurable retry policy |
| In-Memory Cache | Prefixed cache keys, absolute + sliding expiration, tenant-aware key isolation |
| Structured Logging | Database, File, Application Insights, and Serilog providers with automatic log cleanup |
| Template Rendering | $$PLACEHOLDER$$ substitution engine for HTML/text templates |
| Result Pattern | Result<T> for error handling without exceptions |
| Extension Methods | String, Number, Date, Enum helpers |
| Progress Tracker | Generic progress reporting with subscription support for long-running operations |
| Event Bus | In-memory publish/subscribe event bus, thread-safe, singleton-scoped |
| Correlation ID | Automatic request correlation across middleware, logging, and outgoing HTTP calls |
| Multi-Tenancy | Header-based tenant resolution with cache isolation and log segregation |
| Health Checks | SMTP, Redis, and file storage health checks |
| Exception Handler | Global exception middleware returning standardized JSON error responses |
| License Validator | Offline (RSA+AES), online (API), and hybrid license validation with feature query API |
Installation
dotnet add package KNM.Core
Quick Start
// Program.cs — service registration
builder.Services.AddKnmCore()
.WithCache(cache => {
cache.Prefix = "MYAPP_";
cache.DurationInMinutes = 60;
})
.WithEmail(email => {
email.SmtpHost = "smtp.example.com";
email.SmtpPort = 587;
email.SmtpCredentialProvider = async () =>
new NetworkCredential(
await vault.GetSecretAsync("smtp-user"),
await vault.GetSecretAsync("smtp-password"));
email.SmtpDisplayName = "My App";
email.SmtpDisplayEmail = "noreply@example.com";
email.EnableSsl = true;
email.MaxSendRetries = 3;
})
.WithLogger(logger => {
logger.Level = KnmLogLevel.Info;
logger.Provider = KnmLogProvider.File;
logger.FilePath = "logs/app.log";
})
.WithCorrelation(c => c.HeaderName = "X-Request-Id")
.WithMultiTenancy(t => t.HeaderName = "X-Tenant-Id")
.WithHealthChecks()
.WithExceptionHandler(e => e.IncludeDetails = builder.Environment.IsDevelopment())
.WithLogCleanup();
// Program.cs — middleware pipeline
app.UseKnmCorrelation(); // first — sets correlation ID
app.UseKnmTenant(); // after correlation
app.UseKnmExceptionHandler(); // after tenant, before controllers
app.MapHealthChecks("/health");
Or via appsettings.json inline configuration:
builder.Services.AddKnmCore(options => {
options.Cache.Prefix = "MYAPP_";
options.Email.SmtpHost = "smtp.example.com";
});
Email Service
Send a simple email
public class MyService(IEmailSenderService emailSender)
{
public async Task SendWelcome(string userEmail, string userName)
{
var dto = new SendMailDto
{
Company = "MyApp",
LogoUrl = "https://myapp.com/logo.png",
Template = EmailTemplate.General,
Recipients = [
new UserEmailSettings
{
Language = "en",
Subject = $"Welcome, {userName}!",
DisplayName = userName,
EmailAddress = userEmail,
Message = new MailMessage
{
Preview = "Welcome to MyApp",
Title = "WELCOME!",
Salutation = $"Dear {userName}",
Message = "Thank you for joining our platform.",
Goodbye = "The MyApp Team"
}
}
]
};
var result = await emailSender.SendAsync(dto);
if (!result.IsSuccess)
Console.WriteLine($"Send failed: {result.Error}");
}
}
Available Templates
| Template | Use case |
|---|---|
EmailTemplate.General |
Basic informational email |
EmailTemplate.WithButton |
Email with a call-to-action button |
EmailTemplate.WithDetailedDetails |
Email with icon + highlighted details block |
EmailTemplate.WithCode |
Email with a prominently displayed OTP/verification code |
Custom Template Override
Supply per-project HTML/TXT files via EmailOptions.TemplateOverrides. When a key is present the renderer loads from disk; otherwise it falls back to the embedded template.
builder.Services.AddKnmCore()
.WithEmail(email => {
email.TemplateOverrides[EmailTemplate.General] =
("wwwroot/emails/General.html", "wwwroot/emails/General.txt");
});
Paths are absolute or relative to the working directory. The file content is cached in memory after the first load, identical to embedded templates.
Cache Service
IAppCache provides a unified interface for both in-memory and Redis caching. The backend is selected automatically based on CacheOptions.UseRedis.
In-memory (default)
builder.Services.AddKnmCore(o => {
o.Cache.Prefix = "MYAPP_";
o.Cache.DurationInMinutes = 60;
});
Redis
Note: Redis must be configured via the inline
configureaction inAddKnmCore(), not viaWithCache(), so the connection string is available when the DI container is built.
builder.Services.AddKnmCore(o => {
o.Cache.UseRedis = true;
o.Cache.RedisConnectionString = "localhost:6379";
o.Cache.Prefix = "MYAPP_";
o.Cache.DurationInMinutes = 120;
});
Usage
public class MyService(IAppCache cache)
{
// Works with int IDs
public async Task<User?> GetUser(int id)
{
return await cache.GetOrSetAsync(
key: $"user:{id}",
factory: async () => await db.Users.FindAsync(id),
duration: TimeSpan.FromMinutes(15)
);
}
// Works with Guid IDs — the cache key is always a string
public async Task<User?> GetUser(Guid id)
{
return await cache.GetOrSetAsync(
key: $"user:{id}",
factory: async () => await db.Users.FindAsync(id),
duration: TimeSpan.FromMinutes(15)
);
}
}
Values are automatically JSON-serialized when Redis is active.
Logger Service
IKnmLogger is a structured application logger with pluggable providers. Configure via WithLogger().
Providers
| Provider | Description |
|---|---|
KnmLogProvider.File |
Daily + size-rotated log files (e.g. app.2026-03-11.log, app.2026-03-11_001.log) |
KnmLogProvider.Database |
Persists entries via IKnmLogStore (consumer-implemented) |
KnmLogProvider.ApplicationInsights |
Sends TraceTelemetry / ExceptionTelemetry to Azure Application Insights |
KnmLogProvider.Serilog |
Delegates to a fully configured Serilog pipeline (any sink, enricher, or filter) |
Configuration
// File provider
.WithLogger(logger => {
logger.Provider = KnmLogProvider.File;
logger.Level = KnmLogLevel.Info;
logger.FilePath = "logs/app.log"; // date suffix added automatically
logger.LogCulture = "en-US"; // language for log messages
logger.MaxFileSizeKb = 5120; // rotate at 5 MB (default: 10 MB, 0 = disabled)
});
// Database provider — requires IKnmLogStore registration (see below)
.WithLogger(logger => {
logger.Provider = KnmLogProvider.Database;
logger.Level = KnmLogLevel.Warning;
});
// Application Insights provider
.WithLogger(logger => {
logger.Provider = KnmLogProvider.ApplicationInsights;
logger.AppInsightsConnString = "InstrumentationKey=...";
});
// Serilog provider (any sink — Console, File, Seq, Elastic, …)
.WithLogger(logger => {
logger.Provider = KnmLogProvider.Serilog;
logger.Level = KnmLogLevel.Debug;
logger.SerilogConfiguration = cfg => cfg
.WriteTo.Console()
.WriteTo.File("logs/app-.log", rollingInterval: RollingInterval.Day)
.Enrich.FromLogContext();
});
Database Logging with IKnmLogStore
When using KnmLogProvider.Database, KNM.Core resolves IKnmLogStore from the DI container to persist log entries. You own the schema — KNM.Core never creates or migrates tables.
Step 1 — Create your table (example SQL Server):
CREATE TABLE KnmLogs (
Id INT IDENTITY PRIMARY KEY,
TimestampUtc DATETIME2 NOT NULL,
Level TINYINT NOT NULL, -- maps to KnmLogLevel enum
EventName NVARCHAR(200) NOT NULL,
Category NVARCHAR(500) NOT NULL,
Message NVARCHAR(MAX) NOT NULL,
EventId INT NULL,
EntityGuid UNIQUEIDENTIFIER NULL, -- for Guid-based entity correlation
CorrelationId NVARCHAR(64) NULL, -- request correlation
TenantId NVARCHAR(128) NULL -- multi-tenant isolation
);
Step 2 — Implement IKnmLogStore:
// EF Core example
public class EfLogStore(MyDbContext db) : IKnmLogStore
{
public async Task SaveAsync(KnmLogEntry entry)
{
db.KnmLogs.Add(entry);
await db.SaveChangesAsync();
}
}
// Dapper example
public class DapperLogStore(IDbConnection connection) : IKnmLogStore
{
public async Task SaveAsync(KnmLogEntry entry)
{
await connection.ExecuteAsync(
"INSERT INTO KnmLogs (TimestampUtc, Level, EventName, Category, Message, EventId) " +
"VALUES (@TimestampUtc, @Level, @EventName, @Category, @Message, @EventId)",
new { entry.TimestampUtc, Level = (int)entry.Level, entry.EventName,
entry.Category, entry.Message, entry.EventId });
}
}
Step 3 — Register in DI:
builder.Services.AddScoped<IKnmLogStore, EfLogStore>(); // or DapperLogStore, etc.
builder.Services.AddKnmCore()
.WithLogger(logger => {
logger.Provider = KnmLogProvider.Database;
logger.Level = KnmLogLevel.Warning;
});
KnmLogEntry fields:
| Property | Type | Description |
|---|---|---|
TimestampUtc |
DateTime |
UTC timestamp of the log event |
Level |
KnmLogLevel |
Severity (Trace=0, Debug=1, Info=2, Warning=3, Error=4, Critical=5) |
EventName |
string |
Action or event name (e.g. "SendEmail") |
Category |
string |
Logger category (typically the class name) |
Message |
string |
Log message (PII-redacted when RedactPii = true) |
EventId |
int? |
Optional numeric event identifier |
EntityGuid |
Guid? |
Optional GUID-based entity identifier |
CorrelationId |
string? |
Request correlation identifier |
TenantId |
string? |
Tenant identifier for multi-tenant isolation |
Usage
public class MyService(IKnmLogger logger)
{
// With int entity ID
public void DoWork(int orderId)
{
logger.LogInfo("Order processing started", action: "DoWork", entity: "Order", entityId: orderId);
try { /* ... */ }
catch (Exception ex)
{
logger.LogError("Order processing failed", exception: ex, action: "DoWork", entity: "Order", entityId: orderId);
}
}
// With Guid entity ID
public void DoWork(Guid userId)
{
logger.LogInfo("User sync started", action: "DoWork", entity: "User", entityGuid: userId);
try { /* ... */ }
catch (Exception ex)
{
logger.LogError("User sync failed", userId, exception: ex, action: "DoWork", entity: "User");
}
}
}
Log methods
| Method | Level |
|---|---|
LogInfo(message, ...) |
Information |
LogWarning(message, ...) |
Warning |
LogError(message, exception?, ...) |
Error |
LogCritical(message, exception?, ...) |
Critical |
Background Jobs
KnmBackgroundService<TJob> wraps any IBackgroundJob implementation as an IHostedService with automatic retry, configurable interval, and integrated IKnmLogger logging. The job is resolved from a fresh DI scope on every execution, so scoped dependencies are fully supported.
Implement a job
public class ReportJob(IReportService reports) : IBackgroundJob
{
public async Task ExecuteAsync(CancellationToken cancellationToken)
{
await reports.GenerateDailyReportAsync(cancellationToken);
}
}
Register
builder.Services.AddKnmCore()
.WithBackgroundJob<ReportJob>(job => {
job.JobName = "DailyReport";
job.ExecutionInterval = TimeSpan.FromHours(24);
job.MaxRetries = 3;
job.RetryDelay = TimeSpan.FromMinutes(5);
job.StopOnUnhandledFailure = false; // keep running even after exhausted retries
});
Multiple jobs can be chained:
builder.Services.AddKnmCore()
.WithBackgroundJob<ReportJob>(...)
.WithBackgroundJob<CleanupJob>(...);
BackgroundJobOptions
| Property | Default | Description |
|---|---|---|
JobName |
type name | Name used in log messages |
ExecutionInterval |
1 minute | Delay between execution cycles |
MaxRetries |
3 | Retry attempts per cycle on failure |
RetryDelay |
5 seconds | Fixed delay between retries |
StopOnUnhandledFailure |
false | Terminate hosted service after retries exhausted |
Enabled |
true | When false, the job is not registered |
Correlation ID
KNM.Core provides automatic request correlation via ICorrelationContext. A unique correlation ID is generated per request (or read from an incoming header) and propagated through:
- Log entries —
CorrelationIdfield inKnmLogEntry+ file log format[timestamp] LEVEL [correlationId] category: message - Outgoing HTTP calls — via
CorrelationIdDelegatingHandler - Response headers — automatically added to HTTP responses
Configuration
builder.Services.AddKnmCore()
.WithCorrelation(c => {
c.HeaderName = "X-Request-Id"; // default: "X-Correlation-Id"
c.Enabled = true; // default: true
});
// Middleware pipeline (register early, before logging/exception middleware)
app.UseKnmCorrelation();
Propagate to outgoing HTTP calls
builder.Services.AddHttpClient<MyApiClient>()
.AddHttpMessageHandler<CorrelationIdDelegatingHandler>();
Access in code
public class MyService(ICorrelationContext correlation)
{
public void DoWork()
{
var id = correlation.CorrelationId; // auto-generated GUID or from incoming header
}
}
Multi-Tenancy
Header-based tenant resolution with automatic cache key isolation and log segregation.
Configuration
builder.Services.AddKnmCore()
.WithMultiTenancy(t => {
t.HeaderName = "X-Tenant-Id"; // default: "X-Tenant-Id"
});
// Middleware pipeline (after correlation, before exception handler)
app.UseKnmTenant();
How it works
- Cache keys — automatically prefixed with
t:{tenantId}:when a tenant is active - Log entries —
TenantIdfield inKnmLogEntry+ file log format includes[t:tenantId] - Custom resolution — implement
ITenantResolverto resolve from JWT claims, subdomains, etc. and register in DI to override the default header-based resolver
Access in code
public class MyService(ITenantContext tenant)
{
public void DoWork()
{
var tenantId = tenant.TenantId; // null when no tenant resolved
}
}
Health Checks
Built-in health checks for SMTP, Redis, and file storage. Integrates with the standard ASP.NET Core health check system.
Configuration
builder.Services.AddKnmCore()
.WithHealthChecks(hc => {
hc.IncludeSmtp = true; // default: true
hc.IncludeRedis = true; // default: true (only registered when UseRedis=true)
hc.IncludeFileStorage = true; // default: true
hc.Timeout = TimeSpan.FromSeconds(5);
});
app.MapHealthChecks("/health");
| Check | Tags | What it verifies |
|---|---|---|
knm-smtp |
smtp, email | TCP connection to SMTP host:port |
knm-redis |
redis, cache | SET/REMOVE on IDistributedCache |
knm-file-storage |
storage, files | Directory exists (local) or connection string present (Azure) |
Exception Handler
Global exception middleware that catches unhandled exceptions, logs them via IKnmLogger, and returns a standardized JSON error response.
Configuration
builder.Services.AddKnmCore()
.WithExceptionHandler(e => {
e.IncludeDetails = builder.Environment.IsDevelopment(); // stack traces in dev only
e.LogLevel = KnmLogLevel.Error; // default: Error
});
// Register after correlation + tenant middleware, before controllers
app.UseKnmExceptionHandler();
Response format
{
"success": false,
"error": "An unexpected error occurred. Please try again later.",
"errorCode": "INTERNAL_ERROR",
"traceId": "d4f5e6a7-...",
"details": "System.NullReferenceException: ..."
}
traceIdis the correlation ID (when available)detailsis only included whenIncludeDetails = true
Email Retry Policy
Configure automatic retry on SMTP send failures with optional exponential backoff.
.WithEmail(email => {
email.MaxSendRetries = 3; // default: 0 (no retry)
email.RetryDelay = TimeSpan.FromSeconds(2); // default: 2s
email.UseExponentialBackoff = true; // default: true → 2s, 4s, 8s
});
Log Cleanup Job
Built-in background job that periodically purges old log entries from the database via IKnmLogStore.PurgeAsync(DateTime).
Configuration
builder.Services.AddKnmCore()
.WithLogger(logger => {
logger.Provider = KnmLogProvider.Database;
logger.LogRetentionDays = 30; // entries older than 30 days are deleted
})
.WithLogCleanup(job => {
job.ExecutionInterval = TimeSpan.FromHours(24); // default: 24h
});
Implement PurgeAsync
The consumer must implement PurgeAsync in their IKnmLogStore:
public class EfLogStore(MyDbContext db) : IKnmLogStore
{
public async Task SaveAsync(KnmLogEntry entry)
{
db.KnmLogs.Add(entry);
await db.SaveChangesAsync();
}
public async Task<int> PurgeAsync(DateTime olderThan)
{
return await db.KnmLogs
.Where(l => l.TimestampUtc < olderThan)
.ExecuteDeleteAsync();
}
}
Note:
PurgeAsynchas a default interface implementation that returns -1 (no-op). ExistingIKnmLogStoreimplementations continue to work without changes.
Progress Tracker
IProgressTracker provides generic progress reporting with subscription support for long-running operations. Registered as a singleton — safe to inject anywhere.
Configuration
builder.Services.AddKnmCore()
.WithProgressTracking();
Create and report progress
public class ImportService(IProgressTracker tracker)
{
public async Task ImportAsync(string operationId, List<Record> records)
{
var progress = tracker.CreateProgress<int>(operationId);
for (var i = 0; i < records.Count; i++)
{
await ProcessRecord(records[i]);
progress.Report((i + 1) * 100 / records.Count);
}
tracker.Complete(operationId);
}
}
Subscribe to progress updates
public class ProgressHub(IProgressTracker tracker)
{
public IDisposable Watch(string operationId, Func<int, Task> onProgress)
{
return tracker.Subscribe<int>(operationId, onProgress);
}
public int? GetCurrent(string operationId)
{
return tracker.GetLastProgress<int>(operationId);
}
}
API
| Method | Description |
|---|---|
CreateProgress<T>(operationId) |
Creates an IProgress<T> bound to the operation |
ReportAsync<T>(operationId, value, ct?) |
Reports progress and notifies all subscribers |
Subscribe<T>(operationId, handler) |
Subscribes to updates — returns IDisposable for cleanup |
GetLastProgress<T>(operationId) |
Gets the last reported value, or null |
Complete(operationId) |
Removes all tracking state for a completed operation |
Event Bus
IEventBus is an in-memory publish/subscribe event bus. Thread-safe, singleton-scoped. Failed subscriber callbacks are caught and logged — delivery continues to remaining subscribers.
Configuration
builder.Services.AddKnmCore()
.WithEventBus();
Subscribe and publish
public record OrderCreatedEvent(Guid OrderId, string CustomerEmail);
public class NotificationService(IEventBus eventBus) : IDisposable
{
private readonly IDisposable _subscription = eventBus.Subscribe<OrderCreatedEvent>(
async e => await SendEmailAsync(e.CustomerEmail, e.OrderId));
private Task SendEmailAsync(string email, Guid orderId)
{
// send notification...
return Task.CompletedTask;
}
public void Dispose() => _subscription.Dispose();
}
public class OrderService(IEventBus eventBus)
{
public async Task CreateOrderAsync(Order order)
{
// save order...
await eventBus.PublishAsync(new OrderCreatedEvent(order.Id, order.CustomerEmail));
}
}
API
| Method | Description |
|---|---|
Subscribe<TEvent>(handler) |
Subscribes to events of type TEvent — returns IDisposable for cleanup |
PublishAsync<TEvent>(event, ct?) |
Publishes an event to all subscribers of TEvent |
Result Pattern
// Return Result<T> from your methods
public Result<User> GetUser(int id)
{
var user = db.Users.Find(id);
return user is null
? Result<User>.Failure("User not found", "USER_404")
: Result<User>.Success(user);
}
// Consume with chaining
var result = GetUser(42)
.Map(user => user.Email)
.Map(email => email.ToLower());
if (result.IsSuccess)
Console.WriteLine(result.Value);
else
Console.WriteLine($"Error [{result.ErrorCode}]: {result.Error}");
HttpClient Extensions
HttpClientExtensions wraps any HttpClient with automatic Polly retry governed by RetryPolicyOptions. All methods return Result<T> or Result — exceptions never propagate to the caller.
| Method | Description |
|---|---|
GetWithRetryAsync<T>(url, retryPolicy?, timeout?) |
GET → deserialize JSON to T |
PostWithRetryAsync<T>(url, body, retryPolicy?, timeout?) |
POST JSON body → deserialize response |
PutWithRetryAsync<T>(url, body, retryPolicy?, timeout?) |
PUT JSON body → deserialize response |
DeleteWithRetryAsync(url, retryPolicy?, timeout?) |
DELETE → Result |
Configure retry policy globally
builder.Services.AddKnmCore()
.WithRetryPolicy(retry => {
retry.MaxRetries = 5;
retry.BaseDelay = TimeSpan.FromMilliseconds(500);
retry.UseExponentialBackoff = true;
retry.UseJitter = true;
retry.DefaultTimeout = TimeSpan.FromSeconds(20);
retry.RetryOnStatusCodes = [HttpStatusCode.ServiceUnavailable, HttpStatusCode.GatewayTimeout];
retry.RetryOnHttpRequestException = true;
retry.RetryOnExceptionTypes = [typeof(TimeoutException)];
});
Usage (with DI)
public class MyService(HttpClient httpClient, RetryPolicyOptions retryPolicy)
{
public async Task<Result<WeatherData>> GetWeather()
=> await httpClient.GetWithRetryAsync<WeatherData>(
"https://api.example.com/weather/rome",
retryPolicy: retryPolicy,
timeout: TimeSpan.FromSeconds(10));
}
Usage (one-off, no DI)
var result = await httpClient.GetWithRetryAsync<WeatherData>(
url: "https://api.example.com/weather/rome");
// Uses HttpClientExtensions.DefaultRetryPolicy (3 retries, 30s timeout, 5xx/408 triggers)
File Storage Service
IFileStorage provides a unified API for file upload, download, delete, existence check, and URL resolution. Inject it via DI and swap the provider with WithFileStorage().
| Provider | Description |
|---|---|
FileStorageProvider.LocalDisk |
Files stored on local file system under LocalBasePath |
FileStorageProvider.AzureBlob |
Files stored in Azure Blob Storage container |
Configuration
// Local disk (default)
builder.Services.AddKnmCore()
.WithFileStorage(fs => {
fs.Provider = FileStorageProvider.LocalDisk;
fs.LocalBasePath = "/var/app/uploads";
});
// Azure Blob Storage
builder.Services.AddKnmCore()
.WithFileStorage(fs => {
fs.Provider = FileStorageProvider.AzureBlob;
fs.AzureConnectionString = "DefaultEndpointsProtocol=https;...";
fs.AzureContainerName = "my-container";
fs.AzurePublicBaseUrl = "https://cdn.example.com"; // optional CDN prefix
});
Usage
public class DocumentService(IFileStorage storage)
{
public async Task<Result<string>> SaveAsync(Stream pdfStream, string name)
=> await storage.UploadAsync(pdfStream, $"docs/{name}.pdf", "application/pdf");
public async Task<Result<Stream>> ReadAsync(string name)
=> await storage.DownloadAsync($"docs/{name}.pdf");
public async Task<string?> GetLink(string name)
{
var result = await storage.GetUrlAsync($"docs/{name}.pdf");
return result.IsSuccess ? result.Value : null;
}
}
Configuration Extensions
Extension methods for IConfiguration and IConfigurationSection providing safe, fail-fast access patterns and DataAnnotations-based validation.
| Method | Description |
|---|---|
GetRequired<T>(key) |
Returns the value or throws InvalidOperationException if missing/empty |
GetOrDefault<T>(key, defaultValue) |
Returns the value or defaultValue — never throws |
BindAndValidate<T>(sectionKey?) |
Binds section to T and validates DataAnnotations → Result<T> |
All methods work on both IConfiguration and IConfigurationSection.
// Fail-fast: throws if "App:ApiKey" is absent or empty
var apiKey = configuration.GetRequired<string>("App:ApiKey");
// Safe fallback
var timeout = configuration.GetOrDefault<int>("App:TimeoutMs", defaultValue: 5000);
// Bind + validate (uses [Required], [Range], etc. attributes on the options class)
public class SmtpOptions
{
[Required] public string Host { get; set; } = string.Empty;
[Range(1, 65535)] public int Port { get; set; } = 587;
}
Result<SmtpOptions> result = configuration.BindAndValidate<SmtpOptions>("Smtp");
if (!result.IsSuccess)
throw new InvalidOperationException(result.Error);
// Also works on a section directly
var section = configuration.GetSection("Smtp");
Result<SmtpOptions> result2 = section.BindAndValidate<SmtpOptions>();
Template Renderer Extensions
TemplateRendererExtensions adds two optional rendering engines on top of the default $$PLACEHOLDER$$ substitution. Both methods are extension methods on ITemplateRenderer — inject it as usual and call the method directly.
Handlebars
Uses Handlebars.Net — familiar {{ }} syntax with helpers, partials, and block expressions.
public class MyService(ITemplateRenderer renderer)
{
public string BuildEmail(string name, string[] items)
{
var template = """
Hello, {{Name}}!
{{#each Items}}
- {{this}}
{{/each}}
""";
return renderer.RenderHandlebars(template, new { Name = name, Items = items });
}
}
Scriban
Uses Scriban — a fast scripting engine with full loop/conditional support and a Liquid-compatible mode.
public class MyService(ITemplateRenderer renderer)
{
public string BuildEmail(string name, string[] items)
{
var template = """
Hello, {{ Name }}!
{{ for item in Items }}
- {{ item }}
{{ end }}
""";
return renderer.RenderScriban(template, new { Name = name, Items = items });
}
}
Note: Scriban exposes property names exactly as they are declared (PascalCase). Use
{{ Name }},{{ Title }}, etc.
Extension Methods
String
"hello world".Truncate(8); // "hello..."
"User@Example.COM".NormalizeEmail(); // "user@example.com"
"<b>Hello</b>".ToCleanText(); // "Hello"
myObject.ToJson(indent: true); // pretty JSON
"{\"name\":\"Alice\"}".FromJson<User>(); // User object
Number
1234.56m.ToCurrency("it-IT"); // "€ 1.234,56"
1234.5678m.RoundBankers(2); // 1234.57
1_500_000L.ToHumanReadable(); // "1.5M"
Date
DateTime.Now.ToShortDate(); // "11/03/2026" (default: it-IT)
DateTime.Now.ToShortDate("en-US"); // "3/11/2026"
DateTime.Now.ToFormattedDate("yyyy-MM-dd"); // "2026-03-11"
DateTime.Now.ToIso8601(); // "2026-03-11T10:00:00.000Z"
new DateTime(1990, 5, 15).GetAge(); // 35
DateTime.Now.ToUnixTimestamp(); // Unix epoch seconds
DateTime.Now.IsWeekend(); // false
DateTime.Now.StartOfMonth(); // 2026-03-01
DateTime.Now.EndOfMonth(); // 2026-03-31
Enum
MyEnum.SomeValue.GetDescription(); // reads [Description] attribute
"Active".TryParse<StatusEnum>(); // StatusEnum? (null-safe)
License Validator
Opt-in module for software license validation. Supports offline (file-based with RSA + AES), online (API), and hybrid modes. Disabled by default.
Registration
builder.Services.AddKnmCore()
.WithLicenseValidator(license =>
{
license.Mode = ValidationMode.Hybrid;
license.ApiBaseUrl = "https://licenses.konima.it";
license.ApiKey = "my-api-key";
license.LicenseKey = "ABCDE-FGHIJ-KLMNO-PQRST-UVWXY";
license.LicenseDirectory = "./Licenses";
license.ValidateOnStartup = true;
license.ThrowOnInvalidLicense = false;
license.CacheExpiration = TimeSpan.FromHours(24);
});
Or from appsettings.json:
builder.Services.AddKnmCore()
.WithLicenseValidator(builder.Configuration.GetSection("LicenseValidator"));
Feature Query API
After validation, use ILicenseFeatures (singleton) for stable, always-available feature checks:
[Inject] private ILicenseFeatures License { get; set; } = default!;
// Top-level features
if (License.Has("REPORTING")) { /* module enabled */ }
var maxUsers = License.Get<int>("MAX_USERS");
// Sub-module features
if (License.Has("REPORTING", "EXPORT_PDF")) { /* sub-feature enabled */ }
var maxReports = License.Get<int>("REPORTING", "MAX_REPORTS");
// All features
var features = License.GetAll();
On-Demand Validation
Use ILicenseValidator for explicit re-validation:
[Inject] private ILicenseValidator Validator { get; set; } = default!;
var result = await Validator.ValidateLicenseAsync();
if (result.IsValid)
{
// result.HasFeature("EXPORT_PDF")
// result.GetFeatureValue<int>("MAX_USERS")
}
Validation Modes
| Mode | Description |
|---|---|
OfflineOnly |
Validates using local PublicKey.xml + License.txt files |
OnlineOnly |
Validates via HTTP to the license API |
Hybrid |
Online first, falls back to offline (configurable) |
Startup Gate
Block application startup if the license is invalid:
license.ValidateOnStartup = true;
license.ThrowOnInvalidLicense = true; // throws InvalidOperationException
Custom Key Provider
Implement ILicenseKeyProvider for database-backed license storage:
builder.Services.AddKnmCore()
.WithLicenseValidator(license =>
{
license.Mode = ValidationMode.Hybrid;
license.UseCustomProvider<DatabaseLicenseKeyProvider>();
});
Multi-Tenant Support
ILicenseFeatures is tenant-aware. When multi-tenancy is enabled via .WithMultiTenancy(), each tenant's features are stored independently in the singleton.
Architecture
KNM.Core/
├── Models/ DTOs, enums, CoreOptions, Result<T>, KnmErrorResponse
├── Services/
│ ├── Interfaces/ IEmailSenderService, IAppCache, ITemplateRenderer, IKnmLogger,
│ │ ICorrelationContext, ITenantContext, ITenantResolver, IKnmLogStore,
│ │ IProgressTracker, IEventBus
│ └── ... Concrete implementations (CorrelationContext, TenantContext, LogCleanupJob, etc.)
├── Middleware/ CorrelationIdMiddleware, TenantMiddleware, KnmExceptionMiddleware + extensions
├── HealthChecks/ SmtpHealthCheck, RedisHealthCheck, FileStorageHealthCheck
├── Extensions/ StringExtensions, NumberExtensions, DateExtensions, EnumExtensions, TemplateRendererExtensions
├── Templates/ Embedded HTML + TXT email templates
├── Resources/ Localized strings (en-US default, en-GB, it-IT, es-ES, fr-FR, de-DE)
└── Internal/ EmailInternal, Logging providers (not public API)
Requirements
- .NET 10+
- Microsoft.Extensions.Caching.Memory
- Microsoft.Extensions.DependencyInjection.Abstractions
- Microsoft.Extensions.Logging.Abstractions
Security & Compliance
KNM.Core includes built-in support for GDPR-aware data handling and secure credential management. See SECURITY.md for the full security policy and GDPR compliance checklist.
Credential Providers
Avoid storing SMTP passwords, Redis connection strings, or Azure keys as plain-text properties. Use credential providers to resolve secrets at runtime from a vault or environment:
.WithEmail(email =>
{
email.SmtpHost = "smtp.example.com";
email.SmtpCredentialProvider = async () =>
new NetworkCredential(
await vault.GetSecretAsync("smtp-user"),
await vault.GetSecretAsync("smtp-password"));
})
Credential providers are available for SMTP (SmtpCredentialProvider), Redis (RedisCredentialProvider), and Azure Blob Storage (AzureCredentialProvider).
PII Redaction
LoggerOptions.RedactPii (default: true) enables automatic redaction of email addresses, names, and other personal data in log output via the IPiiRedactor interface. Combine with LoggerOptions.LogRetentionDays (default: 30) to enforce data retention policies.
Email Audit Trail
SendAsync returns an EmailAuditRecord containing a non-PII audit entry (timestamp, recipient count, template used, result status) suitable for compliance logging.
Rate Limiting
Set EmailOptions.MaxEmailsPerMinute to cap outbound email throughput and prevent abuse or accidental bulk sends.
Cache PII TTL
When caching entries that contain personal data, pass containsPii: true to enforce a shorter TTL controlled by CacheOptions.PiiTtlMinutes (default: 5 minutes):
await cache.SetAsync("user:42", userData, containsPii: true);
Attachment Validation
- File size limit:
EmailOptions.MaxAttachmentSizeMb(default: 10 MB) - Extension whitelist:
EmailOptions.AllowedAttachmentExtensionsrestricts permitted file types - Path traversal prevention: attachment paths must be absolute with no
..sequences
License
MIT — KoNiMa S.r.l.
Showing the top 20 packages that depend on KNM.Core.
| Packages | Downloads |
|---|---|
|
KNM.Reporting
Unified reporting abstraction supporting SSRS, FastReport, QuestPDF, and ClosedXML with fluent DI configuration
|
1 |
|
KNM.Reporting
Unified reporting abstraction supporting SSRS, FastReport, QuestPDF, and ClosedXML with fluent DI configuration
|
2 |
|
KNM.Reporting.Designer
Plug and play Blazor admin panel for report generation, management, and monitoring in the KoNiMa ecosystem
|
0 |
.NET 10.0
- Azure.Storage.Blobs (>= 12.27.0)
- Handlebars.Net (>= 2.1.6)
- Microsoft.ApplicationInsights (>= 3.0.0)
- Microsoft.Extensions.Caching.StackExchangeRedis (>= 10.0.5)
- Polly (>= 8.6.6)
- Scriban (>= 7.0.3)
- Serilog (>= 4.3.1)
- Serilog.Extensions.Logging (>= 10.0.0)
| Version | Downloads | Last updated |
|---|---|---|
| 1.5.0 | 3 | 30/03/2026 |
| 1.4.0 | 3 | 29/03/2026 |
| 1.3.1 | 4 | 26/03/2026 |
| 1.3.0 | 0 | 26/03/2026 |
| 1.2.0 | 4 | 25/03/2026 |
| 0.0.2-alpha | 2 | 12/03/2026 |
| 0.0.1-alpha | 1 | 11/03/2026 |