KNM.Core 0.0.2-alpha
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 |
| In-Memory Cache | Prefixed cache keys, absolute + sliding expiration |
| Structured Logging | Database, File, and Application Insights providers |
| 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 |
Installation
dotnet add package KNM.Core
Quick Start
// Program.cs
builder.Services.AddKnmCore()
.WithCache(cache => {
cache.Prefix = "MYAPP_";
cache.DurationInMinutes = 60;
})
.WithEmail(email => {
email.SmtpHost = "smtp.example.com";
email.SmtpPort = 587;
email.SmtpUser = "user@example.com";
email.SmtpPassword = "password";
email.SmtpDisplayName = "My App";
email.SmtpDisplayEmail = "noreply@example.com";
email.EnableSsl = true;
})
.WithLogger(logger => {
logger.Level = KnmLogLevel.Info;
logger.Provider = KnmLogProvider.File;
logger.FilePath = "logs/app.log";
});
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)
{
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)
);
}
}
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 |
Writes to a DbContext via a configurable factory + SaveLogEntry reflection call |
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
.WithLogger(logger => {
logger.Provider = KnmLogProvider.Database;
logger.Level = KnmLogLevel.Warning;
logger.DbContextFactory = sp => sp.GetRequiredService<MyAppDbContext>();
// DbContext must expose a SaveLogEntry(string level, string message, ...) method
});
// 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();
});
Usage
public class MyService(IKnmLogger logger)
{
public void DoWork()
{
logger.LogInfo("Operation started", action: "DoWork", entity: "MyService");
try { /* ... */ }
catch (Exception ex)
{
logger.LogError("Operation failed", exception: ex, action: "DoWork");
}
}
}
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 |
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)
Architecture
KNM.Core/
├── Models/ DTOs, enums, CoreOptions, Result<T>
├── Services/
│ ├── Interfaces/ IEmailSenderService, IAppCache, ITemplateRenderer, IKnmLogger
│ └── ... Concrete implementations
├── Extensions/ StringExtensions, NumberExtensions, DateExtensions, EnumExtensions, TemplateRendererExtensions
├── Templates/ Embedded HTML + TXT email templates
├── Resources/ Localized strings (en-US default, it-IT, es-ES, fr-FR, de-DE)
└── Internal/ EmailInternal (not public API)
Requirements
- .NET 10+
- Microsoft.Extensions.Caching.Memory
- Microsoft.Extensions.DependencyInjection.Abstractions
- Microsoft.Extensions.Logging.Abstractions
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.Memory (>= 10.0.4)
- Microsoft.Extensions.Caching.StackExchangeRedis (>= 10.0.4)
- Microsoft.Extensions.Configuration.Binder (>= 10.0.4)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.4)
- Microsoft.Extensions.Hosting.Abstractions (>= 10.0.4)
- Microsoft.Extensions.Logging (>= 10.0.4)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.4)
- Microsoft.Extensions.Options (>= 10.0.4)
- Polly (>= 8.6.6)
- Scriban (>= 5.12.1)
- 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 |