A comprehensive collection of .NET utility libraries providing extension methods, helpers, and utilities to simplify everyday development tasks and reduce boilerplate code.
- Overview
- Installation
- Versioning and Releases
- Core Library (Ploch.Common)
- Extended Libraries
- Real-World Examples
Ploch.Common is a suite of .NET libraries targeting netstandard2.0 and net8.0+, offering:
- Rich Extension Methods for strings, collections, types, and more
- Parameter Validation with fluent Guard APIs
- Reflection Utilities for type inspection and manipulation
- I/O Helpers for path operations
- Testing Support with random data generators
- Dependency Injection extensions
- Serialization abstractions (JSON, XML)
- And much more...
# Core library
dotnet add package Ploch.Common
# Additional libraries
dotnet add package Ploch.Common.DependencyInjection
dotnet add package Ploch.Common.Serialization
dotnet add package Ploch.Common.Serialization.SystemTextJson
dotnet add package Ploch.TestingSupportThis project uses Nerdbank.GitVersioning (NBGV) for automatic
version computation from version.json and git commit history.
- Stable releases are published to NuGet.org via the Release workflow
- Prerelease packages are published to GitHub Packages on every push to
master - SourceLink is enabled — step into library source code during debugging
- Symbol packages (
.snupkg) are published to the NuGet symbol server
- Go to Actions → Release workflow → Run workflow
- Enter the release version (e.g.
3.0) — the patch version is computed by NBGV - Optionally enter the next development version (defaults to minor increment)
- The workflow builds, tests, publishes to NuGet.org, creates a GitHub Release, and bumps the version
dotnet tool restore
dotnet nbgv get-versionConvenient extension methods for common string operations.
using Ploch.Common;
string? value = GetValue();
// Check if string is null or empty
if (value.IsNullOrEmpty())
{
// Handle empty string
}
// Check if string is NOT null or empty
if (value.IsNotNullOrEmpty())
{
ProcessValue(value);
}
// Check for whitespace
if (value.IsNullOrWhiteSpace())
{
// Handle whitespace-only string
}
// Convert empty strings to null
string? result = "".NullIfEmpty(); // Returns null
string? result2 = " ".NullIfWhiteSpace(); // Returns nullusing Ploch.Common;
// Parse to integers
string number = "42";
int value = number.ToInt32(); // Returns 42
long bigValue = number.ToInt64(); // Returns 42L
// Safe conversion with TryConvert
if (number.TryConvertToInt32(out int result))
{
Console.WriteLine($"Parsed: {result}");
}
// Base64 encoding/decoding
string original = "Hello, World!";
string encoded = original.ToBase64String(); // Encodes to base64
string decoded = encoded.FromBase64String(); // Decodes backusing Ploch.Common;
// Case-insensitive comparison
string a = "Hello";
string b = "hello";
bool isEqual = a.EqualsIgnoreCase(b); // Returns true
// Replace at start of string
string path = "/api/users";
string newPath = path.ReplaceStart("/api", "/v2/api"); // Returns "/v2/api/users"
// Check if string contains any of the provided values
string text = "Hello World";
bool found = text.ContainsAny("World", "Universe", "Galaxy"); // Returns true
bool found2 = text.ContainsAny(StringComparison.OrdinalIgnoreCase, "WORLD"); // Returns truePowerful extensions for working with IEnumerable, arrays, and collections.
using Ploch.Common.Collections;
int status = 2;
// Check if value is in a set (like SQL IN)
if (status.ValueIn(1, 2, 3, 4))
{
// Status is one of the valid values
}
string role = "Admin";
if (role.ValueIn("Admin", "Manager", "Supervisor"))
{
// User has elevated permissions
}
// With custom comparer
if (name.ValueIn(StringComparer.OrdinalIgnoreCase, "admin", "manager"))
{
// Case-insensitive check
}using Ploch.Common.Collections;
var numbers = new[] { 1, 2, 3, 4, 5 };
// Check if none match a predicate (opposite of Any)
bool noneNegative = numbers.None(n => n < 0); // Returns true
// Join elements into a string
string result = numbers.Join(", "); // Returns "1, 2, 3, 4, 5"
// Join with custom selector
var users = GetUsers();
string names = users.Join(", ", u => u.Name);
// Join with different final separator
var items = new[] { "apples", "oranges", "bananas" };
string list = items.JoinWithFinalSeparator(", ", " and ");
// Returns "apples, oranges and bananas"
// Check if collection is empty
bool isEmpty = numbers.IsEmpty(); // Returns false
bool isNullOrEmpty = ((int[])null).IsNullOrEmpty(); // Returns true
// Get second element
int second = numbers.Second(); // Returns 2
// Exclude specific items
var filtered = numbers.ExceptItems(3, 5); // Returns [1, 2, 4]using Ploch.Common.Collections;
var items = GetItems();
// Conditional filtering with If
var query = items
.OrderBy(x => x.Created)
.If(createdAfter.HasValue, q => q.Where(x => x.Created > createdAfter.Value))
.If(first.HasValue, q => q.Take(first.Value));
// ForEach operation
items.ForEach(item => Console.WriteLine(item.Name));
// Shuffle collection randomly
var shuffled = items.Shuffle();
// Take random items
var randomItems = items.TakeRandom(5);
// Check if integers are sequential
var numbers = new[] { 1, 2, 3, 4, 5 };
bool sequential = numbers.AreIntegersSequentialInOrder(); // Returns trueFluent API for parameter validation with clear error messages.
using Ploch.Common.ArgumentChecking;
public void ProcessUser(User user, string email)
{
// Throws ArgumentNullException if null
user.NotNull(nameof(user));
email.NotNull(nameof(email));
// For reference types - throws InvalidOperationException if null
var name = user.Name.RequiredNotNull(nameof(user.Name));
// With custom error message
var address = user.Address.RequiredNotNull(
nameof(user.Address),
"User {0} must have an address"
);
}using Ploch.Common.ArgumentChecking;
public void CreateAccount(string username, string password, List<string> roles)
{
// Validate string is not null or empty
username.NotNullOrEmpty(nameof(username));
password.NotNullOrEmpty(nameof(password));
// Validate collection is not null or empty
roles.NotNullOrEmpty(nameof(roles));
// RequiredNotNullOrEmpty throws InvalidOperationException
var validatedName = username.RequiredNotNullOrEmpty(
nameof(username),
"Username {0} cannot be empty"
);
}using Ploch.Common.ArgumentChecking;
public void SetTimeout(int milliseconds, OrderStatus status)
{
// Ensure value is positive
milliseconds.Positive(nameof(milliseconds));
// Ensure enum value is defined
status.NotOutOfRange(nameof(status));
// Boolean conditions
var isValid = ValidateInput();
isValid.RequiredTrue("Input validation failed");
}Check if a value exists in a collection (similar to SQL IN operator).
using Ploch.Common;
int statusCode = GetStatusCode();
// Check if value is in collection
if (statusCode.In(200, 201, 204))
{
// Success status codes
}
// Check if NOT in collection
if (statusCode.NotIn(400, 401, 403, 404))
{
// Not a client error
}
// Works with enumerables
var validCodes = new List<int> { 200, 201, 202 };
if (statusCode.In(validCodes))
{
// Valid status
}
// With custom comparer
if (name.In(StringComparer.OrdinalIgnoreCase, "admin", "root"))
{
// Case-insensitive check
}Utilities for type inspection and reflection operations.
using Ploch.Common.Reflection;
Type type = typeof(MyService);
// Check if type implements an interface
bool implements = type.IsImplementing(typeof(IService));
// Check if it's a concrete implementation (not abstract/interface)
bool isConcrete = type.IsConcreteImplementation(typeof(IService));
// Generic version
bool implementsGeneric = type.IsConcreteImplementation<IService>();
// Check for generic interfaces
bool hasGeneric = type.IsImplementing(typeof(IRepository<>), concreteOnly: true);
// Check if type is enumerable
bool isEnumerable = type.IsEnumerable();
// Check if type is nullable
bool isNullable = typeof(int?).IsNullable(); // Returns true
// Check if type is "simple" (primitive, string, decimal, enum, or nullable of these)
bool isSimple = typeof(int).IsSimpleType(); // Returns true
bool isSimple2 = typeof(DateTime).IsSimpleType(); // Returns trueusing Ploch.Common.Reflection;
// Get readable type names for generics
Type listType = typeof(List<int>);
string name = listType.GetReadableTypeName(); // Returns "List<int>"
Type dictType = typeof(Dictionary<string, List<User>>);
string dictName = dictType.GetReadableTypeName();
// Returns "Dictionary<string, List<User>>"
// Works with arrays
Type arrayType = typeof(int[]);
string arrayName = arrayType.GetReadableTypeName(); // Returns "int[]"File path manipulation and validation utilities.
using Ploch.Common.IO;
// Get directory name
string dirPath = "/usr/local/bin";
string name = dirPath.GetDirectoryName(); // Returns "bin"
// Convert to safe file name
string input = "My Document: Version 1.0";
string safe = PathUtils.ToSafeFileName(input);
// Returns "My Document_ Version 1_0"
// Normalize paths
string path = "/some/path/";
string normalized = PathUtils.NormalizePathWithTrailingSeparator(path);
// Returns "/some/path/" with proper separators
string withoutTrailing = PathUtils.NormalizePathWithoutTrailingSeparator(path);
// Returns "/some/path"
// Get relative path
string from = "/home/user/projects";
string to = "/home/user/documents/file.txt";
string relative = PathUtils.GetRelativePath(from, to);
// Returns "../documents/file.txt"
// Manage file extensions
string file = "/path/to/file.txt";
string withNewExt = file.WithExtension(".md");
// Returns "/path/to/file.md"
string withoutExt = PathUtils.GetFullPathWithoutExtension(file);
// Returns "/path/to/file"Generate random data for testing purposes.
using Ploch.Common.Randomizers;
// Get randomizer for a specific type
var stringRandomizer = Randomizer.GetRandomizer<string>();
string randomString = stringRandomizer.GetRandom();
var intRandomizer = Randomizer.GetRandomizer<int>();
int randomInt = intRandomizer.GetRandom();
// With ranges
var rangedInt = (IRangedRandomizer<int>)intRandomizer;
int value = rangedInt.GetRandom(min: 1, max: 100);
var dateRandomizer = Randomizer.GetRandomizer<DateTime>();
DateTime randomDate = dateRandomizer.GetRandom();
// Boolean randomizer
var boolRandomizer = Randomizer.GetRandomizer<bool>();
bool randomBool = boolRandomizer.GetRandom();
// Using runtime type
Type type = typeof(int);
var randomizer = Randomizer.GetRandomizer(type);
object randomValue = randomizer.GetRandom();Extensions for working with dependency injection containers.
using Ploch.Common.DependencyInjection;
// Register services using a service bundle
services.AddServicesBundle<CommandServicesBundle>(configuration);
// Create a services bundle
public class CommandServicesBundle : ServicesBundle
{
public override void ConfigureServices(
IServiceCollection services,
IConfiguration configuration)
{
services.AddTransient<ICommandHandler, DefaultCommandHandler>();
services.AddSingleton<ICommandRegistry, CommandRegistry>();
// Bind configuration
services.Configure<CommandOptions>(
configuration.GetSection("Commands")
);
}
}Abstraction layer for serialization with multiple format support.
using Ploch.Common.Serialization;
// System.Text.Json
using Ploch.Common.Serialization.SystemTextJson;
var serializer = new SystemTextJsonSerializer();
// Serialize
var user = new User { Name = "John", Email = "john@example.com" };
string json = serializer.Serialize(user);
// Deserialize
var deserializedUser = serializer.Deserialize<User>(json);
// Async operations
await serializer.SerializeAsync(user, stream);
var user2 = await serializer.DeserializeAsync<User>(stream);
// Newtonsoft.Json
using Ploch.Common.Serialization.NewtonsoftJson;
var newtonsoftSerializer = new NewtonsoftJsonSerializer();
string json2 = newtonsoftSerializer.Serialize(user);Tools for dynamic assembly scanning and type discovery.
using Ploch.Common.Reflection;
// Configure type loader
var commands = TypeLoader.Configure(cfg => cfg
.WithBaseType<ICommand>()
.FromAssembliesContaining("MyApp")
.ExcludeAssemblies("*.Tests")
).LoadTypes();
// Manual configuration
var typeScanner = new AppDomainTypesLoader(
new TypeLoadingConfiguration(
assemblyName => assemblyName.AddInclude("MyApp.*"),
BaseTypes: [typeof(ICommand), typeof(IMessageHandler)]
)
);
typeScanner.ProcessAllAssemblies();
var loadedTypes = typeScanner.LoadedTypes
.Where(t => t.IsConcreteImplementation<ICommand>());Testing utilities and random data generators.
using Ploch.TestingSupport;
// Base test class with common functionality
public class MyTests : TestFixtureBase
{
[Fact]
public void TestMethod()
{
// Use built-in test helpers
var randomData = GenerateRandomTestData();
// ...
}
}Custom assertions for FluentAssertions.
using Ploch.TestingSupport.FluentAssertions;
// Custom domain-specific assertions
result.Should().BeSuccessful();
user.Should().HaveValidEmail();
collection.Should().ContainItemsMatching(predicate);using Ploch.Common;
using Ploch.Common.ArgumentChecking;
public class UserService
{
public User CreateUser(string email, string password, UserRole role)
{
// Validate parameters
email.NotNullOrEmpty(nameof(email));
password.NotNullOrEmpty(nameof(password));
role.NotOutOfRange(nameof(role));
// Additional validation
if (!email.ContainsAny("@"))
{
throw new ArgumentException("Invalid email format", nameof(email));
}
// Ensure valid role
if (!role.In(UserRole.Admin, UserRole.User, UserRole.Guest))
{
throw new ArgumentException("Invalid role", nameof(role));
}
return new User { Email = email, Role = role };
}
}using Ploch.Common.Reflection;
using Ploch.Common.DependencyInjection;
public static class ServiceConfiguration
{
public static IServiceCollection AddApplicationServices(
this IServiceCollection services)
{
// Discover and register all command handlers
var handlers = TypeLoader.Configure(cfg => cfg
.WithBaseType<ICommandHandler>()
.FromAssembliesContaining("MyApp.Commands")
).LoadTypes();
foreach (var handlerType in handlers)
{
if (handlerType.IsConcreteImplementation<ICommandHandler>())
{
services.AddTransient(typeof(ICommandHandler), handlerType);
}
}
return services;
}
}using Ploch.Common.IO;
using Ploch.Common.ArgumentChecking;
public class FileManager
{
private readonly string _basePath;
public FileManager(string basePath)
{
_basePath = basePath.NotNullOrEmpty(nameof(basePath));
}
public string CreateSafeFilePath(string fileName, string extension)
{
fileName.NotNullOrEmpty(nameof(fileName));
// Create safe file name
string safeFileName = PathUtils.ToSafeFileName(fileName);
// Add extension
string fileWithExt = safeFileName.WithExtension(extension);
// Create full path
string fullPath = Path.Combine(_basePath, fileWithExt);
// Normalize
return PathUtils.NormalizePathWithoutTrailingSeparator(fullPath);
}
public string GetRelativeDocumentPath(string documentPath)
{
documentPath.NotNullOrEmpty(nameof(documentPath));
return PathUtils.GetRelativePath(_basePath, documentPath);
}
}using Ploch.Common.Collections;
public class DataProcessor
{
public ProcessResult ProcessRecords(
IEnumerable<Record> records,
DateTime? afterDate,
RecordStatus? status,
int? maxResults)
{
// Validate input
if (records.IsNullOrEmpty())
{
return ProcessResult.Empty;
}
// Build query with conditional filters
var query = records
.OrderBy(r => r.CreatedDate)
.If(afterDate.HasValue, q => q.Where(r => r.CreatedDate > afterDate.Value))
.If(status.HasValue, q => q.Where(r => r.Status == status.Value))
.If(maxResults.HasValue, q => q.Take(maxResults.Value));
var processed = query
.ForEach(r => r.MarkAsProcessed())
.ToList();
// Generate summary
var statuses = processed.Join(", ", r => r.Status);
return new ProcessResult
{
Count = processed.Count,
Summary = $"Processed {processed.Count} records with statuses: {statuses}"
};
}
}using Ploch.Common;
using Ploch.Common.ArgumentChecking;
public class AppConfiguration
{
public static T GetRequiredConfig<T>(
IConfiguration configuration,
string sectionName)
{
sectionName.NotNullOrEmpty(nameof(sectionName));
configuration.NotNull(nameof(configuration));
var section = configuration.GetSection(sectionName);
var config = section.Get<T>();
return config.RequiredNotNull(
nameof(config),
$"Configuration section '{sectionName}' is required but was not found"
);
}
}[Your License Here]
Contributions are welcome! Please feel free to submit a Pull Request.
For issues, questions, or contributions, please visit the GitHub repository.