mirror of
https://github.com/goatcorp/Dalamud.git
synced 2026-02-21 07:17:45 +01:00
IOC: scoped/on-demand services (#1120)
This commit is contained in:
parent
ca8a05b672
commit
daa9f72218
8 changed files with 309 additions and 62 deletions
|
|
@ -6,6 +6,7 @@ using System.Runtime.Serialization;
|
|||
using System.Threading.Tasks;
|
||||
|
||||
using Dalamud.Logging.Internal;
|
||||
using Dalamud.Plugin.Internal.Types;
|
||||
|
||||
namespace Dalamud.IoC.Internal;
|
||||
|
||||
|
|
@ -45,9 +46,12 @@ internal class ServiceContainer : IServiceProvider, IServiceType
|
|||
/// </summary>
|
||||
/// <param name="objectType">The type of object to create.</param>
|
||||
/// <param name="scopedObjects">Scoped objects to be included in the constructor.</param>
|
||||
/// <param name="scope">The scope to be used to create scoped services.</param>
|
||||
/// <returns>The created object.</returns>
|
||||
public async Task<object?> CreateAsync(Type objectType, params object[] scopedObjects)
|
||||
public async Task<object?> CreateAsync(Type objectType, object[] scopedObjects, IServiceScope? scope = null)
|
||||
{
|
||||
var scopeImpl = scope as ServiceScopeImpl;
|
||||
|
||||
var ctor = this.FindApplicableCtor(objectType, scopedObjects);
|
||||
if (ctor == null)
|
||||
{
|
||||
|
|
@ -76,11 +80,22 @@ internal class ServiceContainer : IServiceProvider, IServiceType
|
|||
parameters
|
||||
.Select(async p =>
|
||||
{
|
||||
if (p.parameterType.GetCustomAttribute<ServiceManager.ScopedService>() != null)
|
||||
{
|
||||
if (scopeImpl == null)
|
||||
{
|
||||
Log.Error("Failed to create {TypeName}, depends on scoped service but no scope", objectType.FullName!);
|
||||
return null;
|
||||
}
|
||||
|
||||
return await scopeImpl.CreatePrivateScopedObject(p.parameterType, scopedObjects);
|
||||
}
|
||||
|
||||
var service = await this.GetService(p.parameterType, scopedObjects);
|
||||
|
||||
if (service == null)
|
||||
{
|
||||
Log.Error("Requested service type {TypeName} was not available (null)", p.parameterType.FullName!);
|
||||
Log.Error("Requested ctor service type {TypeName} was not available (null)", p.parameterType.FullName!);
|
||||
}
|
||||
|
||||
return service;
|
||||
|
|
@ -95,7 +110,7 @@ internal class ServiceContainer : IServiceProvider, IServiceType
|
|||
|
||||
var instance = FormatterServices.GetUninitializedObject(objectType);
|
||||
|
||||
if (!await this.InjectProperties(instance, scopedObjects))
|
||||
if (!await this.InjectProperties(instance, scopedObjects, scope))
|
||||
{
|
||||
Log.Error("Failed to create {TypeName}, a requested property service type could not be satisfied", objectType.FullName!);
|
||||
return null;
|
||||
|
|
@ -112,10 +127,12 @@ internal class ServiceContainer : IServiceProvider, IServiceType
|
|||
/// The properties can be marked with the <see cref="RequiredVersionAttribute"/> to lock down versions.
|
||||
/// </summary>
|
||||
/// <param name="instance">The object instance.</param>
|
||||
/// <param name="scopedObjects">Scoped objects.</param>
|
||||
/// <param name="publicScopes">Scoped objects to be injected.</param>
|
||||
/// <param name="scope">The scope to be used to create scoped services.</param>
|
||||
/// <returns>Whether or not the injection was successful.</returns>
|
||||
public async Task<bool> InjectProperties(object instance, params object[] scopedObjects)
|
||||
public async Task<bool> InjectProperties(object instance, object[] publicScopes, IServiceScope? scope = null)
|
||||
{
|
||||
var scopeImpl = scope as ServiceScopeImpl;
|
||||
var objectType = instance.GetType();
|
||||
|
||||
var props = objectType.GetProperties(BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public |
|
||||
|
|
@ -136,7 +153,21 @@ internal class ServiceContainer : IServiceProvider, IServiceType
|
|||
|
||||
foreach (var prop in props)
|
||||
{
|
||||
var service = await this.GetService(prop.propertyInfo.PropertyType, scopedObjects);
|
||||
object service = null;
|
||||
|
||||
if (prop.propertyInfo.PropertyType.GetCustomAttribute<ServiceManager.ScopedService>() != null)
|
||||
{
|
||||
if (scopeImpl == null)
|
||||
{
|
||||
Log.Error("Failed to create {TypeName}, depends on scoped service but no scope", objectType.FullName!);
|
||||
}
|
||||
else
|
||||
{
|
||||
service = await scopeImpl.CreatePrivateScopedObject(prop.propertyInfo.PropertyType, publicScopes);
|
||||
}
|
||||
}
|
||||
|
||||
service ??= await this.GetService(prop.propertyInfo.PropertyType, publicScopes);
|
||||
|
||||
if (service == null)
|
||||
{
|
||||
|
|
@ -150,6 +181,12 @@ internal class ServiceContainer : IServiceProvider, IServiceType
|
|||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a service scope, enabling the creation of objects with scoped services.
|
||||
/// </summary>
|
||||
/// <returns>An implementation of a service scope.</returns>
|
||||
public IServiceScope GetScope() => new ServiceScopeImpl(this);
|
||||
|
||||
/// <inheritdoc/>
|
||||
object? IServiceProvider.GetService(Type serviceType) => this.GetService(serviceType);
|
||||
|
||||
|
|
@ -185,7 +222,7 @@ internal class ServiceContainer : IServiceProvider, IServiceType
|
|||
}
|
||||
|
||||
// resolve dependency from scoped objects
|
||||
var scoped = scopedObjects.FirstOrDefault(o => o.GetType() == serviceType);
|
||||
var scoped = scopedObjects.FirstOrDefault(o => o.GetType().IsAssignableTo(serviceType));
|
||||
if (scoped == default)
|
||||
{
|
||||
return null;
|
||||
|
|
@ -211,7 +248,12 @@ internal class ServiceContainer : IServiceProvider, IServiceType
|
|||
.Union(this.instances.Keys)
|
||||
.ToArray();
|
||||
|
||||
var ctors = type.GetConstructors(BindingFlags.Public | BindingFlags.Instance);
|
||||
// Allow resolving non-public ctors for Dalamud types
|
||||
var ctorFlags = BindingFlags.Public | BindingFlags.Instance;
|
||||
if (type.Assembly == Assembly.GetExecutingAssembly())
|
||||
ctorFlags |= BindingFlags.NonPublic;
|
||||
|
||||
var ctors = type.GetConstructors(ctorFlags);
|
||||
foreach (var ctor in ctors)
|
||||
{
|
||||
if (this.ValidateCtor(ctor, types))
|
||||
|
|
@ -228,8 +270,10 @@ internal class ServiceContainer : IServiceProvider, IServiceType
|
|||
var parameters = ctor.GetParameters();
|
||||
foreach (var parameter in parameters)
|
||||
{
|
||||
var contains = types.Contains(parameter.ParameterType);
|
||||
if (!contains)
|
||||
var contains = types.Any(x => x.IsAssignableTo(parameter.ParameterType));
|
||||
|
||||
// Scoped services are created on-demand
|
||||
if (!contains && parameter.ParameterType.GetCustomAttribute<ServiceManager.ScopedService>() == null)
|
||||
{
|
||||
Log.Error("Failed to validate {TypeName}, unable to find any services that satisfy the type", parameter.ParameterType.FullName!);
|
||||
return false;
|
||||
|
|
|
|||
101
Dalamud/IoC/Internal/ServiceScope.cs
Normal file
101
Dalamud/IoC/Internal/ServiceScope.cs
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Dalamud.IoC.Internal;
|
||||
|
||||
/// <summary>
|
||||
/// Container enabling the creation of scoped services.
|
||||
/// </summary>
|
||||
internal interface IServiceScope : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Register objects that may be injected to scoped services,
|
||||
/// but not directly to created objects.
|
||||
/// </summary>
|
||||
/// <param name="scopes">The scopes to add.</param>
|
||||
public void RegisterPrivateScopes(params object[] scopes);
|
||||
|
||||
/// <summary>
|
||||
/// Create an object.
|
||||
/// </summary>
|
||||
/// <param name="objectType">The type of object to create.</param>
|
||||
/// <param name="scopedObjects">Scoped objects to be included in the constructor.</param>
|
||||
/// <returns>The created object.</returns>
|
||||
public Task<object?> CreateAsync(Type objectType, params object[] scopedObjects);
|
||||
|
||||
/// <summary>
|
||||
/// Inject <see cref="PluginInterfaceAttribute" /> interfaces into public or static properties on the provided object.
|
||||
/// The properties have to be marked with the <see cref="PluginServiceAttribute" />.
|
||||
/// The properties can be marked with the <see cref="RequiredVersionAttribute" /> to lock down versions.
|
||||
/// </summary>
|
||||
/// <param name="instance">The object instance.</param>
|
||||
/// <param name="scopedObjects">Scoped objects to be injected.</param>
|
||||
/// <returns>Whether or not the injection was successful.</returns>
|
||||
public Task<bool> InjectPropertiesAsync(object instance, params object[] scopedObjects);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implementation of a service scope.
|
||||
/// </summary>
|
||||
internal class ServiceScopeImpl : IServiceScope
|
||||
{
|
||||
private readonly ServiceContainer container;
|
||||
|
||||
private readonly List<object> privateScopedObjects = new();
|
||||
private readonly List<object> scopeCreatedObjects = new();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ServiceScopeImpl" /> class.
|
||||
/// </summary>
|
||||
/// <param name="container">The container this scope will use to create services.</param>
|
||||
public ServiceScopeImpl(ServiceContainer container)
|
||||
{
|
||||
this.container = container;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void RegisterPrivateScopes(params object[] scopes)
|
||||
{
|
||||
this.privateScopedObjects.AddRange(scopes);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<object?> CreateAsync(Type objectType, params object[] scopedObjects)
|
||||
{
|
||||
return this.container.CreateAsync(objectType, scopedObjects, this);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<bool> InjectPropertiesAsync(object instance, params object[] scopedObjects)
|
||||
{
|
||||
return this.container.InjectProperties(instance, scopedObjects, this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a service scoped to this scope, with private scoped objects.
|
||||
/// </summary>
|
||||
/// <param name="objectType">The type of object to create.</param>
|
||||
/// <param name="scopedObjects">Additional scoped objects.</param>
|
||||
/// <returns>The created object, or null.</returns>
|
||||
public async Task<object?> CreatePrivateScopedObject(Type objectType, params object[] scopedObjects)
|
||||
{
|
||||
var instance = this.scopeCreatedObjects.FirstOrDefault(x => x.GetType() == objectType);
|
||||
if (instance != null)
|
||||
return instance;
|
||||
|
||||
instance =
|
||||
await this.container.CreateAsync(objectType, scopedObjects.Concat(this.privateScopedObjects).ToArray());
|
||||
if (instance != null)
|
||||
this.scopeCreatedObjects.Add(instance);
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var createdObject in this.scopeCreatedObjects.OfType<IDisposable>()) createdObject.Dispose();
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue