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 configure action in AddKnmCore(), not via WithCache(), 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

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