Skip to content

Commit

Permalink
Singleton Factory (Reflective Creation) (#2)
Browse files Browse the repository at this point in the history
* Rename folder

* Created helper for getting singleton constructors

* Created Singleton Factory

This can be used to get all instantiate all singletons at the start of a program, instead of waiting for them each to be called

* Create test for singleton factory, to ensure it properly instantiates the singletons

* Formatting
  • Loading branch information
TylerCarrol authored Jun 2, 2024
1 parent f2cac0b commit e4a4ff2
Show file tree
Hide file tree
Showing 11 changed files with 194 additions and 21 deletions.
2 changes: 1 addition & 1 deletion TJC.Singleton.Tests/Helpers/MocSingletonFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ internal static class MocSingletonFactory
public static List<T> GetInstances<T>(Func<T> getSingleton, int amount)
{
var singletons = new List<T>();
var threads = new List<Thread>();
var threads = new List<Thread>();

for (var i = 0; i < amount; i++)
threads.Add(new Thread(() => singletons.Add(getSingleton())));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@

internal class MockSingletonPrivateConstructorWithParameters : SingletonBaseClass<MockSingletonPrivateConstructorWithParameters>, IIdentifier
{
#pragma warning disable IDE0051
private MockSingletonPrivateConstructorWithParameters(Guid? id = null)
#pragma warning restore IDE0051
{
Id = id ?? Guid.NewGuid();
}

public Guid Id { get; }
}
8 changes: 8 additions & 0 deletions TJC.Singleton.Tests/Mocks/Valid/MockSingletonInstantiated.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace TJC.Singleton.Tests.Mocks.Valid;

internal class MockSingletonInstantiated : SingletonBaseClass<MockSingletonInstantiated>, IIdentifier
{
private MockSingletonInstantiated() { }

public Guid Id { get; } = Guid.NewGuid();
}
1 change: 0 additions & 1 deletion TJC.Singleton.Tests/TJC.Singleton.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
Expand Down
16 changes: 16 additions & 0 deletions TJC.Singleton.Tests/Tests/Instantiated/IsInstantiatedTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using TJC.Singleton.Factories;

namespace TJC.Singleton.Tests.Tests.Instantiated;

[TestClass]
public class IsInstantiatedTest
{
[TestMethod]
public void SingletonGetInstantiatedAfterBeingReferencedTest()
{
// MockSingletonInstantiated can only be used in this test and nowhere else, since object instances can persist between tests
Assert.IsFalse(MockSingletonInstantiated.IsInstantiated, $"{nameof(MockSingletonInstantiated)} was already instantiated");
SingletonFactory.InstantiatedAll(trace: true);
Assert.IsTrue(MockSingletonInstantiated.IsInstantiated, $"{nameof(MockSingletonInstantiated)} is not instantiated");
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace TJC.Singleton.Tests.Tests.Instance;
namespace TJC.Singleton.Tests.Tests.ThreadSafety;

[TestClass]
public class NonThreadSafeInstanceTest
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace TJC.Singleton.Tests.Tests.Instance;
namespace TJC.Singleton.Tests.Tests.ThreadSafety;

[TestClass]
public class ValidInstanceTest
Expand Down
68 changes: 68 additions & 0 deletions TJC.Singleton/Factories/SingletonFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using System.Diagnostics;
using System.Reflection;
using TJC.Singleton.Helpers;

namespace TJC.Singleton.Factories;

public static class SingletonFactory
{
#region Constants

private const string InstanceName = nameof(SingletonBaseClass<PlaceholderSingleton>.Instance);

private class PlaceholderSingleton : SingletonBaseClass<PlaceholderSingleton>;

#endregion

public static void InstantiatedAll(bool trace = true,
bool throwIfFailed = false)
{
var failedToInstantiate = new List<string>();
var singletons = GetSingletonTypes();

if (trace)
Trace.WriteLine($"{singletons.Count} Singletons Found");

foreach (var singleton in singletons)
if (!singleton.Instantiate(trace))
failedToInstantiate.Add(singleton.Name);

if (throwIfFailed && failedToInstantiate.Count > 0)
throw new Exception($"{string.Join(", ", failedToInstantiate)}");
}

public static List<Type> GetSingletonTypes()
{
var singletons = new List<Type>();

// Iterate through all assemblies & types to find all singletons
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
{
foreach (var type in assembly.GetTypes())
{
if (!SingletonIdentifierHelpers.IsConcreteSingleton(type))
continue; // Skip types that are not singletons

if (!SingletonConstructorHelpers.HasValidSingletonConstructor(type))
continue; // Skip singletons with invalid constructors

singletons.Add(type);
}
}

return singletons;
}

private static bool Instantiate(this Type singleton, bool trace)
{
if (trace)
Trace.WriteLine($"[{singleton.Name}] Instantiating");
var instanceProp = singleton.GetProperty(InstanceName,
BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy)
?? throw new Exception($"[{singleton.Name}] does not have property [{InstanceName}]");
var instanceValue = instanceProp.GetValue(singleton);
if (trace)
Trace.WriteLine($"[{singleton.Name}] {(instanceValue != null ? "Instantiated" : "Failed to Instantiate")}");
return instanceValue != null;
}
}
48 changes: 48 additions & 0 deletions TJC.Singleton/Helpers/SingletonConstructorHelpers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using System.Reflection;
using TJC.Singleton.Exceptions;

namespace TJC.Singleton.Helpers;

public static class SingletonConstructorHelpers
{
#region Get Singlet Constructor

public static ConstructorInfo GetSingletonConstructor<T>() =>
GetSingletonConstructor(typeof(T));

public static ConstructorInfo GetSingletonConstructor(Type type)
{
// Ensure there is no public constructor
var publicConstructors = type.GetConstructors().Where(x => x.IsPublic).ToList();
if (publicConstructors.Count != 0)
throw new InvalidSingletonConstructorException($"[{type}] singleton should not have public constructor{(publicConstructors.Count > 1 ? "s" : string.Empty)}");

// Ensure there is a non-public parameterless constructor
var ctor = type.GetConstructor(BindingFlags.Instance | BindingFlags.NonPublic, null, [], null) ??
throw new InvalidSingletonConstructorException($"[{type}] singleton is missing a non-public parameterless constructor");

return ctor;
}

#endregion

#region Check if Singleton has Valid Constructor

public static bool HasValidSingletonConstructor<T>() =>
HasValidSingletonConstructor(typeof(T));

public static bool HasValidSingletonConstructor(Type type)
{
try
{
GetSingletonConstructor(type);
return true;
}
catch
{
return false;
}
}

#endregion
}
24 changes: 24 additions & 0 deletions TJC.Singleton/Helpers/SingletonIdentifierHelpers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
namespace TJC.Singleton.Helpers;

public static class SingletonIdentifierHelpers
{
public static bool IsConcreteSingleton(Type type)
{
// Ensure type is a concrete class
if (!type.IsClass || type.IsAbstract)
return false;

// Ensure type derives from singleton
var openGenericType = typeof(SingletonBaseClass<>);

var baseType = type.BaseType;
while (baseType != null)
{
if (baseType.IsGenericType && baseType.GetGenericTypeDefinition() == openGenericType)
return true;
baseType = baseType.BaseType;
}

return false;
}
}
40 changes: 24 additions & 16 deletions TJC.Singleton/SingletonBaseClass.cs
Original file line number Diff line number Diff line change
@@ -1,30 +1,38 @@
using TJC.Singleton.Exceptions;
using TJC.Singleton.Helpers;

namespace TJC.Singleton;

/// <summary>
/// Creates a single instance of <seealso cref="TMyClass"/> that can be accessed through the <see cref="Instance"/> property.
/// Creates a single instance of <seealso cref="TDerivedClass"/> that can be accessed through the <see cref="Instance"/> property.
/// </summary>
/// <typeparam name="TMyClass"></typeparam>
/// <typeparam name="TDerivedClass"></typeparam>
/// <exception cref="InvalidSingletonConstructorException">Must have a non-public parameterless constructor.</exception>
public abstract class SingletonBaseClass<TMyClass> where TMyClass : SingletonBaseClass<TMyClass>
public abstract class SingletonBaseClass<TDerivedClass> where TDerivedClass : SingletonBaseClass<TDerivedClass>
{
private static readonly Lazy<TMyClass> _instance = new(CreateInstance);
#region Fields

public static TMyClass Instance => _instance.Value;
private static readonly Lazy<TDerivedClass> _instance = new(CreateInstance);

private static TMyClass CreateInstance()
{
// Ensure there is no public constructor
var pubConstructors = typeof(TMyClass).GetConstructors().Where(x => x.IsPublic).ToList();
if (pubConstructors.Count != 0)
throw new InvalidSingletonConstructorException($"[{typeof(TMyClass)}] singleton should not have public constructor{(pubConstructors.Count > 1 ? "s" : string.Empty)}");
#endregion

#region Properties

public static TDerivedClass Instance => _instance.Value;

public static bool IsInstantiated => _instance.IsValueCreated;

// Ensure there is a non-public parameterless constructor
var ctor = typeof(TMyClass).GetConstructor(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic, null, [], null) ??
throw new InvalidSingletonConstructorException($"[{typeof(TMyClass)}] singleton is missing a non-public parameterless constructor");
#endregion

// Use reflection to create an instance of the derived class
return (TMyClass)ctor.Invoke(null) ?? throw new SingletonInitializationException($"[{typeof(TMyClass)}] singleton failed to initialize");
#region Methods

private static TDerivedClass CreateInstance()
{
// Use reflection to create an instance of the derived class.
var ctor = SingletonConstructorHelpers.GetSingletonConstructor<TDerivedClass>();
return (TDerivedClass)ctor.Invoke(null) ??
throw new SingletonInitializationException($"[{typeof(TDerivedClass)}] singleton failed to initialize");
}

#endregion
}

0 comments on commit e4a4ff2

Please sign in to comment.