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<TValue> 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<TValue> 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<TValue> 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<TValue> 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);
}
}
Related topics
- Logging — HTTP and MediatR logging,
[DoNotLog], and sensitive payload markers. - Exception handling —
Resultand pipeline errors. - General services — dispatcher and notifications that surface
Result.Errors.