Table of Contents

Services

This page lists the main public types in CodeBlock.DevKit.Core that teams reuse across layers. Source is embedded from the DevKit repository via DocFX so it stays aligned with the packages you reference.

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);
    }
}