-
Notifications
You must be signed in to change notification settings - Fork 0
OutWit.Common
OutWit.Common is a zero-dependency foundational library for the.NET platform, designed to accelerate the development of robust and high-performance applications. It provides a set of ready-to-use components, utilities, and architectural patterns that solve common development tasks.
The library is designed for broad compatibility, supporting various.NET versions from netstandard2.0 to modern versions like net9.0. This makes it a versatile tool for both new and existing projects.
OutWit.Common is built on several key ideas that define its architecture and API.
-
"Smart Model" Pattern (
ModelBase): The library's core element is theModelBaseabstract class. It serves as a foundation for creating domain models that come with built-in capabilities for deep value comparison, cloning, and debug-friendly string representation. This is not just a collection of utilities but a thoughtful approach to data modeling that reduces boilerplate code and increases reliability. -
Immutability and Safety: The library actively promotes the use of immutable updates through the
With()pattern. Instead of modifying an existing object's state, a copy is created with the necessary changes. This approach, borrowed from functional programming, helps avoid side effects, makes application state more predictable, and simplifies the development of multithreaded systems. -
Fluent and Discoverable API: The extensive use of extension methods allows for creating intuitive and easily readable code. Methods logically chain together in call sequences, making the API more "discoverable" through IntelliSense and lowering the entry barrier for new developers.
-
Performance and Optimization: Despite its rich functionality, the library places a strong emphasis on performance. Caching mechanisms (e.g., for
ToString()generation), parallel data processing (SplitParallel), and efficient binary conversions are used in key areas to ensure high-speed execution of critical operations.
The OutWit.Common library is distributed as a NuGet package. Use one of the following methods to install it in your project.
.NET CLI:
dotnet add package OutWit.Common
Package Manager Console:
Install-Package OutWit.Common
Visual Studio UI:
-
Right-click on your project in Solution Explorer and select "Manage NuGet Packages...".
-
Go to the "Browse" tab.
-
In the search bar, type
OutWit.Common. -
Select the package from the list and click "Install".
The ModelBase class is the cornerstone of the OutWit.Common architecture. It is an abstract foundation for creating domain models (entities, value objects), endowing them with powerful behavior out of the box.
ModelBase is an abstract class that implements INotifyPropertyChanged (by inheriting from NotifyPropertyChangedBase) and ICloneable. Inheriting from ModelBase instead of object gives your classes the following advantages:
-
Value-Based Comparison: The ability to define the logical equivalence of objects.
-
Declarative
ToString(): Simple control over the string representation of an object for logging and debugging. -
Immutable Updates: Safe state modification through the creation of copies.
-
Change Notifications: Integration with data binding mechanisms in UI frameworks.
In C#, the == operator for reference types defaults to checking for reference equality, not object content. ModelBase introduces an abstract method Is(ModelBase modelBase, double tolerance) that allows you to implement a deep, value-based comparison.
Descendants must override this method to compare all significant properties.
Example Is() implementation:
public class UserProfile : ModelBase
{
public int Id { get; set; }
public string Username { get; set; }
public double Rating { get; set; }
public override bool Is(ModelBase modelBase, double tolerance = DEFAULT_TOLERANCE)
{
if (!(modelBase is UserProfile other))
return false;
// Compare integer and string properties
if (!this.Id.Is(other.Id) ||!this.Username.Is(other.Username))
return false;
// Compare floating-point numbers with a tolerance
return this.Rating.Is(other.Rating, tolerance);
}
// Clone() implementation is mandatory
public override ModelBase Clone()
{
return new UserProfile
{
Id = this.Id,
Username = this.Username,
Rating = this.Rating
};
}
}
This example uses the Is() extension methods from ValueUtils to compare primitive types, making the code clean and readable.
Overriding the ToString() method for debugging is a routine task. ModelBase automates this process using the ToStringAttribute. Simply mark the properties that should be included in the output, and the library does the rest.
The attribute has two useful properties:
-
Name: Allows you to specify an alternative name for the property in the output. -
Format: Sets a format string for the property's value (e.g., for dates or numbers).
Example of using [ToString] attribute:
using System;
using OutWit.Common.Abstract;
using OutWit.Common.Attributes;
public class Product : ModelBase
{
// Use the 'Name' property to set a custom label in the output.
[ToString(Name = "ID")]
public int ProductId { get; set; }
// If 'Name' is omitted, the property's actual name is used.
[ToString]
public string Sku { get; set; }
// Use the 'Format' property for standard.NET string formatting.
[ToString(Format = "X8")]
public decimal Price { get; set; }
// This property is NOT decorated, so it will be ignored by ToString().
public int StockQuantity { get; set; }
// Required ModelBase implementations
public override bool Is(ModelBase other, double tolerance = DEFAULT_TOLERANCE)
{
if (other is not Product p) return false;
return p.ProductId == ProductId && p.Sku == Sku && Math.Abs(p.Price - Price) < (decimal)tolerance;
}
public override ModelBase Clone() => new Product
{ ProductId = this.ProductId, Sku = this.Sku, Price = this.Price, StockQuantity = this.StockQuantity };
}
// --- Usage ---
var product = new Product
{
ProductId = 101,
Sku = "OWC-LIB-01",
Price = 29.99m,
StockQuantity = 500
};
// The ToString() method automatically formats the output based on the attributes.
Console.WriteLine(product.ToString());
// Expected Output:
// ID: 101, Sku: OWC-LIB-01, Price: $29.99
To improve performance, the results of attribute analysis are cached for each type, so reflection is performed only once.
Modifying objects in-place (mutation) can lead to hard-to-find bugs, especially in multithreaded or complex systems. OutWit.Common offers a safer alternative through the With() extension method, which implements the immutable update pattern.
The With() method creates a clone of the original object (using its Clone() implementation), modifies the specified property in the clone, and returns the new instance. The original object remains untouched.
Example of using With():
var originalUser = new UserProfile { Id = 1, Username = "user1", Rating = 4.5 };
// Create a new user with an updated rating
var updatedUser = originalUser.With(u => u.Rating, 4.7);
// originalUser has not changed
Console.WriteLine(originalUser.Rating); // Output: 4.5
// updatedUser is a new object with the new value
Console.WriteLine(updatedUser.Rating); // Output: 4.7
// More complex logic can also be used
var complexUpdateUser = originalUser.With(u =>
{
u.Username = "user_updated";
u.Rating += 0.1;
});
This approach makes the data flow in an application explicit and predictable. Instead of tracking who might have changed an object and when, you work with a sequence of immutable states. This simplifies debugging, testing, and implementing features like Undo/Redo.
OutWit.Common provides both specialized collection classes for specific tasks and a rich set of utilities in the form of extension methods for working with standard.NET collection interfaces.
-
EvictingQueue<T>: This is a fixed-size queue that operates on a FIFO (First-In, First-Out) basis. When the queue is full and a new item is added, the oldest item is automatically removed. This is ideal for storing a history of recent actions, message logs, or a cache of recent items.var recentFiles = new EvictingQueue<string>(5); recentFiles.Enqueue("file1.txt"); recentFiles.Enqueue("file2.txt"); //... recentFiles.Enqueue("file6.txt"); // "file1.txt" will be automatically removed -
EvictingBuffer<T>: A more complex structure representing an evicting buffer designed for streaming data (e.g., from a network or file). It allows you to add data (Append), read it (Read), and free up space at the beginning of the buffer (Free) by shifting the remaining data. -
RingBuffer<T>: A ring (or circular) buffer. After reaching the end of the collection, it automatically "wraps around" to the beginning. It is useful for tasks where you need to cycle through a set of items, such as UI themes, background images, or available servers.var themes = new RingBuffer<string>(new { "Light", "Dark", "Blue" }); Console.WriteLine(themes.Current()); // Light Console.WriteLine(themes.Next()); // Dark Console.WriteLine(themes.Next()); // Blue Console.WriteLine(themes.Next()); // Light (again)
The static class CollectionUtils contains numerous extension methods that simplify and enrich work with standard collections (IEnumerable<T>, IDictionary<K,V>, ICollection, etc.).
The table below describes the most useful methods.
| Method Signature | Description | Usage Example |
|---|---|---|
bool Is<T>(this IEnumerable<T> me, IEnumerable<T> other) |
Performs a deep, element-by-element comparison of two sequences. Uses Is() from ModelBase or IComparable for elements. |
list1.Is(list2) |
bool Check(this IDictionary me, IDictionary other) |
Performs a deep comparison of two dictionaries, checking keys and values. | dict1.Check(dict2) |
IEnumerable<T> Split<T>(this T me, int chunksCount) |
Splits an array into the specified number of chunks of roughly equal size. | largeArray.Split(4) |
IEnumerable<T> SplitParallel<T>(this T me, int chunksCount) |
Similar to Split, but performs the split in parallel to improve performance on large arrays. |
hugeArray.SplitParallel(Environment.ProcessorCount) |
void AddOrUpdate<K, V>(this IDictionary<K, V> me, K key, V value) |
Adds a key-value pair if the key is absent, or updates the value if the key already exists. | scores.AddOrUpdate("player1", 100); |
void AddOrUpdate<K1, K2, TDict, V>(...) |
Adds or updates a value in a nested dictionary. Automatically creates the inner dictionary if it doesn't exist. | nestedDict.AddOrUpdate(key1, key2, value); |
TValue TryGetValue<K, V>(this IReadOnlyDictionary<K, V> me, K key, V defaultValue) |
Tries to get a value by key. If the key is not found, returns the specified default value. | var setting = config.TryGetValue("theme", "default-theme"); |
TValue FindClosest<TValue>(this IEnumerable<TValue> me, int value, Func<TValue, int> getter) |
Finds the item in a collection whose numeric property is closest to the given value. | prices.FindClosest(99, p => p.Amount) |
ObservableCollection<T> ToObservable<T>(this IEnumerable<T> me) |
Converts any IEnumerable<T> to an ObservableCollection<T>, which is convenient for MVVM. |
myList.ToObservable() |
OutWit.Common includes powerful tools for implementing the Model-View-ViewModel (MVVM) pattern, which is the standard for UI development on platforms like WPF, UWP, MAUI, and Avalonia.
This is a simple abstract class that implements the INotifyPropertyChanged interface. It provides a protected virtual method OnPropertyChanged that encapsulates the logic for raising the PropertyChanged event. Inheriting from this class is the quickest way to make your ViewModels and models compatible with data binding.
public class MyViewModel : NotifyPropertyChangedBase
{
private string _userName;
public string UserName
{
get => _userName;
set
{
_userName = value;
OnPropertyChanged(); // The property name will be automatically inferred
}
}
}
The standard ObservableCollection<T> only notifies the UI of changes to the collection itself (adding, removing, or replacing items). It does not react to changes in the properties within the collection's items.
ObservableCollectionEx<T> solves this problem. This class inherits from ObservableCollection<T> and additionally subscribes to the PropertyChanged event of each item added to it. When an item's property changes,
ObservableCollectionEx<T> raises its own CollectionContentChanged event. This allows the UI to, for example, re-sort or re-filter the list when a property used for sorting or filtering changes.
// ViewModel
public ObservableCollectionEx<TaskItem> Tasks { get; }
//...
// If the IsCompleted property of a TaskItem changes,
// the UI bound to Tasks can react to it thanks to
// the CollectionContentChanged event.
Creating grouped lists in MVVM is a complex task that requires a lot of boilerplate code to manage groups and items. GroupedObservableCollection<T, G> completely automates this process.
This class takes a "flat" list of items and a function (groupValueGetter) that extracts a grouping key from an item. Based on this, GroupedObservableCollection automatically creates and maintains a hierarchical structure—a collection of groups, where each group is an ObservableCollectionEx<T>.
Key Features:
-
Automatic Group Creation: When an item with a new grouping key is added, a new group is automatically created for it.
-
Automatic Group Deletion: When the last item is removed from a group, the empty group is automatically deleted.
-
Reacts to Changes: If an item's property used for grouping changes, the item is automatically moved from one group to another.
Example:
// Model
public class Person : NotifyPropertyChangedBase
{
public string Name { get; set; }
public string Department { get; set; /*... with OnPropertyChanged() */ }
}
// ViewModel
public class StaffViewModel
{
// The source "flat" collection
private ObservableCollectionEx<Person> _allPeople;
// The grouped collection for UI binding
public GroupedObservableCollection<Person, string> PeopleByDepartment { get; }
public StaffViewModel()
{
_allPeople = new ObservableCollectionEx<Person>();
// Group by the Department property
PeopleByDepartment = new GroupedObservableCollection<Person, string>(p => p.Department);
// Link the collections (this step needs to be implemented manually or via a wrapper)
// For example, when adding to _allPeople, also add to PeopleByDepartment
}
}
Using GroupedObservableCollection can significantly reduce ViewModel code by delegating all the complex group management logic to the library.
OutWit.Common contains a large number of static utility classes for solving everyday programming tasks.
Provides extension methods for safe and cross-platform handling of file system paths.
-
AppendPath(string path): Safely combines paths. -
DirectoryName(): Gets the directory name from a path. -
IsFullPath(): Checks if a path is absolute. -
ApplicationDataPath()/ProgramDataPath(): Generates paths in standard system folders (AppData,ProgramData) based on the assembly name. -
AssemblyDirectory(): Returns the directory where the assembly is located. -
CheckFolder(): Checks if a folder exists and creates it if necessary.
Contains high-performance extension methods for converting primitive types (bool, int, double, etc.) and their arrays to and from a byte array (byte). This is extremely useful when working with binary protocols, file formats, or low-level I/O.
int myInts = { 1, 2, 3 };
byte bytes = myInts.ToBytes(); // Convert to bytes
int restoredInts = bytes.ToInt(); // Convert back
Provides reliable Is() extension methods for comparing various data types.
-
Is(this double me, double value, double tolerance): Compares floating-point numbers with a specified tolerance, avoiding precision issues. Similar methods exist forfloatanddecimal. -
Is<T>(this T me, T other): A generic comparison for any type that implementsIComparable<T>. -
Check(this object me, object second): A universal comparison method that automatically selects the correct strategy (forModelBase, collections,IComparable, etc.).
Utilities for generating cryptographically secure random data.
-
RandomString(ushort length, string allowedChars): Generates a random string of a given length from a set of allowed characters. UsesRandomNumberGeneratorto ensure cryptographic strength. -
RandomTempFile(string extension): Generates a unique name for a temporary file in the system's temporary folder.
Simple methods for calculating popular hashes.
-
GetSha256Hash(this string me): Computes the SHA256 hash of a string. -
GetFileMD5Hash(this string filePath): Computes the MD5 hash of a file's content.
Contains advanced helpers for working with INotifyPropertyChanged.
-
FirePropertyChanged<T>(this T me, string propertyName): Allows raising thePropertyChangedevent for an object from outside. It uses caching of compiled expressions for high performance. -
NameOfProperty<T, TResult>(this Expression<Func<T, TResult>> me): A refactoring-safe way to get a property's name from a lambda expression.
-
SearchUtils: Contains binary search methods for sorted lists (IList<int>), such asFindClosestValueIndexandFindLessOrEqualValueIndex. -
StringUtils: Basic string utilities, likeTrimEnd(int nSymbols). -
ExceptionUtils: Helpers for creating exceptions, such asThrowDelegateException.
OutWit.Common offers a flexible and extensible system for managing string resources and application localization. This system is particularly useful when building modular or plugin-based architectures.
The IResources interface defines the basic contract for any resource provider. It requires an indexer to retrieve a string by key (for both the current and a specified culture) and a
ResetCulture method to change the current culture.
The ResourcesBase<T> class is a base implementation of IResources that simplifies the integration of standard.NET resource files (.resx). You create a descendant class, specifying the type generated for your .resx file as the generic parameter, and pass the assembly containing the resource to the constructor.
// File: MyStrings.resx
//...contains the key "HelloWorld" with the value "Hello, World!"
// Code
public class MyProjectResources : ResourcesBase<Resources.MyStrings>
{
public MyProjectResources() : base(Assembly.GetExecutingAssembly())
{
}
}
// Usage
IResources res = new MyProjectResources();
string greeting = res["HelloWorld"]; // "Hello, World!"
The ResourcesMerged class is a key component for creating modular applications. It allows you to combine multiple IResources implementations into a single logical dictionary. When a resource is requested,
ResourcesMerged queries each of the nested dictionaries in sequence until it finds the key.
This allows each module or plugin to provide its own localization files, while the main application works with them through a single, aggregated interface, without knowing the internal structure of each module.
Example for a modular application:
// In Module A:
IResources moduleAResources = new ModuleAResources();
// In Module B:
IResources moduleBResources = new ModuleBResources();
// In the main application:
var allResources = new ResourcesMerged(new { moduleAResources, moduleBResources });
// Now you can request strings from any module through a single object
string stringFromA = allResources["KeyFromModuleA"];
string stringFromB = allResources;
// Changing the language will apply to all modules
allResources.ResetCulture("de-DE");
This approach ensures loose coupling and high extensibility of the localization system.