Singleton Generator
Overview
The Singleton Generator creates thread-safe singleton implementations with explicit initialization and threading semantics. It eliminates common singleton footguns like incorrect lazy initialization and double-checked locking bugs.
When to Use
Use the Singleton generator when you need:
- A single instance of a class throughout the application lifecycle
- Explicit initialization timing: Control whether the instance is created eagerly or lazily
- Thread-safety guarantees: Configurable threading model for your use case
- Compile-time safety: The generator validates your singleton at build time
Installation
The generator is included in the PatternKit.Generators package:
dotnet add package PatternKit.Generators
Quick Start
using PatternKit.Generators.Singleton;
[Singleton]
public partial class AppClock
{
public DateTime Now => DateTime.UtcNow;
private AppClock() { }
}
Generated:
public partial class AppClock
{
private static readonly AppClock __PatternKit_Instance = new AppClock();
/// <summary>Gets the singleton instance of this type.</summary>
public static AppClock Instance => __PatternKit_Instance;
}
Usage:
var now = AppClock.Instance.Now;
Initialization Modes
Eager Initialization (Default)
The instance is created when the type is first accessed. This is the simplest and safest approach:
[Singleton] // Mode defaults to SingletonMode.Eager
public partial class Configuration
{
public string ConnectionString { get; }
private Configuration()
{
ConnectionString = Environment.GetEnvironmentVariable("CONNECTION_STRING") ?? "";
}
}
Generated:
private static readonly Configuration __PatternKit_Instance = new Configuration();
public static Configuration Instance => __PatternKit_Instance;
Pros:
- Simple and thread-safe by CLR guarantee
- No runtime overhead on access
- Deterministic initialization
Cons:
- Instance created even if never accessed
- Initialization order depends on type access order
Lazy Initialization
The instance is created on first access to the Instance property:
[Singleton(Mode = SingletonMode.Lazy)]
public partial class ExpensiveService
{
private ExpensiveService()
{
// Expensive initialization here
}
}
Generated (thread-safe):
private static readonly Lazy<ExpensiveService> __PatternKit_LazyInstance =
new Lazy<ExpensiveService>(() => new ExpensiveService());
public static ExpensiveService Instance => __PatternKit_LazyInstance.Value;
Pros:
- Instance only created when actually needed
- Can reduce startup time for rarely-used services
Cons:
- Slight runtime overhead on first access
- Less predictable initialization timing
Threading Options
When using SingletonMode.Lazy, you can configure the threading model:
ThreadSafe (Default)
Uses Lazy<T> which is thread-safe by default:
[Singleton(Mode = SingletonMode.Lazy, Threading = SingletonThreading.ThreadSafe)]
public partial class SafeCache { }
SingleThreadedFast
For scenarios where you guarantee single-threaded access:
[Singleton(Mode = SingletonMode.Lazy, Threading = SingletonThreading.SingleThreadedFast)]
public partial class UiService
{
// Only accessed from UI thread
private UiService() { }
}
Generated:
private static UiService? __PatternKit_Instance;
/// <summary>
/// Gets the singleton instance of this type.
/// WARNING: This implementation is not thread-safe.
/// </summary>
public static UiService Instance => __PatternKit_Instance ??= new UiService();
⚠️ Warning: Only use SingleThreadedFast when you can guarantee single-threaded access. Multi-threaded access may result in multiple instances being created.
Custom Factory Methods
When your singleton needs custom initialization logic, use [SingletonFactory]:
[Singleton(Mode = SingletonMode.Lazy)]
public partial class ConfigManager
{
public string ConfigPath { get; }
private ConfigManager(string path)
{
ConfigPath = path;
}
[SingletonFactory]
private static ConfigManager Create()
{
var path = Environment.GetEnvironmentVariable("CONFIG_PATH") ?? "config.json";
return new ConfigManager(path);
}
}
Factory method requirements:
- Must be
static - Must be parameterless
- Must return the containing type
- Only one factory method per type
Attributes
[Singleton]
Marks a partial class for singleton generation.
| Property | Type | Default | Description |
|---|---|---|---|
Mode |
SingletonMode |
Eager |
When the instance is created |
Threading |
SingletonThreading |
ThreadSafe |
Threading model (for Lazy mode) |
InstancePropertyName |
string |
"Instance" |
Name of the generated property |
[SingletonFactory]
Marks a static method as the factory for creating the singleton instance.
[SingletonFactory]
private static MyService Create() => new MyService();
Supported Types
The generator supports:
| Type | Supported |
|---|---|
partial class |
✅ |
partial record class |
✅ |
struct |
❌ (singleton value semantics are odd) |
interface |
❌ |
Diagnostics
| ID | Severity | Description |
|---|---|---|
| PKSNG001 | Error | Type marked with [Singleton] must be partial |
| PKSNG002 | Error | Singleton type must be a class (not struct/interface) |
| PKSNG003 | Error | No usable constructor or [SingletonFactory] found |
| PKSNG004 | Error | Multiple [SingletonFactory] methods found |
| PKSNG005 | Warning | Public constructor detected; singleton can be bypassed |
| PKSNG006 | Error | Instance property name conflicts with existing member |
| PKSNG007 | Error | Generic types are not supported for singleton generation |
| PKSNG008 | Error | Nested types are not supported for singleton generation |
| PKSNG009 | Error | Invalid instance property name (not a valid C# identifier) |
| PKSNG010 | Error | Abstract types not supported (unless [SingletonFactory] provided) |
Best Practices
1. Make Constructors Private
Always make your constructor private to prevent bypassing the singleton:
// ✅ Good: Private constructor
[Singleton]
public partial class Logger
{
private Logger() { }
}
// ⚠️ Bad: Public constructor (generates PKSNG005 warning)
[Singleton]
public partial class Logger
{
public Logger() { } // Anyone can create instances!
}
2. Prefer Eager Initialization
Unless you have a specific reason for lazy initialization, prefer eager mode:
// ✅ Preferred: Simple, predictable, no overhead
[Singleton]
public partial class AppConfig { }
// Only use lazy when needed
[Singleton(Mode = SingletonMode.Lazy)]
public partial class ExpensiveToCreate { }
3. Avoid Mutable State
Singletons with mutable state can cause subtle bugs:
// ⚠️ Caution: Mutable singleton state
[Singleton]
public partial class Counter
{
public int Value { get; set; } // Shared mutable state
}
// ✅ Better: Immutable or thread-safe state
[Singleton]
public partial class Counter
{
private int _value;
public int Value => _value;
public int Increment() => Interlocked.Increment(ref _value);
}
4. Use Custom Property Names When Needed
Avoid conflicts with existing members:
[Singleton(InstancePropertyName = "Default")]
public partial class Settings
{
// Can't use "Instance" if it already exists
public int Instance { get; set; }
}
Examples
Configuration Manager
using PatternKit.Generators.Singleton;
[Singleton(Mode = SingletonMode.Lazy)]
public partial class ConfigManager
{
public string AppName { get; }
public string Environment { get; }
public string ConnectionString { get; }
private ConfigManager(string appName, string env, string connStr)
{
AppName = appName;
Environment = env;
ConnectionString = connStr;
}
[SingletonFactory]
private static ConfigManager Create()
{
// Load from environment or config file
return new ConfigManager(
Environment.GetEnvironmentVariable("APP_NAME") ?? "MyApp",
Environment.GetEnvironmentVariable("ENVIRONMENT") ?? "Development",
Environment.GetEnvironmentVariable("CONNECTION_STRING") ?? "");
}
}
// Usage
var config = ConfigManager.Instance;
Console.WriteLine($"Running {config.AppName} in {config.Environment}");
Service Locator
[Singleton]
public partial class ServiceLocator
{
private readonly Dictionary<Type, object> _services = new();
private ServiceLocator() { }
public void Register<T>(T service) where T : class
{
_services[typeof(T)] = service;
}
public T Resolve<T>() where T : class
{
return (T)_services[typeof(T)];
}
}
// Usage
ServiceLocator.Instance.Register<ILogger>(new ConsoleLogger());
var logger = ServiceLocator.Instance.Resolve<ILogger>();
Application Clock
[Singleton]
public partial class AppClock
{
public DateTime UtcNow => DateTime.UtcNow;
public DateTimeOffset Now => DateTimeOffset.Now;
private AppClock() { }
}
// Usage - consistent time source throughout app
var timestamp = AppClock.Instance.UtcNow;
Troubleshooting
PKSNG001: Type must be partial
Cause: Missing partial keyword.
Fix:
// ❌ Wrong
[Singleton]
public class MySingleton { }
// ✅ Correct
[Singleton]
public partial class MySingleton { }
PKSNG003: No usable constructor or factory
Cause: Type has no parameterless constructor and no factory method.
Fix: Add a parameterless constructor or factory:
// Option 1: Parameterless constructor
[Singleton]
public partial class MySingleton
{
private MySingleton() { }
}
// Option 2: Factory method
[Singleton]
public partial class MySingleton
{
private MySingleton(string config) { }
[SingletonFactory]
private static MySingleton Create() => new("default.json");
}
PKSNG005: Public constructor warning
Cause: Public constructor allows bypassing singleton.
Fix: Make constructor private:
// ❌ Generates warning
public MySingleton() { }
// ✅ No warning
private MySingleton() { }
See Also
- Memento Generator — For saving/restoring singleton state
- Prototype Generator — For cloning patterns
- GoF: Singleton