Skip to content

Instantly share code, notes, and snippets.

@DCFApixels
Last active May 2, 2024 10:01
Show Gist options
  • Save DCFApixels/c0dd6ba4ee6d4fa641a158108d1b82c8 to your computer and use it in GitHub Desktop.
Save DCFApixels/c0dd6ba4ee6d4fa641a158108d1b82c8 to your computer and use it in GitHub Desktop.
Service-Locator c#
// README https://gist.github.com/DCFApixels/c0dd6ba4ee6d4fa641a158108d1b82c8#file-service-md
// inspired by https://github.com/Leopotam/globals/blob/master/src/Service.cs
// author: DCFApixels
// license: MIT
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.CompilerServices;
using static DCFApixels.Service;
namespace DCFApixels
{
[AttributeUsage(AttributeTargets.All, Inherited = false, AllowMultiple = true)]
public sealed class AssociateServiceFactoryWithAttribute : Attribute
{
public readonly Type targetType;
public readonly string staticFactoryMethodName;
public AssociateServiceFactoryWithAttribute(Type targetType, string staticFactoryMethodName = "")
{
this.targetType = targetType;
this.staticFactoryMethodName = staticFactoryMethodName;
}
}
public interface IService
{
void OnEnabled();
void OnDisabled();
}
internal static class Service
{
private static Dictionary<Type, MethodBase> _associatedFactoryMethods;
internal static bool TryGetAssotiatedFactoryMethod(Type type, out MethodBase method)
{
if (_isInit == false) { Init(); }
bool result = _associatedFactoryMethods.TryGetValue(type, out method);
if(result == false) { method = FindConstructor(type); }
return method != null;
}
// Implementing via lazy initialization removes dependency on the static constructor implementation and ensures that associations are generated when needed.
private static bool _isInit;
private static void Init()
{
_associatedFactoryMethods = new Dictionary<Type, MethodBase>();
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
{
var types = assembly.GetTypes();
foreach (var type in types)
{
var atrs = type.GetCustomAttributes<AssociateServiceFactoryWithAttribute>();
foreach (var atr in atrs)
{
if (atr.targetType.IsGenericType)
{
throw new Exception("Generic types are not supported yet.");
}
if (_associatedFactoryMethods.TryGetValue(atr.targetType, out var otherMethod))
{
throw new Exception($"Cannot be assigned multi-association with {atr.targetType.Name}, remove the {nameof(AssociateServiceFactoryWithAttribute)} from {type.Name} or {otherMethod.DeclaringType.Name}.");
}
var factory = FindFactoryMethod(type, atr.staticFactoryMethodName);
if (factory == null)
{
if (string.IsNullOrEmpty(atr.staticFactoryMethodName))
{
throw new Exception($"Type {type.Name} does not have a no-argument constructor.");
}
else
{
throw new Exception($"Failed to find a static {atr.staticFactoryMethodName} method in {type.Name}.");
}
}
_associatedFactoryMethods.Add(atr.targetType, factory);
}
}
}
_isInit = true;
}
internal static MethodBase FindConstructor(Type type)
{
return type.GetConstructor(BindingFlags.Instance | BindingFlags.Public, null, Type.EmptyTypes, null);
}
internal static MethodBase FindFactoryMethod(Type type, string staticFactoryMethodName)
{
if (string.IsNullOrEmpty(staticFactoryMethodName) == false)
{
return type.GetMethod(staticFactoryMethodName, BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
}
return FindConstructor(type);
}
}
public static class Service<T> where T : class
{
private static MethodBase _factoryMethod;
private static T _instance;
private readonly static Type _type;
public static event Action<T> Changed = delegate { };
static Service()
{
_type = typeof(T);
}
/// <summary>Returns True if instance of T type is already set</summary>
public static bool IsNull { get { return _instance == null; } }
/// <summary>Returns True if instance of T type is not set</summary>
public static bool IsNotNull { get { return _instance != null; } }
public static void Register(Func<T> factory)
{
_factoryMethod = factory.Method;
}
/// <summary>Gets global instance of T type</summary>
/// <param name="createIfNotExists">If true and instance not exists - new instance will be created.</param>
/// <returns>Instance of T type</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static T Get(bool createIfNotExists = false)
{
#if DEBUG
if (createIfNotExists && _factoryMethod == null && !TryGetAssotiatedFactoryMethod(_type, out _factoryMethod)) { ThrowNoFactoryMethod(); }
#endif
if (_instance != null) { return _instance; }
if (createIfNotExists) { Set(CreateInstance()); }
return _instance;
}
private static bool _isRecursiveCallSet = false;
/// <summary>Sets global instance of T type</summary>
/// <param name="instance">New instance of T type.</param>
/// <returns>Instance of T type</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static T Set(T instance)
{
if (_instance == instance) { return instance; }
if (_isRecursiveCallSet) { ThrowRecursiveCallSet(); }
_isRecursiveCallSet = true;
var oldInstance = _instance;
_instance = instance;
if (oldInstance != null && oldInstance is IService service1)
{
service1.OnDisabled();
}
if (instance != null && instance is IService service2)
{
service2.OnEnabled();
}
Changed(instance);
_isRecursiveCallSet = false;
return _instance;
}
/// <summary>Returns True if otherInstance is the current instance</summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool Equals(T otherInstance)
{
return _instance == otherInstance;
}
/// <summary>Sets global instance of T type to Null</summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Reset()
{
Set(null);
}
/// <summary>Creates a new global instance T</summary>
/// <returns>New instance of T type</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static T Renew()
{
Set(null);
return Get(true);
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static T CreateInstance()
{
if (_factoryMethod == null)
{
TryGetAssotiatedFactoryMethod(_type, out _factoryMethod);
}
if (_factoryMethod is ConstructorInfo constructorInfo)
{
return (T)constructorInfo.Invoke(null);
}
return (T)_factoryMethod.Invoke(null, BindingFlags.Default, null, null, null);
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ThrowNoFactoryMethod()
{
throw new ArgumentException($"There is no factory method set for {typeof(T).Name}. Use Register method set it manually, or create a no-argument constructor or use {nameof(AssociateServiceFactoryWithAttribute)}.");
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ThrowRecursiveCallSet()
{
throw new ArgumentException($"Recursive call to the {nameof(Set)} method.");
}
}
}

Реализация Service-Locator для C#

Установка

Просто скопируйте файл Service.cs в проект.

Применение

class SomeService
{
    public int count;
}

// Инициализация экземпляра.
Service<SomeService>.Set(new SomeService());

// Запрос экземпляра.
Service<SomeService>.Get().count = 100;

// Очистка.
Service<SomeService>.Set(null);

Метод Get в Relaese сборке работает достаточно быстро, чтобы не кешировать его результат

Автоматическая инициализация экземпляра

// Eсли экземпляр не был установлен через Set(service) - будет создан посредством вызова фабрики.
Service<SomeService>.Get(true).count = 100;

Есть 3 способа назначить фабричый метод для автоматической инициализации:

  • Автоматический
    По умолчанию Get(true) попытается вызвать конструктор без параметров для заданного типа T;
  • Ручной
    Вместо конструктора можно назначить фабрику вручную через Service<T>.Register(Func<T> factory);
  • Через атрибут
    Фабрику можно также назначить и с помощью атрибута AssociateServiceFactoryWith, ниже описано как пользоваться;

AssociateServiceFactoryWith

Пример использования:

public interface ISomeInterface
{
    void Do();
}
// Конструктор без аргументов класса SomeService, станет фабричным методом для типа ISoneInterface
[AssociateServiceFactoryWith(typeof(ISomeInterface))]
public class SomeService : ISomeInterface
{
    public void Do()
    {
        Console.WriteLine(nameof(AutoSomeService));
    }
}

//Если сервис ISomeInterface не существует - будет вызван конструктор класса SomeService
Service<ISomeInterface>.Get(true).Do();
public interface ISomeInterface
{
    void Do();
}
// Метод Factory класса SomeService, станет фабричным методом для типа ISoneInterface
[AssociateServiceFactoryWith(typeof(ISomeInterface), nameof(Factory))]
public class SomeService : ISomeInterface
{
    public void Do()
    {
        Console.WriteLine(nameof(AutoSomeService));
    }

    public static ISomeInterface Factory()
    {
        return new AutoSomeService();
    }
}
  
//Если сервис ISomeInterface не существует - будет вызван метод SomeService.Builder()
Service<ISomeInterface>.Get(true).Do();

AssociateServiceFactoryWith не поддерживает работу с Generic

События

Cобытие Service<T>.Changed реагирует на вызов Service<T>.Set(instance)

Интерфейс IService добавляет коллбеки для сервисов:

  • OnActivated - вызывается для установленного сервиса;
  • OnDeactivated - вызывается для сервиса если был установлен другой экземпляр, можно использовать для освобождения ресурсов или отсписки от событий;

Запрещен вызов Set внутри событий

Прочее

Остальные методы:

  • Service<T>.IsNull - Аналогично Service<T>.Get() == null;
  • Service<T>.IsNotNull - Аналогично Service<T>.Get() != null;
  • Service<T>.Equals(other) - Аналогично Service<T>.Get() == other;
  • Service<T>.Reset() - Аналог Service<Е>.Set(null);
  • Service<T>.Renew() - Аналог Service<Е>.Set(null), но с одновременным созданием нового экземпляра;

Eще один пример получения экземпляра сервиса, без кеширования:

public class Foo
{
    private ISomeInterface Service => Service<ISomeInterface>.Get();
    public void Do()
    {
        Service.Do();
        Service.Count = 100;
    }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment