Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
- name: Pack solution [Release]
run: dotnet pack --no-restore --no-build -c Release -p:VersionSuffix=$GITHUB_RUN_NUMBER -o out
- name: Upload artifacts
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: Nuget packages
path: |
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,15 @@ jobs:
- name: Pack solution [Release]
run: dotnet pack --no-restore --no-build -c Release -p:Version=$version -o out
- name: Upload artifacts
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: Nuget packages
path: |
out/*
- name: Publish Nuget packages to Nuget registry
run: dotnet nuget push "out/*" -k ${{secrets.NUGET_AUTH_TOKEN}}
- name: Upload nuget packages as release artifacts
uses: actions/github-script@v2
uses: actions/github-script@v7
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
Expand Down
7 changes: 2 additions & 5 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@ name: Run code tests

on:
pull_request:
branches:
- master
- develop
push:
branches:
- master
Expand Down Expand Up @@ -54,9 +51,9 @@ jobs:
reporttypes: 'Html'
tag: 'test_${{ github.run_number }}'
- name: Upload artifacts
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: Code coverage artifacts
name: Code coverage artifacts for ${{ matrix.os }}
path: |
${{ matrix.os }}.lcov.info
Clover.xml
Expand Down
1 change: 1 addition & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
<AnalysisMode>Recommended</AnalysisMode>
<GraphQLVersion>8.0.0</GraphQLVersion>
<GraphQLAspNetCore3Version>6.0.0</GraphQLAspNetCore3Version>
<NuGetAuditMode>direct</NuGetAuditMode>
</PropertyGroup>

<ItemGroup>
Expand Down
6 changes: 6 additions & 0 deletions src/GraphQL.DI/DIObjectGraphBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ namespace GraphQL.DI;
/// <summary>
/// This is a required base type of all DI-created graph types. <see cref="DIObjectGraphBase"/> may be
/// used if the <see cref="IResolveFieldContext.Source"/> type is <see cref="object"/>.
/// <para>
/// If the derived class implements <see cref="IDisposable"/>, the class must be registered within the DI container.
/// </para>
/// <para>
/// When registered within the DI container, the service lifetime must be <see cref="Microsoft.Extensions.DependencyInjection.ServiceLifetime.Transient">Transient</see>.
/// </para>
/// </summary>
public abstract class DIObjectGraphBase<TSource> : IDIObjectGraphBase<TSource>, IResolveFieldContext<TSource>
{
Expand Down
28 changes: 26 additions & 2 deletions src/GraphQL.DI/DIObjectGraphType.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Diagnostics;
using System.Linq.Expressions;
using System.Reflection;
using GraphQL.Types;
Expand Down Expand Up @@ -60,13 +61,36 @@ protected override IEnumerable<MemberInfo> GetRegisteredMembers()
// each field resolver will build a new instance of DIObject
/// <inheritdoc/>
protected override LambdaExpression BuildMemberInstanceExpression(MemberInfo memberInfo)
=> (Expression<Func<IResolveFieldContext, TDIGraph>>)((IResolveFieldContext context) => MemberInstanceFunc(context));
{
// use an explicit type here rather than simply LambdaExpression
Expression<Func<IResolveFieldContext, TDIGraph>> func;
if (typeof(IDisposable).IsAssignableFrom(typeof(TDIGraph))) {
func = (IResolveFieldContext context) => MemberInstanceDisposableFunc(context);
} else {
func = (IResolveFieldContext context) => MemberInstanceFunc(context);
}
return func;
}

/// <inheritdoc/>
private static TDIGraph MemberInstanceFunc(IResolveFieldContext context)
{
// create a new instance of DIObject, filling in any constructor arguments from DI
var graph = ActivatorUtilities.GetServiceOrCreateInstance<TDIGraph>(context.RequestServices ?? throw new MissingRequestServicesException());
var graph = ActivatorUtilities.GetServiceOrCreateInstance<TDIGraph>(context.RequestServices ?? ThrowMissingRequestServicesException());
// set the context
graph.Context = context;
// return the object
return graph;

static IServiceProvider ThrowMissingRequestServicesException() => throw new MissingRequestServicesException();
}

/// <inheritdoc/>
private static TDIGraph MemberInstanceDisposableFunc(IResolveFieldContext context)
{
// pull DIObject from dependency injection, as it is disposable and must be managed by DI
var graph = (context.RequestServices ?? throw new MissingRequestServicesException()).GetService<TDIGraph>()
?? throw new InvalidOperationException($"Could not resolve an instance of {typeof(TDIGraph).Name} from the service provider. DI graph types that implement IDisposable must be registered in the service provider.");
// set the context
graph.Context = context;
// return the object
Expand Down
20 changes: 20 additions & 0 deletions src/GraphQL.DI/GraphQLBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,26 @@ public static IGraphQLBuilder AddDIGraphTypes(this IGraphQLBuilder builder)
return builder;
}

/// <summary>
/// Scans the calling assembly for classes that implement <see cref="IDIObjectGraphBase{TSource}"/>
/// and registers them as transients within the DI container.
/// </summary>
public static IGraphQLBuilder AddDIGraphBases(this IGraphQLBuilder builder)
=> AddDIGraphBases(builder, Assembly.GetCallingAssembly());

/// <summary>
/// Scans the specified assembly for classes that implement <see cref="IDIObjectGraphBase"/>
/// and registers them as transients within the DI container.
/// </summary>
public static IGraphQLBuilder AddDIGraphBases(this IGraphQLBuilder builder, Assembly assembly)
{
foreach (var type in assembly.GetTypes()
.Where(x => x.IsClass && !x.IsAbstract && typeof(IDIObjectGraphBase).IsAssignableFrom(x))) {
builder.Services.TryRegister(type, type, ServiceLifetime.Transient);
}
return builder;
}

/// <summary>
/// Scans the calling assembly for classes that implement <see cref="IDIObjectGraphBase{TSource}"/> and
/// registers clr type mappings on the schema between that <see cref="DIObjectGraphType{TDIGraph, TSource}"/>
Expand Down
63 changes: 63 additions & 0 deletions src/GraphQL.DI/GraphQLDIBuilderExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using System.Reflection;
using GraphQL.DI;

namespace GraphQL;

/// <summary>
/// Provides extension methods to configure GraphQL.NET services within a dependency injection framework.
/// </summary>
public static class GraphQLDIBuilderExtensions
{
/// <summary>
/// Performs the following:
/// <list type="bullet">
/// <item>
/// Registers <see cref="DIObjectGraphType{TDIGraph}"/> and
/// <see cref="DIObjectGraphType{TDIGraph, TSource}"/> as generic types.
/// </item>
/// <item>
/// Scans the calling assembly for classes that implement <see cref="IDIObjectGraphBase{TSource}"/>
/// and registers them as transients within the DI container.
/// </item>
/// <item>
/// Scans the calling assembly for classes that implement <see cref="IDIObjectGraphBase{TSource}"/> and
/// registers clr type mappings on the schema between that <see cref="DIObjectGraphType{TDIGraph, TSource}"/>
/// (constructed from that class and its source type), and the source type.
/// Skips classes where the source type is <see cref="object"/>, or where the class is marked with
/// the <see cref="DoNotMapClrTypeAttribute"/>, or where another graph type would be automatically mapped
/// to the specified type, or where a graph type has already been registered to the specified clr type.
/// </item>
/// </list>
/// </summary>
public static IGraphQLBuilder AddDI(this IGraphQLBuilder builder)
=> AddDI(builder, Assembly.GetCallingAssembly());

/// <summary>
/// Performs the following:
/// <list type="bullet">
/// <item>
/// Registers <see cref="DIObjectGraphType{TDIGraph}"/> and
/// <see cref="DIObjectGraphType{TDIGraph, TSource}"/> as generic types.
/// </item>
/// <item>
/// Scans the specified assembly for classes that implement <see cref="IDIObjectGraphBase{TSource}"/>
/// and registers them as transients within the DI container.
/// </item>
/// <item>
/// Scans the specified assembly for classes that implement <see cref="IDIObjectGraphBase{TSource}"/> and
/// registers clr type mappings on the schema between that <see cref="DIObjectGraphType{TDIGraph, TSource}"/>
/// (constructed from that class and its source type), and the source type.
/// Skips classes where the source type is <see cref="object"/>, or where the class is marked with
/// the <see cref="DoNotMapClrTypeAttribute"/>, or where another graph type would be automatically mapped
/// to the specified type, or where a graph type has already been registered to the specified clr type.
/// </item>
/// </list>
/// </summary>
public static IGraphQLBuilder AddDI(this IGraphQLBuilder builder, Assembly assembly)
{
return builder
.AddDIGraphTypes()
.AddDIGraphBases(assembly)
.AddDIClrTypeMappings(assembly);
}
}
12 changes: 8 additions & 4 deletions src/Tests/DIObjectGraphTypeTests/DIObjectGraphTypeTestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,17 @@ public DIObjectGraphTypeTestBase() : base()
_contextMock.SetupGet(x => x.Schema).Returns((ISchema)null!);
}

protected IComplexGraphType Configure<T, TSource>(bool instance = false, bool scoped = false) where T : DIObjectGraphBase<TSource>, new()
protected IComplexGraphType Configure<T, TSource>(bool instance = false, bool scoped = false, bool registered = true) where T : DIObjectGraphBase<TSource>, new()
{
if (instance) {
if (scoped) {
_scopedServiceProviderMock.Setup(x => x.GetService(typeof(T))).Returns(() => new T()).Verifiable();
if (registered) {
if (scoped) {
_scopedServiceProviderMock.Setup(x => x.GetService(typeof(T))).Returns(() => new T()).Verifiable();
} else {
_serviceProviderMock.Setup(x => x.GetService(typeof(T))).Returns(() => new T()).Verifiable();
}
} else {
_serviceProviderMock.Setup(x => x.GetService(typeof(T))).Returns(() => new T()).Verifiable();
_serviceProviderMock.Setup(x => x.GetService(typeof(T))).Returns(() => null).Verifiable();
}
}
_graphType = new DIObjectGraphType<T, TSource>();
Expand Down
33 changes: 30 additions & 3 deletions src/Tests/DIObjectGraphTypeTests/Field.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@ public class CStaticMethod : DIObjectGraphBase
public static string? Field1() => "hello";
}

[Fact]
public void InstanceMethod()
[Theory]
[InlineData(true)]
[InlineData(false)]
public void InstanceMethod(bool registered)
{
Configure<CInstanceMethod, object>(true);
Configure<CInstanceMethod, object>(true, registered: registered);
VerifyField("Field1", nullable: true, concurrent: false, returnValue: "hello");
Verify(false);
}
Expand Down Expand Up @@ -443,6 +445,31 @@ public class CIgnore : DIObjectGraphBase<object>
public static string Field2() => "hello";
}

[Fact]
public void DisposableRegistered()
{
Configure<CDisposable, object>(true);
VerifyField("FieldTest", true, false, "hello");
Verify(false);
}

[Fact]
public async Task DisposableUnRegistered()
{
Configure<CDisposable, object>(true, registered: false);
var err = await Should.ThrowAsync<InvalidOperationException>(() => VerifyFieldAsync("FieldTest", true, false, "hello"));
err.Message.ShouldBe("Could not resolve an instance of CDisposable from the service provider. DI graph types that implement IDisposable must be registered in the service provider.");
Verify(false);
}

public class CDisposable : DIObjectGraphBase, IDisposable
{
[Name("FieldTest")]
public string? Field1() => "hello";

void IDisposable.Dispose() => GC.SuppressFinalize(this);
}

[Fact]
public void AddsMetadata()
{
Expand Down
85 changes: 85 additions & 0 deletions src/Tests/Execution/GraphQLBuilderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,36 @@ public void AddDIGraphTypes()
_mockServiceRegister.Setup(x => x.TryRegister(typeof(DIObjectGraphType<,>), typeof(DIObjectGraphType<,>), ServiceLifetime.Transient, RegistrationCompareMode.ServiceType)).Returns(_mockServiceRegister.Object).Verifiable();
_graphQLBuilder.AddDIGraphTypes().ShouldBe(_graphQLBuilder);
_mockGraphQLBuilder.Verify();
_mockServiceRegister.Verify();
}

[Fact]
public void AddDIGraphBases()
{
var registeredTypes = new List<Type>();
_mockServiceRegister.Setup(x => x.TryRegister(It.IsAny<Type>(), It.IsAny<Type>(), ServiceLifetime.Transient, RegistrationCompareMode.ServiceType))
.Returns<Type, Type, ServiceLifetime, RegistrationCompareMode>((serviceType, implementationType, _, _) => {
// Verify the type meets the criteria (class, not abstract, implements IDIObjectGraphBase)
if (!implementationType.IsClass || implementationType.IsAbstract || !typeof(IDIObjectGraphBase).IsAssignableFrom(implementationType)) {
throw new InvalidOperationException($"Invalid type registration: {implementationType}");
}
// Service type should match implementation type
if (serviceType != implementationType) {
throw new InvalidOperationException($"Service type {serviceType} does not match implementation type {implementationType}");
}
registeredTypes.Add(implementationType);
return _mockServiceRegister.Object;
})
.Verifiable();

// Call with specific assembly to test the overload and avoid assembly scanning issues
_graphQLBuilder.AddDIGraphBases().ShouldBe(_graphQLBuilder);
_mockGraphQLBuilder.Verify();
_mockServiceRegister.Verify();

// Verify Base1 and Base2 were registered
registeredTypes.ShouldContain(typeof(Base1));
registeredTypes.ShouldContain(typeof(Base2));
}

[Fact]
Expand All @@ -43,6 +73,61 @@ public void AddDIClrTypeMappings()
mapper.GetGraphTypeFromClrType(typeof(Class3), false, typeof(Class4)).ShouldBe(typeof(Class4));
}

[Fact]
public void AddDI()
{
// Setup expectations for AddDIGraphBases
var requiredTypes = new List<Type> {
typeof(DIObjectGraphType<>),
typeof(DIObjectGraphType<,>),
typeof(Base1),
typeof(Base2)
};
var registeredTypes = new List<Type>();
_mockServiceRegister.Setup(x => x.TryRegister(It.IsAny<Type>(), It.IsAny<Type>(), ServiceLifetime.Transient, RegistrationCompareMode.ServiceType))
.Returns<Type, Type, ServiceLifetime, RegistrationCompareMode>((serviceType, implementationType, _, _) => {
if (serviceType == implementationType && requiredTypes.Contains(serviceType)) {
registeredTypes.Add(serviceType);
return _mockServiceRegister.Object;
}
if (!implementationType.IsClass || implementationType.IsAbstract || !typeof(IDIObjectGraphBase).IsAssignableFrom(implementationType)) {
throw new InvalidOperationException($"Invalid type registration: {implementationType}");
}
if (serviceType != implementationType) {
throw new InvalidOperationException($"Service type {serviceType} does not match implementation type {implementationType}");
}
registeredTypes.Add(implementationType);
return _mockServiceRegister.Object;
})
.Verifiable();

// Setup expectations for AddDIClrTypeMappings
IGraphTypeMappingProvider? mapper = null;
_mockServiceRegister.Setup(x => x.Register(typeof(IGraphTypeMappingProvider), It.IsAny<IGraphTypeMappingProvider>(), false))
.Returns<Type, IGraphTypeMappingProvider, bool>((_, m, _) => {
mapper = m;
return _mockServiceRegister.Object;
});

// Call AddDI with specific assembly
_graphQLBuilder.AddDI().ShouldBe(_graphQLBuilder);

// Verify all expectations were met
_mockGraphQLBuilder.Verify();
_mockServiceRegister.Verify();

// Verify required types were registered by AddDI
foreach (var type in requiredTypes) {
registeredTypes.ShouldContain(type);
}

// Verify mapper was created and works correctly
mapper.ShouldNotBeNull();
mapper.GetGraphTypeFromClrType(typeof(Class1), false, null).ShouldBe(typeof(DIObjectGraphType<Base1, Class1>));
mapper.GetGraphTypeFromClrType(typeof(Class2), false, null).ShouldBe(typeof(DIObjectGraphType<Base2, Class2>));
mapper.GetGraphTypeFromClrType(typeof(Class3), false, null).ShouldBeNull();
}

private class Class1 { }
private class Class2 { }
private class Class3 { }
Expand Down
Loading