Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
443 changes: 443 additions & 0 deletions docs/generators/singleton.md

Large diffs are not rendered by default.

26 changes: 26 additions & 0 deletions src/PatternKit.Examples/SingletonGeneratorDemo/AppClock.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using PatternKit.Generators.Singleton;

namespace PatternKit.Examples.SingletonGeneratorDemo;

/// <summary>
/// Application clock singleton using eager initialization.
/// Provides a consistent time source throughout the application,
/// which is especially useful for testing (can be mocked).
/// </summary>
[Singleton] // Defaults to eager initialization
public partial class AppClock
{
/// <summary>Gets the current UTC time.</summary>
public DateTime UtcNow => DateTime.UtcNow;

/// <summary>Gets the current local time.</summary>
public DateTimeOffset Now => DateTimeOffset.Now;

/// <summary>Gets the current date (UTC).</summary>
public DateOnly Today => DateOnly.FromDateTime(DateTime.UtcNow);

/// <summary>Gets the Unix timestamp in seconds.</summary>
public long UnixTimestamp => DateTimeOffset.UtcNow.ToUnixTimeSeconds();

private AppClock() { }
}
57 changes: 57 additions & 0 deletions src/PatternKit.Examples/SingletonGeneratorDemo/ConfigManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using PatternKit.Generators.Singleton;

namespace PatternKit.Examples.SingletonGeneratorDemo;

/// <summary>
/// Configuration manager singleton using lazy initialization with custom factory.
/// Demonstrates real-world pattern for managing application configuration.
/// </summary>
[Singleton(Mode = SingletonMode.Lazy)]
public partial class ConfigManager
{
/// <summary>Gets the application name.</summary>
public string AppName { get; }

/// <summary>Gets the current environment (Development, Staging, Production).</summary>
public string Environment { get; }

/// <summary>Gets the database connection string.</summary>
public string ConnectionString { get; }

/// <summary>Gets whether debug logging is enabled.</summary>
public bool DebugLogging { get; }

/// <summary>Gets the configuration load timestamp.</summary>
public DateTime LoadedAt { get; }

private ConfigManager(string appName, string environment, string connectionString, bool debugLogging)
{
AppName = appName;
Environment = environment;
ConnectionString = connectionString;
DebugLogging = debugLogging;
LoadedAt = DateTime.UtcNow;
}

/// <summary>
/// Factory method that loads configuration from environment variables.
/// In a real application, this might read from a config file or service.
/// </summary>
[SingletonFactory]
private static ConfigManager Create()
{
// In a real app, this would read from appsettings.json, environment, etc.
var appName = System.Environment.GetEnvironmentVariable("APP_NAME") ?? "PatternKitDemo";
var environment = System.Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Development";
var connectionString = System.Environment.GetEnvironmentVariable("CONNECTION_STRING") ?? "Server=localhost;Database=Demo";
var debugLogging = System.Environment.GetEnvironmentVariable("DEBUG_LOGGING") == "true";

return new ConfigManager(appName, environment, connectionString, debugLogging);
}

/// <summary>
/// Returns a string representation of the current configuration.
/// </summary>
public override string ToString() =>
$"ConfigManager[App={AppName}, Env={Environment}, Debug={DebugLogging}, LoadedAt={LoadedAt:HH:mm:ss}]";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
using PatternKit.Generators.Singleton;
using System.Collections.Concurrent;

namespace PatternKit.Examples.SingletonGeneratorDemo;

/// <summary>
/// Simple service registry singleton demonstrating thread-safe lazy initialization.
/// In production, consider using a proper DI container like Microsoft.Extensions.DependencyInjection.
/// </summary>
[Singleton(Mode = SingletonMode.Lazy, Threading = SingletonThreading.ThreadSafe)]
public partial class ServiceRegistry
{
// Use Lazy<object> for thread-safe single-call factory semantics
private readonly ConcurrentDictionary<Type, Lazy<object>> _services = new();

private ServiceRegistry() { }

/// <summary>
/// Registers a service instance.
/// </summary>
public void Register<TService>(TService service) where TService : class
{
ArgumentNullException.ThrowIfNull(service);
_services[typeof(TService)] = new Lazy<object>(() => service);
}

/// <summary>
/// Registers a factory for lazy service creation.
/// The factory is guaranteed to be called at most once, even under concurrent access.
/// </summary>
public void RegisterFactory<TService>(Func<TService> factory) where TService : class
{
ArgumentNullException.ThrowIfNull(factory);
_services[typeof(TService)] = new Lazy<object>(() => factory());
}

/// <summary>
/// Resolves a registered service.
/// </summary>
/// <exception cref="InvalidOperationException">Service not registered.</exception>
public TService Resolve<TService>() where TService : class
{
var type = typeof(TService);

if (_services.TryGetValue(type, out var lazy))
{
return (TService)lazy.Value;
}

throw new InvalidOperationException($"Service {type.Name} is not registered.");
}

/// <summary>
/// Tries to resolve a service, returning null if not found.
/// </summary>
public TService? TryResolve<TService>() where TService : class
{
var type = typeof(TService);

if (_services.TryGetValue(type, out var lazy))
{
return (TService)lazy.Value;
}

return null;
}

/// <summary>
/// Checks if a service is registered.
/// </summary>
public bool IsRegistered<TService>() where TService : class
{
return _services.ContainsKey(typeof(TService));
}

/// <summary>
/// Clears all registrations. Useful for testing.
/// </summary>
public void Clear()
{
_services.Clear();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
namespace PatternKit.Generators.Singleton;

/// <summary>
/// Marks a partial class or record class for Singleton pattern code generation.
/// Generates a thread-safe singleton instance accessor with configurable initialization mode.
/// </summary>
/// <remarks>
/// The generator supports two initialization modes:
/// <list type="bullet">
/// <item><description>Eager: Instance is created when the type is first accessed (static field initializer)</description></item>
/// <item><description>Lazy: Instance is created on first access to the Instance property</description></item>
/// </list>
/// For Lazy mode, thread-safety is configurable via the <see cref="Threading"/> property.
/// </remarks>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public sealed class SingletonAttribute : Attribute
{
/// <summary>
/// Gets or sets the singleton initialization mode.
/// Default is <see cref="SingletonMode.Eager"/>.
/// </summary>
public SingletonMode Mode { get; set; } = SingletonMode.Eager;

/// <summary>
/// Gets or sets the threading model for singleton access.
/// Default is <see cref="SingletonThreading.ThreadSafe"/>.
/// Only applies when <see cref="Mode"/> is <see cref="SingletonMode.Lazy"/>.
/// </summary>
public SingletonThreading Threading { get; set; } = SingletonThreading.ThreadSafe;

/// <summary>
/// Gets or sets the name of the generated singleton instance property.
/// Default is "Instance".
/// </summary>
public string InstancePropertyName { get; set; } = "Instance";
}

/// <summary>
/// Specifies when the singleton instance is created.
/// </summary>
public enum SingletonMode
{
/// <summary>
/// Instance is created when the type is first accessed.
/// Uses a static field initializer for simple, thread-safe initialization.
/// </summary>
Eager = 0,

/// <summary>
/// Instance is created on first access to the Instance property.
/// Uses <see cref="System.Lazy{T}"/> for thread-safe lazy initialization.
/// </summary>
Lazy = 1
}

/// <summary>
/// Specifies the threading model for singleton instance access.
/// </summary>
public enum SingletonThreading
{
/// <summary>
/// Thread-safe singleton access using locks or Lazy&lt;T&gt;.
/// Recommended for most scenarios.
/// </summary>
ThreadSafe = 0,

/// <summary>
/// No thread synchronization. Faster but only safe in single-threaded scenarios.
/// WARNING: May result in multiple instance creation if accessed from multiple threads.
/// </summary>
SingleThreadedFast = 1
}

/// <summary>
/// Marks a static method as the factory for creating the singleton instance.
/// The method must be static, parameterless, and return the containing type.
/// </summary>
/// <remarks>
/// Use this when the singleton requires custom initialization logic
/// beyond what a parameterless constructor can provide.
/// Only one method in a type may be marked with this attribute.
/// </remarks>
/// <example>
/// <code>
/// [Singleton(Mode = SingletonMode.Lazy)]
/// public partial class ConfigManager
/// {
/// private ConfigManager(string path) { }
///
/// [SingletonFactory]
/// private static ConfigManager Create() => new ConfigManager("config.json");
/// }
/// </code>
/// </example>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
public sealed class SingletonFactoryAttribute : Attribute
{
}
11 changes: 10 additions & 1 deletion src/PatternKit.Generators/AnalyzerReleases.Unshipped.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,13 @@ PKPRO007 | PatternKit.Generators.Prototype | Error | DeepCopy strategy not yet i
PKPRO008 | PatternKit.Generators.Prototype | Error | Generic types not supported for Prototype pattern
PKPRO009 | PatternKit.Generators.Prototype | Error | Nested types not supported for Prototype pattern
PKPRO010 | PatternKit.Generators.Prototype | Error | Abstract types not supported for Prototype pattern

PKSNG001 | PatternKit.Generators.Singleton | Error | Type marked with [Singleton] must be partial
PKSNG002 | PatternKit.Generators.Singleton | Error | Singleton type must be a class
PKSNG003 | PatternKit.Generators.Singleton | Error | No usable constructor or factory method found
PKSNG004 | PatternKit.Generators.Singleton | Error | Multiple [SingletonFactory] methods found
PKSNG005 | PatternKit.Generators.Singleton | Warning | Public constructor detected
PKSNG006 | PatternKit.Generators.Singleton | Error | Instance property name conflicts with existing member
PKSNG007 | PatternKit.Generators.Singleton | Error | Generic types are not supported
PKSNG008 | PatternKit.Generators.Singleton | Error | Nested types are not supported
PKSNG009 | PatternKit.Generators.Singleton | Error | Invalid instance property name
PKSNG010 | PatternKit.Generators.Singleton | Error | Abstract types not supported for Singleton pattern
Loading
Loading