Table of Contents

Extensions and helpers

The DevKit CodeBlock.DevKit.Core library groups cross-cutting utilities you can reuse from any layer (domain, application, infrastructure, UI). They are not business features themselves—they keep common checks, formatting, and plumbing consistent.

Extensions (CodeBlock.DevKit.Core.Extensions)

StringExtensions.cs

  • Namespace: CodeBlock.DevKit.Core.Extensions
// Copyright (c) CodeBlock.Dev. All rights reserved.
// For more information visit https://codeblock.dev

using System.Text.RegularExpressions;

namespace CodeBlock.DevKit.Core.Extensions;

/// <summary>
/// Extension methods for string manipulation and validation.
/// Provides common string operations like formatting, validation, and URL handling.
/// </summary>
public static class StringExtensions
{
    /// <summary>
    /// It adds space before each uppercase letter except the first
    /// For example: UserRegistered => User Registered
    /// </summary>

    public static string ToSpacedWords(this string input)
    {
        if (input.IsNullOrEmptyOrWhiteSpace())
            return input;

        return Regex.Replace(input, "(?<!^)([A-Z])", " $1");
    }

    /// <summary>
    /// Checks if the string contains only alphanumeric characters and underscores.
    /// </summary>

    public static bool IsAlphanumericAndUnderscore(this string input)
    {
        return Regex.IsMatch(input, "^[a-zA-Z0-9_]+$");
    }

    /// <summary>
    /// Checks if the string is null, empty, or contains only whitespace.
    /// </summary>

    public static bool IsNullOrEmptyOrWhiteSpace(this string input)
    {
        return string.IsNullOrEmpty(input) || string.IsNullOrWhiteSpace(input);
    }

    /// <summary>
    /// Removes all spaces from the string.
    /// </summary>

    public static string RemoveSpaces(this string input)
    {
        if (!string.IsNullOrEmpty(input))
            return input.Replace(" ", "");

        return input;
    }

    /// <summary>
    /// Combines a base URL with a relative URL, handling trailing slashes properly.
    /// </summary>

    public static string AddRelativeUrl(this string baseUrl, string relativeUrl)
    {
        if (baseUrl.IsNullOrEmptyOrWhiteSpace())
            return relativeUrl;

        if (relativeUrl.IsNullOrEmptyOrWhiteSpace())
            return baseUrl;

        return $"{baseUrl.TrimEnd('/')}/{relativeUrl.TrimStart('/')}";
    }
}

DateTimeExtensions.cs

  • Namespace: CodeBlock.DevKit.Core.Extensions
// Copyright (c) CodeBlock.Dev. All rights reserved.
// For more information visit https://codeblock.dev

using System.Text;
using CodeBlock.DevKit.Core.Resources;
using Microsoft.Extensions.Localization;

namespace CodeBlock.DevKit.Core.Extensions;

/// <summary>
/// Extension methods for DateTime operations and formatting.
/// Provides utilities for duration formatting, time ago calculations, and localized display.
/// </summary>
public static class DateTimeExtensions
{
    /// <summary>
    /// Formats a duration given in milliseconds based on the following rules:
    /// - If less than 1000 ms, return as "X ms" (rounded to 2 decimal places if needed).
    /// - If 1000 ms or more, convert to "X s Y ms".
    /// - If 60 seconds or more, convert to "X min Y s Z ms".
    /// - If the value is an exact minute or second, exclude unnecessary parts.
    /// </summary>
    /// <param name="milliseconds">The duration in milliseconds.</param>
    /// <returns>A formatted string representing the duration.</returns>
    public static string FormatDuration(this double milliseconds)
    {
        if (milliseconds < 1000)
        {
            return milliseconds % 1 == 0 ? $"{milliseconds} ms" : $"{milliseconds:F2} ms";
        }

        var totalSeconds = (int)(milliseconds / 1000);
        var remainingMs = Math.Round(milliseconds % 1000, 2);
        var minutes = totalSeconds / 60;
        var seconds = totalSeconds % 60;

        if (minutes > 0)
        {
            if (remainingMs == 0)
                return seconds == 0 ? $"{minutes} min" : $"{minutes} min {seconds} s";
            return $"{minutes} min {seconds} s {remainingMs:F2} ms";
        }

        return remainingMs == 0 ? $"{seconds} s" : $"{seconds} s {remainingMs:F2} ms";
    }

    /// <summary>
    /// Converts the date to a short date string like "Feb 7" or "2025 Feb 7".
    /// If the date is within the same year as the min and max dates, the year is omitted.
    /// </summary>
    public static string ToShortDateString(this DateTime current, DateTime min, DateTime max)
    {
        if (min.Year == max.Year)
        {
            // Same year: "Feb 7"
            return current.ToString("MMM d");
        }
        else
        {
            // Different years: "2025 Feb 7"
            return current.ToString("yyyy MMM d");
        }
    }

    /// <summary>
    /// Converts a nullable DateTime to local time.
    /// </summary>
    public static DateTime? ToLocalTime(this DateTime? dateTime)
    {
        if (dateTime == null)
            return null;

        return dateTime.Value.ToLocalTime();
    }

    /// <summary>
    /// Converts a DateTime to a localized "time ago" string.
    /// </summary>
    public static string ToLocalizedTimeAgo(this DateTime localDateTime, IStringLocalizer<CoreResource> localizer)
    {
        var now = DateTime.UtcNow;
        var timespan = now - localDateTime.ToUniversalTime();

        var years = timespan.Days / 365;
        var months = timespan.Days % 365 / 30;
        var days = timespan.Days % 30;
        var hours = timespan.Hours;
        var minutes = timespan.Minutes;
        var seconds = timespan.Seconds;

        var sb = new StringBuilder();

        if (years > 0)
            sb.Append($"{years} {localizer[CoreResource.Years]}, ");
        if (months > 0)
            sb.Append($"{months} {localizer[CoreResource.Months]}, ");
        if (days > 0)
            sb.Append($"{days} {localizer[CoreResource.Days]}, ");
        if (hours > 0)
            sb.Append($"{hours} {localizer[CoreResource.Hours]}, ");
        if (minutes > 0)
            sb.Append($"{minutes} {localizer[CoreResource.Minutes]}, ");
        if (seconds > 0 || sb.Length == 0)
            sb.Append($"{seconds} {localizer[CoreResource.Seconds]} ");

        sb.Append($" {localizer[CoreResource.Ago]} ");

        return sb.ToString().Trim();
    }
}

DictionaryExtensions.cs

  • Namespace: CodeBlock.DevKit.Core.Extensions
// Copyright (c) CodeBlock.Dev. All rights reserved.
// For more information visit https://codeblock.dev

namespace CodeBlock.DevKit.Core.Extensions;

/// <summary>
/// Extension methods for Dictionary operations.
/// Provides utility methods for merging and manipulating dictionary collections.
/// </summary>
public static class DictionaryExtensions
{
    /// <summary>
    /// Merges the target dictionary into the source dictionary.
    /// </summary>
    /// <typeparam name="TKey">The type of the dictionary keys.</typeparam>
    /// <typeparam name="TValue">The type of the dictionary values.</typeparam>
    /// <param name="source">The source dictionary to merge into.</param>
    /// <param name="target">The target dictionary to merge from.</param>
    /// <returns>The merged source dictionary.</returns>

    public static Dictionary<TKey, TValue> Merge<TKey, TValue>(this Dictionary<TKey, TValue> source, Dictionary<TKey, TValue> target)
    {
        source ??= new Dictionary<TKey, TValue>();

        if (target != null)
        {
            foreach (var item in target)
            {
                source[item.Key] = item.Value;
            }
        }

        return source;
    }
}

NumberFormatting.cs

  • Namespace: CodeBlock.DevKit.Core.Extensions
// Copyright (c) CodeBlock.Dev. All rights reserved.
// For more information visit https://codeblock.dev

using System.Globalization;

namespace CodeBlock.DevKit.Core.Extensions;

/// <summary>
/// Extension methods for number formatting with culture support.
/// Provides utilities for price formatting, thousand separators, and localized number display.
/// </summary>
public static class NumberFormatting
{
    /// <summary>
    /// Formats a decimal price value with thousand separators and configurable decimal places.
    /// </summary>
    /// <param name="value">The decimal price to format.</param>
    /// <param name="decimalPlaces">Number of decimal places (default: 2).</param>
    /// <param name="culture">Optional culture for formatting (default: current culture).</param>
    /// <returns>A formatted price string.</returns>

    public static string ToFormattedPrice(this decimal value, int decimalPlaces = 2, CultureInfo culture = null)
    {
        culture ??= CultureInfo.CurrentCulture;
        return value.ToString($"N{decimalPlaces}", culture);
    }

    /// <summary>
    /// Formats a nullable decimal price with thousand separators and configurable decimal places.
    /// Returns an empty string if the value is null.
    /// </summary>

    public static string ToFormattedPrice(this decimal? value, int decimalPlaces = 2, CultureInfo culture = null)
    {
        return value.HasValue ? value.Value.ToFormattedPrice(decimalPlaces, culture) : string.Empty;
    }

    /// <summary>
    /// Formats an integer price value with thousand separators (no decimal places).
    /// </summary>

    public static string ToFormattedPrice(this int value, CultureInfo culture = null)
    {
        culture ??= CultureInfo.CurrentCulture;
        return value.ToString("N0", culture);
    }

    /// <summary>
    /// Formats a nullable integer price value with thousand separators.
    /// Returns an empty string if the value is null.
    /// </summary>

    public static string ToFormattedPrice(this int? value, CultureInfo culture = null)
    {
        return value.HasValue ? value.Value.ToFormattedPrice(culture) : string.Empty;
    }

    /// <summary>
    /// Formats any number (integer or decimal) by inserting a thousand separator every three digits.
    /// Keeps the original decimal places unchanged unless specified.
    /// </summary>

    public static string ToThousandSeparated(this decimal value, int decimalPlaces = 2, CultureInfo culture = null)
    {
        culture ??= CultureInfo.CurrentCulture;
        return value.ToString($"N{decimalPlaces}", culture);
    }

    /// <summary>
    /// Formats a nullable decimal number by inserting a thousand separator every three digits.
    /// Returns an empty string if the value is null.
    /// </summary>

    public static string ToThousandSeparated(this decimal? value, int decimalPlaces = 2, CultureInfo culture = null)
    {
        return value.HasValue ? value.Value.ToThousandSeparated(decimalPlaces, culture) : string.Empty;
    }

    /// <summary>
    /// Formats an integer by inserting a thousand separator every three digits.
    /// </summary>

    public static string ToThousandSeparated(this float value, CultureInfo culture = null)
    {
        culture ??= CultureInfo.CurrentCulture;
        return value.ToString("N0", culture);
    }

    /// <summary>
    /// Formats an integer by inserting a thousand separator every three digits.
    /// </summary>

    public static string ToThousandSeparated(this int value, CultureInfo culture = null)
    {
        culture ??= CultureInfo.CurrentCulture;
        return value.ToString("N0", culture);
    }

    /// <summary>
    /// Formats a nullable integer by inserting a thousand separator every three digits.
    /// Returns an empty string if the value is null.
    /// </summary>

    public static string ToThousandSeparated(this int? value, CultureInfo culture = null)
    {
        return value.HasValue ? value.Value.ToThousandSeparated(culture) : string.Empty;
    }

    /// <summary>
    /// Formats a nullable integer by inserting a thousand separator every three digits.
    /// Returns an empty string if the value is null.
    /// </summary>

    public static string ToThousandSeparated(this long? value, CultureInfo culture = null)
    {
        return value.HasValue ? value.Value.ToThousandSeparated(culture) : string.Empty;
    }

    /// <summary>
    /// Formats an integer by inserting a thousand separator every three digits.
    /// </summary>

    public static string ToThousandSeparated(this long value, CultureInfo culture = null)
    {
        culture ??= CultureInfo.CurrentCulture;
        return value.ToString("N0", culture);
    }
}

RandomDataGenerator.cs

  • Namespace: CodeBlock.DevKit.Core.Extensions
// Copyright (c) CodeBlock.Dev. All rights reserved.
// For more information visit https://codeblock.dev

namespace CodeBlock.DevKit.Core.Extensions;

/// <summary>
/// Utility class for generating random test data and sample values.
/// Useful for unit testing, development, and demo purposes.
/// </summary>
public static class RandomDataGenerator
{
    private static readonly Random _rand = new Random();
    private const string CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";

    /// <summary>
    /// Generates a random string of specified length using alphanumeric characters.
    /// </summary>
    /// <param name="length">Length of the random string (default: 8).</param>
    /// <returns>A random alphanumeric string.</returns>

    public static string GetRandomString(int length = 8)
    {
        var stringChars = new char[length];

        for (int i = 0; i < stringChars.Length; i++)
            stringChars[i] = CHARS[_rand.Next(CHARS.Length)];

        return new string(stringChars);
    }

    /// <summary>
    /// Generates a random integer within the specified range.
    /// </summary>
    /// <param name="min">Minimum value (inclusive).</param>
    /// <param name="max">Maximum value (exclusive).</param>
    /// <returns>A random integer.</returns>

    public static int GetRandomInt(int min = int.MinValue, int max = int.MaxValue)
    {
        return _rand.Next(min, max);
    }

    /// <summary>
    /// Generates a random long integer within the specified range.
    /// </summary>
    /// <param name="min">Minimum value (inclusive).</param>
    /// <param name="max">Maximum value (exclusive).</param>
    /// <returns>A random long integer.</returns>

    public static long GetRandomLong(int min = int.MinValue, int max = int.MaxValue)
    {
        return _rand.Next(min, max);
    }

    /// <summary>
    /// Generates a random numeric string of specified length.
    /// </summary>
    /// <param name="length">Length of the numeric string.</param>
    /// <returns>A random numeric string.</returns>

    public static string GetRandomNumber(int length)
    {
        string randomNumber = "";

        for (int i = 0; i < length; i++)
            randomNumber += GetRandomInt(0, 9);

        return randomNumber;
    }

    /// <summary>
    /// Generates a random DateTime within the last year.
    /// </summary>
    /// <returns>A random DateTime from the past year.</returns>

    public static DateTime GetRandomDateTime()
    {
        DateTime start = DateTime.Now.AddYears(-1);
        DateTime end = DateTime.Now;
        int range = (end - start).Days;
        return start.AddDays(_rand.Next(range));
    }
}

DataAnnotationExtension.cs

  • Namespace: CodeBlock.DevKit.Core.Extensions
// Copyright (c) CodeBlock.Dev. All rights reserved.
// For more information visit https://codeblock.dev

using System.ComponentModel.DataAnnotations;
using System.Reflection;

namespace CodeBlock.DevKit.Core.Extensions;

/// <summary>
/// Extension methods for data annotations and validation.
/// Provides utilities for object validation and enum display name retrieval.
/// </summary>
public static class DataAnnotationExtension
{
    /// <summary>
    /// Validates an object using data annotations and returns validation results.
    /// </summary>
    /// <typeparam name="TObject">The type of object to validate.</typeparam>
    /// <param name="obj">The object to validate.</param>
    /// <param name="results">Output parameter containing validation results.</param>
    /// <returns>True if validation passes, false otherwise.</returns>

    public static bool Validate<TObject>(this TObject obj, out ICollection<ValidationResult> results)
    {
        results = new List<ValidationResult>();

        return Validator.TryValidateObject(obj, new ValidationContext(obj), results, true);
    }

    /// <summary>
    /// Gets the display name of an enum value from its Display attribute.
    /// </summary>
    /// <param name="enumValue">The enum value to get the display name for.</param>
    /// <returns>The display name from the Display attribute, or the enum name if no attribute is found.</returns>

    public static string GetEnumDisplayName(this Enum enumValue)
    {
        return enumValue.GetType().GetMember(enumValue.ToString()).First().GetCustomAttribute<DisplayAttribute>().GetName();
    }
}

Helpers (CodeBlock.DevKit.Core.Helpers)

Result and Result<T> are the standard success/failure envelope for commands and queries (see Exception handling).

Result.cs

  • Namespace: CodeBlock.DevKit.Core.Helpers
// Copyright (c) CodeBlock.Dev. All rights reserved.
// For more information visit https://codeblock.dev

namespace CodeBlock.DevKit.Core.Helpers;

/// <summary>
/// Defines the contract for operation results with success status, messages, and error collections.
/// Used throughout the application for consistent error handling and result reporting.
/// </summary>
public interface IResult
{
    public bool IsSuccess { get; set; }

    public string Message { get; set; }

    public List<string> Errors { get; set; }
}

/// <summary>
/// Represents the result of an operation without a return value.
/// Provides success status, message, and error collection for operation feedback.
/// </summary>
public struct Result : IResult
{
    public bool IsSuccess { get; set; }
    public string Message { get; set; }

    public List<string> Errors { get; set; }

    /// <summary>
    /// Creates a new Result instance.
    /// </summary>
    /// <param name="isSuccess">Whether the operation was successful.</param>
    /// <param name="message">Optional message describing the result.</param>
    /// <param name="errors">Optional collection of error messages.</param>
    public Result(bool isSuccess, string message, List<string> errors)
    {
        IsSuccess = isSuccess;
        Message = message;
        Errors = errors ?? new List<string>();
    }

    /// <summary>
    /// Creates a successful result with an optional message.
    /// </summary>
    /// <param name="message">Optional success message.</param>
    /// <returns>A successful Result instance.</returns>

    public static Result Success(string message = "")
    {
        return new Result(true, message, default);
    }

    /// <summary>
    /// Creates a successful result with a value and optional message.
    /// </summary>
    /// <typeparam name="TValue">The type of the result value.</typeparam>
    /// <param name="value">The result value.</param>
    /// <param name="message">Optional success message.</param>
    /// <returns>A successful Result&lt;TValue&gt; instance.</returns>

    public static Result<TValue> Success<TValue>(TValue value = default, string message = "")
    {
        return new Result<TValue>(true, message, value, default);
    }

    /// <summary>
    /// Creates a failed result with optional errors and message.
    /// </summary>
    /// <param name="errors">Optional collection of error messages.</param>
    /// <param name="message">Optional failure message.</param>
    /// <returns>A failed Result instance.</returns>

    public static Result Failure(List<string> errors = default, string message = "")
    {
        return new Result(false, message, errors);
    }

    /// <summary>
    /// Creates a failed result with a value type and optional errors and message.
    /// </summary>
    /// <typeparam name="TValue">The type of the result value.</typeparam>
    /// <param name="errors">Optional collection of error messages.</param>
    /// <param name="message">Optional failure message.</param>
    /// <returns>A failed Result&lt;TValue&gt; instance.</returns>

    public static Result<TValue> Failure<TValue>(List<string> errors = default, string message = "")
    {
        return new Result<TValue>(false, message, default, errors);
    }

    /// <summary>
    /// Creates a failed result with a single error and optional message.
    /// </summary>
    /// <typeparam name="TValue">The type of the result value.</typeparam>
    /// <param name="error">The error message.</param>
    /// <param name="message">Optional failure message.</param>
    /// <returns>A failed Result&lt;TValue&gt; instance.</returns>

    public static Result<TValue> Failure<TValue>(string error, string message = "")
    {
        var errors = new List<string> { error };
        return new Result<TValue>(false, message, default, errors);
    }
}

/// <summary>
/// Represents the result of an operation with a return value.
/// Provides success status, message, return value, and error collection.
/// </summary>
/// <typeparam name="TValue">The type of the result value.</typeparam>
public struct Result<TValue> : IResult
{
    public bool IsSuccess { get; set; }
    public string Message { get; set; }
    public List<string> Errors { get; set; }
    public TValue Value { get; set; }

    /// <summary>
    /// Creates a new Result&lt;TValue&gt; instance.
    /// </summary>
    /// <param name="isSuccess">Whether the operation was successful.</param>
    /// <param name="message">Optional message describing the result.</param>
    /// <param name="value">The result value.</param>
    /// <param name="errors">Optional collection of error messages.</param>
    public Result(bool isSuccess, string message, TValue value, List<string> errors)
    {
        IsSuccess = isSuccess;
        Message = message;
        Value = value;
        Errors = errors ?? new List<string>();
    }
}

CommandResult.cs

  • Namespace: CodeBlock.DevKit.Core.Helpers
// Copyright (c) CodeBlock.Dev. All rights reserved.
// For more information visit https://codeblock.dev

namespace CodeBlock.DevKit.Core.Helpers;

/// <summary>
/// Represents the result of a command operation with entity ID and message.
/// Used for CQRS pattern to return command execution results and affected entity information.
/// </summary>
public class CommandResult
{
    /// <summary>
    /// Creates a new CommandResult instance.
    /// </summary>
    /// <param name="message">Optional message describing the command result.</param>
    /// <param name="entityId">Optional ID of the affected entity.</param>
    private CommandResult(string message = "", string entityId = "")
    {
        EntityId = entityId;
        Message = message;
    }

    /// <summary>
    /// Creates a new CommandResult instance.
    /// </summary>
    /// <param name="message">Optional message describing the command result.</param>
    /// <param name="entityId">Optional ID of the affected entity.</param>
    /// <returns>A new CommandResult instance.</returns>

    public static CommandResult Create(string message = "", string entityId = "")
    {
        return new CommandResult(message, entityId);
    }

    /// <summary>
    /// Gets the ID of the affected entity.
    /// </summary>
    public string EntityId { get; }

    /// <summary>
    /// Gets the message describing the command result.
    /// </summary>
    public string Message { get; }
}

SortOrder.cs

  • Namespace: CodeBlock.DevKit.Core.Helpers
// Copyright (c) CodeBlock.Dev. All rights reserved.
// For more information visit https://codeblock.dev

using System.ComponentModel.DataAnnotations;
using CodeBlock.DevKit.Core.Resources;

namespace CodeBlock.DevKit.Core.Helpers;

/// <summary>
/// Defines sort order options for data queries and display.
/// Supports localization through resource files for UI display.
/// </summary>
public enum SortOrder
{
    [Display(Name = nameof(CoreResource.SortOrder_Desc), ResourceType = typeof(CoreResource))]
    Desc = 0,

    [Display(Name = nameof(CoreResource.SortOrder_Asc), ResourceType = typeof(CoreResource))]
    Asc = 1,
}

QueryOptions.cs

  • Namespace: CodeBlock.DevKit.Core.Helpers
// Copyright (c) CodeBlock.Dev. All rights reserved.
// For more information visit https://codeblock.dev

namespace CodeBlock.DevKit.Core.Helpers;

/// <summary>
/// Configuration options for query operations including caching settings.
/// Used to control caching behavior and performance optimization for data queries.
/// </summary>
public class QueryOptions
{
    /// <summary>
    /// Initializes a new instance of QueryOptions.
    /// If cacheTimeInSeconds is null, the global 'CacheTimeInSeconds' from settings will be applied.
    /// </summary>
    /// <param name="enableCache">Whether to enable caching for this query.</param>
    /// <param name="cacheTimeInSeconds">Optional cache duration in seconds.</param>

    public QueryOptions(bool enableCache = false, int? cacheTimeInSeconds = null)
    {
        EnableCache = enableCache;
        CacheTimeInSeconds = cacheTimeInSeconds;
    }

    /// <summary>
    /// Gets whether caching is enabled for this query.
    /// </summary>
    public bool EnableCache { get; }

    /// <summary>
    /// Gets the cache duration in seconds, or null to use global settings.
    /// </summary>
    public int? CacheTimeInSeconds { get; }
}

InputValidator.cs

  • Namespace: CodeBlock.DevKit.Core.Helpers
// Copyright (c) CodeBlock.Dev. All rights reserved.
// For more information visit https://codeblock.dev

using System.Text.RegularExpressions;
using CodeBlock.DevKit.Core.Extensions;

namespace CodeBlock.DevKit.Core.Helpers;

/// <summary>
/// Static utility class for input validation operations.
/// Provides validation methods for common input types like email and mobile numbers.
/// </summary>
public static class InputValidator
{
    /// <summary>
    /// Validates if the string is a valid email address.
    /// </summary>
    /// <param name="email">The email string to validate.</param>
    /// <returns>True if the email is valid, false otherwise.</returns>

    public static bool IsValidEmail(this string email)
    {
        if (email.IsNullOrEmptyOrWhiteSpace())
            return false;

        var emailRegex = new Regex(@"^[^@\s]+@[^@\s]+\.[^@\s]+$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
        return emailRegex.IsMatch(email);
    }

    /// <summary>
    /// Validates if the string is a valid mobile number in E.164 format.
    /// </summary>
    /// <param name="mobile">The mobile number string to validate.</param>
    /// <returns>True if the mobile number is valid, false otherwise.</returns>

    public static bool IsValidMobile(this string mobile)
    {
        if (mobile.IsNullOrEmptyOrWhiteSpace())
            return false;

        // E.164 regex: Starts with + followed by 1-3 digit country code and 5-15 digits
        var regex = new Regex(@"^\+[1-9]\d{1,14}$");
        return regex.IsMatch(mobile);
    }
}

Serializer.cs

  • Namespace: CodeBlock.DevKit.Core.Helpers
// Copyright (c) CodeBlock.Dev. All rights reserved.
// For more information visit https://codeblock.dev

using System.Collections;
using System.Reflection;
using CodeBlock.DevKit.Core.Attributes;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace CodeBlock.DevKit.Core.Helpers;

/// <summary>
/// Utility class for JSON serialization with sensitive data redaction support.
/// Automatically redacts properties marked with [DoNotLog] attribute for security.
/// </summary>
public static class Serializer
{
    /// <summary>
    /// Serializes an object to JSON with indented formatting.
    /// </summary>
    /// <param name="obj">The object to serialize.</param>
    /// <returns>A JSON string representation of the object.</returns>

    public static string Serialize(object obj)
    {
        return JsonConvert.SerializeObject(obj, Formatting.Indented);
    }

    /// <summary>
    /// Serializes an object to JSON while redacting sensitive properties marked with [DoNotLog].
    /// </summary>
    /// <param name="obj">The object to serialize with redaction.</param>
    /// <returns>A JSON string with sensitive data redacted.</returns>

    public static string RedactSensitiveData(object obj)
    {
        if (obj == null)
            return null;

        try
        {
            var jToken = JToken.FromObject(obj);
            RedactToken(jToken, obj);
            return jToken.ToString(Formatting.Indented);
        }
        catch (Exception)
        {
            // If serialization fails, try to manually redact using reflection before serializing
            try
            {
                return RedactUsingReflection(obj);
            }
            catch
            {
                // Last resort: serialize without redaction (should be rare)
                return JsonConvert.SerializeObject(obj, Formatting.Indented);
            }
        }
    }

    /// <summary>
    /// Recursively processes JToken to redact sensitive properties.
    /// </summary>
    /// <param name="token">The JToken to process.</param>
    /// <param name="obj">The original object being processed.</param>
    private static void RedactToken(JToken token, object obj)
    {
        if (token is JObject jObject)
        {
            var type = obj.GetType();
            foreach (var property in type.GetProperties(BindingFlags.Public | BindingFlags.Instance))
            {
                var propName = property.Name;
                var value = property.GetValue(obj);
                var jProp = jObject[propName];

                if (jProp == null)
                    continue;

                // Always redact properties marked with [DoNotLog] attribute
                if (property.GetCustomAttribute<DoNotLogAttribute>(inherit: true) != null)
                {
                    jObject[propName] = "***REDACTED***";
                    continue; // Skip recursive processing for redacted properties
                }

                // Recursively process complex types
                if (value != null && !IsSimpleType(property.PropertyType))
                {
                    RedactToken(jProp, value);
                }
            }
        }
        else if (token is JArray jArray && obj is IEnumerable enumerable)
        {
            var enumerator = enumerable.GetEnumerator();
            int index = 0;
            while (enumerator.MoveNext() && index < jArray.Count)
            {
                var current = enumerator.Current;
                if (current != null && !IsSimpleType(current.GetType()))
                {
                    RedactToken(jArray[index], current);
                }
                index++;
            }
        }
    }

    /// <summary>
    /// Fallback method to redact sensitive data using reflection when JToken serialization fails.
    /// Creates a redacted copy of the object before serialization.
    /// </summary>
    /// <param name="obj">The object to redact and serialize.</param>
    /// <returns>A JSON string with sensitive data redacted.</returns>
    private static string RedactUsingReflection(object obj)
    {
        if (obj == null)
            return "null";

        var type = obj.GetType();

        // For simple types, return as-is
        if (IsSimpleType(type))
            return JsonConvert.SerializeObject(obj, Formatting.Indented);

        // For collections, process each item
        if (obj is IEnumerable enumerable && !(obj is string))
        {
            var list = new List<object>();
            foreach (var item in enumerable)
            {
                if (item == null || IsSimpleType(item.GetType()))
                {
                    list.Add(item);
                }
                else
                {
                    // Recursively redact complex items
                    var redactedJson = RedactUsingReflection(item);
                    list.Add(JsonConvert.DeserializeObject(redactedJson));
                }
            }
            return JsonConvert.SerializeObject(list, Formatting.Indented);
        }

        // For complex objects, build a dictionary with redacted values
        var result = new Dictionary<string, object>();

        foreach (var property in type.GetProperties(BindingFlags.Public | BindingFlags.Instance))
        {
            var propName = property.Name;
            var value = property.GetValue(obj);

            // Always redact properties marked with [DoNotLog]
            if (property.GetCustomAttribute<DoNotLogAttribute>(inherit: true) != null)
            {
                result[propName] = "***REDACTED***";
            }
            else if (value == null)
            {
                result[propName] = null;
            }
            else if (IsSimpleType(property.PropertyType))
            {
                result[propName] = value;
            }
            else
            {
                // Recursively redact complex properties
                var redactedJson = RedactUsingReflection(value);
                result[propName] = JsonConvert.DeserializeObject(redactedJson);
            }
        }

        return JsonConvert.SerializeObject(result, Formatting.Indented);
    }

    /// <summary>
    /// Determines if a type is a simple type that doesn't need recursive processing.
    /// </summary>
    /// <param name="type">The type to check.</param>
    /// <returns>True if the type is simple, false otherwise.</returns>
    private static bool IsSimpleType(Type type)
    {
        return type.IsPrimitive
            || type.IsEnum
            || type.Equals(typeof(string))
            || type.Equals(typeof(decimal))
            || type.Equals(typeof(DateTime))
            || type.Equals(typeof(DateTimeOffset))
            || type.Equals(typeof(TimeSpan))
            || type.Equals(typeof(Guid));
    }
}

Validation attributes (CodeBlock.DevKit.Core.Attributes)

ValidateEmailAttribute.cs

  • Namespace: CodeBlock.DevKit.Core.Attributes
// Copyright (c) CodeBlock.Dev. All rights reserved.
// For more information visit https://codeblock.dev

using System.ComponentModel.DataAnnotations;
using CodeBlock.DevKit.Core.Helpers;

namespace CodeBlock.DevKit.Core.Attributes;

/// <summary>
/// Custom validation attribute for email addresses with enhanced error message formatting.
/// Provides better user experience by using display names in error messages.
/// </summary>

public class ValidateEmailAttribute : ValidationAttribute
{
    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        if (value is string email && email.IsValidEmail())
        {
            return ValidationResult.Success;
        }

        // Retrieve the display name from the context, if available
        var displayName = validationContext.DisplayName;

        // Format the error message using the display name, replacing {0} with it
        var errorMessage = string.Format(ErrorMessageString ?? "{0} is not valid", displayName);

        return new ValidationResult(errorMessage);
    }
}

ValidateMobileAttribute.cs

  • Namespace: CodeBlock.DevKit.Core.Attributes
// Copyright (c) CodeBlock.Dev. All rights reserved.
// For more information visit https://codeblock.dev

using System.ComponentModel.DataAnnotations;
using CodeBlock.DevKit.Core.Helpers;

namespace CodeBlock.DevKit.Core.Attributes;

/// <summary>
/// Custom validation attribute for mobile phone numbers using E.164 format.
/// Validates that the input follows international mobile number standards.
/// </summary>

public class ValidateMobileAttribute : ValidationAttribute
{
    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        if (value is string mobile && mobile.IsValidMobile())
            return ValidationResult.Success;

        // Retrieve the display name from the context, if available
        var displayName = validationContext.DisplayName;

        // Format the error message using the display name, replacing {0} with it
        var errorMessage = string.Format(ErrorMessageString ?? "{0} is not valid", displayName);

        return new ValidationResult(errorMessage);
    }
}

ValidateEmailOrMobileAttribute.cs

  • Namespace: CodeBlock.DevKit.Core.Attributes
// Copyright (c) CodeBlock.Dev. All rights reserved.
// For more information visit https://codeblock.dev

using System.ComponentModel.DataAnnotations;
using CodeBlock.DevKit.Core.Helpers;

namespace CodeBlock.DevKit.Core.Attributes;

/// <summary>
/// Custom validation attribute that accepts either a valid email address or mobile number.
/// Use this when a field can contain either format (e.g., contact information fields).
/// </summary>

public class ValidateEmailOrMobileAttribute : ValidationAttribute
{
    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        if (value is string email && email.IsValidEmail())
            return ValidationResult.Success;

        if (value is string mobile && mobile.IsValidMobile())
            return ValidationResult.Success;

        // Retrieve the display name from the context, if available
        var displayName = validationContext.DisplayName;

        // Format the error message using the display name, replacing {0} with it
        var errorMessage = string.Format(ErrorMessageString ?? "{0} is not valid", displayName);

        return new ValidationResult(errorMessage);
    }
}
  • Logging — HTTP and MediatR logging, [DoNotLog], and sensitive payload markers.
  • Exception handlingResult and pipeline errors.
  • General services — dispatcher and notifications that surface Result.Errors.