Skip to content

Generator: Create Factory Pattern #28

Description

@JerrettDavis

Feature: Factory Pattern Source Generator

A source generator that produces boilerplate free, GoF consistent factory implementations without requiring PatternKit at runtime. This generator lives in PatternKit.Generators and allows developers to implement Factory Method and simple Abstract Factory patterns using attributes. The generator emits idiomatic C# that is easy to maintain and entirely self contained.

Two factory models are supported:

  1. Keyed Factory Methods
  2. Creator Based Type Factories (GoF Factory Method)

Both models support synchronous and asynchronous variants, with ValueTask favored when generating async code.


Goals

  • Eliminate factory boilerplate.
  • Encourage GoF aligned design without enforcing external frameworks.
  • Require no runtime PatternKit dependencies.
  • Generate readable code that is a drop in replacement for handwritten factories.
  • Provide async variations, preferring ValueTask where applicable.

Factory Pattern Coverage

1. Keyed Factory Method Generation

A keyed factory maps:

Key -> Method -> Result

Keyed factories are appropriate when selecting behavior, not concrete types. All methods participating in a keyed factory must share the same signature and return type.

Supported behavior:

  • Arbitrary key types.

  • One or more input parameters.

  • Optional default method.

  • Async generation using:

    • ValueTask<TResult> whenever any mapping method returns ValueTask<TResult>
    • ValueTask<TResult> when all methods return Task<TResult> (generator rewrites to ValueTask to reduce allocations)
    • Task<TResult> only when required (for example, when method signatures already expose Task based APIs the generator cannot rewrite)

Generated methods include:

  • Create(...)
  • TryCreate(...)
  • CreateAsync(...) returning ValueTask<TResult>
  • TryCreateAsync(...) returning ValueTask<(bool Success, TResult Result)>

2. Creator Based Type Factory Generation

This model mirrors the GoF Factory Method: a base product type and one or more concrete implementations. The generator produces a factory (Creator) capable of constructing the appropriate instance based on a key.

Supported behavior:

  • Interfaces or abstract classes as product bases.
  • Concrete types annotated with unique keys.
  • Async creation if any product exposes async creation methods.

The generated factory uses ValueTask for async methods whenever possible.

Generated members include:

  • Create(key)
  • TryCreate(key, out TProduct)
  • Optional enum based creation
  • CreateAsync(key) returning ValueTask<TProduct>
  • TryCreateAsync(key) returning ValueTask<(bool Success, TProduct Result)>

Async generation rules:

  • If any concrete product has a static async factory method returning Task<T> or ValueTask<T>, the generator uses ValueTask in the generated signatures.
  • If all products are synchronous, async variants are omitted unless explicitly enabled later.

Source Generator API

All attributes reside in PatternKit.Generators.Factories.

A. Keyed Factory Attributes

KeyedFactoryAttribute

[AttributeUsage(AttributeTargets.Class)]
public sealed class KeyedFactoryAttribute : Attribute
{
    public Type KeyType { get; }
    public string CreateMethodName { get; set; } = "Create";
    public bool CaseInsensitiveStrings { get; set; } = true;

    public KeyedFactoryAttribute(Type keyType)
    {
        KeyType = keyType;
    }
}

FactoryCaseAttribute

[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
public sealed class FactoryCaseAttribute : Attribute
{
    public object Key { get; }
    public FactoryCaseAttribute(object key)
    {
        Key = key;
    }
}

FactoryDefaultAttribute

[AttributeUsage(AttributeTargets.Method)]
public sealed class FactoryDefaultAttribute : Attribute { }

B. Creator Factory Attributes

CreatorFactoryAttribute

[AttributeUsage(AttributeTargets.Interface | AttributeTargets.Class)]
public sealed class CreatorFactoryAttribute : Attribute
{
    public Type KeyType { get; }
    public string FactoryTypeName { get; set; }
    public bool GenerateTryCreate { get; set; } = true;
    public bool GenerateEnumKeys { get; set; } = false;

    public CreatorFactoryAttribute(Type keyType)
    {
        KeyType = keyType;
    }
}

CreatorKeyAttribute

[AttributeUsage(AttributeTargets.Class)]
public sealed class CreatorKeyAttribute : Attribute
{
    public object Key { get; }
    public CreatorKeyAttribute(object key)
    {
        Key = key;
    }
}

Generator Behavior

Keyed Factory Example

Input:

[KeyedFactory(typeof(string))]
public static partial class MimeFactory
{
    [FactoryCase("json")]
    public static string Json() => "application/json";

    [FactoryDefault]
    public static string Default() => "application/octet-stream";
}

Generated synchronous output:

public static partial class MimeFactory
{
    public static string Create(string key)
    {
        if (key is null) throw new ArgumentNullException(nameof(key));

        if (string.Equals(key, "json", StringComparison.OrdinalIgnoreCase))
            return Json();

        return Default();
    }

    public static bool TryCreate(string key, out string value)
    {
        if (key is null)
        {
            value = default!;
            return false;
        }

        if (string.Equals(key, "json", StringComparison.OrdinalIgnoreCase))
        {
            value = Json();
            return true;
        }

        value = Default();
        return false;
    }
}

If any mapping method is async, the generator emits:

public static ValueTask<string> CreateAsync(string key);
public static ValueTask<(bool Success, string Result)> TryCreateAsync(string key);

ValueTask is used unless Task based signatures cannot be rewritten.


Creator Factory Example

Input:

[CreatorFactory(typeof(string), FactoryTypeName = "NotificationFactory")]
public interface INotification {}

[CreatorKey("email")]
public class EmailNotification : INotification {}

[CreatorKey("sms")]
public class SmsNotification : INotification {}

Generated output:

public sealed partial class NotificationFactory
{
    public INotification Create(string key)
    {
        return key switch
        {
            "email" => new EmailNotification(),
            "sms" => new SmsNotification(),
            _ => throw new ArgumentOutOfRangeException(nameof(key))
        };
    }

    public bool TryCreate(string key, out INotification result)
    {
        switch (key)
        {
            case "email":
                result = new EmailNotification();
                return true;
            case "sms":
                result = new SmsNotification();
                return true;
            default:
                result = default!;
                return false;
        }
    }
}

Async variants:

public ValueTask<INotification> CreateAsync(string key);
public ValueTask<(bool Success, INotification Result)> TryCreateAsync(string key);

Async result types are ValueTask unless underlying implementations force the use of Task.


Diagnostics

Keyed Factories

  • KF001: Type with KeyedFactoryAttribute must be a static partial class.
  • KF002: All FactoryCase and FactoryDefault methods must share the same signature.
  • KF003: Duplicate keys detected.
  • KF004: Multiple default methods declared.
  • KF005: Key value is incompatible with the declared KeyType.

Creator Factories

  • CF001: CreatorFactoryAttribute must annotate an interface or abstract class.
  • CF002: Types with CreatorKeyAttribute must implement or inherit the annotated type.
  • CF003: Types must be concrete and constructible.
  • CF004: Duplicate keys detected.
  • CF005: Mismatched key type.

Async Generation Guidelines

The generator uses the following rules:

  1. If all async capable methods return ValueTask, generated async APIs use ValueTask.
  2. If methods return Task, the generator attempts to wrap them in ValueTask where safe.
  3. If a signature cannot be rewritten safely, Task is preserved.
  4. If no async capable methods exist, async APIs are omitted.

This ensures minimal allocations and optimal performance while preserving correctness.


Next Steps

  • Implement incremental generator skeleton.
  • Implement semantic validation for keys and signatures.
  • Generate synchronous and asynchronous factory methods.
  • Generate ValueTask based async methods where appropriate.
  • Add Roslyn diagnostics.
  • Document usage and examples.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions