fix: scoped services must register their dependencies with PluginManager to ensure the backing services are kept alive long enough

This commit is contained in:
goat 2023-09-29 20:47:54 +02:00
parent e9e234b340
commit 4b9de31240
No known key found for this signature in database
GPG key ID: 49E2AA8C6A76498B
3 changed files with 72 additions and 33 deletions

View file

@ -53,7 +53,6 @@ internal class ServiceContainer : IServiceProvider, IServiceType
}
this.instances[typeof(T)] = new(instance.ContinueWith(x => new WeakReference(x.Result)), typeof(T));
this.RegisterInterfaces(typeof(T));
}
/// <summary>

View file

@ -145,12 +145,12 @@ internal static class ServiceManager
if (serviceKind is ServiceKind.None)
continue;
// Scoped service do not go through Service<T>, so we must let ServiceContainer know what their interfaces map to
if (serviceKind is ServiceKind.ScopedService)
{
serviceContainer.RegisterInterfaces(serviceType);
// Let IoC know about the interfaces this service implements
serviceContainer.RegisterInterfaces(serviceType);
// Scoped service do not go through Service<T> and are never early loaded
if (serviceKind.HasFlag(ServiceKind.ScopedService))
continue;
}
Debug.Assert(
!serviceKind.HasFlag(ServiceKind.ManualService) && !serviceKind.HasFlag(ServiceKind.ScopedService),
@ -176,15 +176,10 @@ internal static class ServiceManager
earlyLoadingServices.Add(serviceType);
}
dependencyServicesMap[serviceType] =
(List<Type>)typeof(Service<>)
.MakeGenericType(serviceType)
.InvokeMember(
"GetDependencyServices",
BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.Public,
null,
null,
null);
var typeAsServiceT = ServiceHelpers.GetAsService(serviceType);
dependencyServicesMap[serviceType] = ServiceHelpers.GetDependencies(typeAsServiceT)
.Select(x => typeof(Service<>).MakeGenericType(x))
.ToList();
}
_ = Task.Run(async () =>
@ -327,16 +322,8 @@ internal static class ServiceManager
Log.Verbose("Calling GetDependencyServices for '{ServiceName}'", serviceType.FullName!);
dependencyServicesMap[serviceType] =
((List<Type>)typeof(Service<>)
.MakeGenericType(serviceType)
.InvokeMember(
"GetDependencyServices",
BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.Public,
null,
null,
null))!
.Select(x => x.GetGenericArguments()[0]).ToList();
var typeAsServiceT = ServiceHelpers.GetAsService(serviceType);
dependencyServicesMap[serviceType] = ServiceHelpers.GetDependencies(typeAsServiceT);
allToUnload.Add(serviceType);
}

View file

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
@ -133,8 +134,8 @@ internal static class Service<T> where T : IServiceType
res.AddRange(typeof(T)
.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
.Select(x => x.FieldType)
.Where(x => x.GetCustomAttribute<ServiceManager.ServiceDependency>(true) != null));
.Where(x => x.GetCustomAttribute<ServiceManager.ServiceDependency>(true) != null)
.Select(x => x.FieldType));
res.AddRange(typeof(T)
.GetCustomAttributes()
@ -149,12 +150,31 @@ internal static class Service<T> where T : IServiceType
if (!serviceType.IsAssignableTo(typeof(IServiceType)))
continue;
var attr = serviceType.GetCustomAttribute<PluginInterfaceAttribute>(true);
if (attr == null)
if (serviceType == typeof(PluginManager))
continue;
// Scoped plugin services lifetime is tied to their scopes. They go away when LocalPlugin goes away.
// Nonetheless, their direct dependencies must be considered.
if (serviceType.GetServiceKind() == ServiceManager.ServiceKind.ScopedService)
{
var typeAsServiceT = ServiceHelpers.GetAsService(serviceType);
var dependencies = ServiceHelpers.GetDependencies(typeAsServiceT);
ServiceManager.Log.Verbose("Found dependencies of scoped plugin service {Type} ({Cnt})", serviceType.FullName!, dependencies!.Count);
foreach (var scopedDep in dependencies)
{
if (scopedDep == typeof(PluginManager))
throw new Exception("Scoped plugin services cannot depend on PluginManager.");
ServiceManager.Log.Verbose("PluginManager MUST depend on {Type} via {BaseType}", scopedDep.FullName!, serviceType.FullName!);
res.Add(scopedDep);
}
continue;
}
var pluginInterfaceAttribute = serviceType.GetCustomAttribute<PluginInterfaceAttribute>(true);
if (pluginInterfaceAttribute == null)
continue;
ServiceManager.Log.Verbose("PluginManager MUST depend on {Type}", serviceType.FullName!);
@ -164,7 +184,6 @@ internal static class Service<T> where T : IServiceType
return res
.Distinct()
.Select(x => typeof(Service<>).MakeGenericType(x))
.ToList();
}
@ -295,3 +314,37 @@ internal static class Service<T> where T : IServiceType
}
}
}
/// <summary>
/// Helper functions for services.
/// </summary>
internal static class ServiceHelpers
{
/// <summary>
/// Get a list of dependencies for a service. Only accepts Service&lt;T&gt; types.
/// These are returned as Service&lt;T&gt; types.
/// </summary>
/// <param name="serviceType">The dependencies for this service.</param>
/// <returns>A list of dependencies.</returns>
public static List<Type> GetDependencies(Type serviceType)
{
return (List<Type>)serviceType.InvokeMember(
"GetDependencyServices",
BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.Public,
null,
null,
null) ?? new List<Type>();
}
/// <summary>
/// Get the Service&lt;T&gt; type for a given service type.
/// This will throw if the service type is not a valid service.
/// </summary>
/// <param name="type">The type to obtain a Service&lt;T&gt; for.</param>
/// <returns>The Service&lt;T&gt;.</returns>
public static Type GetAsService(Type type)
{
return typeof(Service<>)
.MakeGenericType(type);
}
}