Skip to content

Commit 0eb7a91

Browse files
committed
Support DI in SignalR.OpenApi example providers
Enable example providers to use constructor injection via DI. Update ReplyToMessageExamplesProvider to use IDateTimeProvider from DI. Add new DI-based example provider and hub for testing. Refactor SignalROpenApiDocumentGenerator to resolve providers using ActivatorUtilities. Add unit test to verify DI support in example providers.
1 parent 28ebf1d commit 0eb7a91

12 files changed

Lines changed: 209 additions & 37 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Copyright (c) SignalR.OpenApi Contributors. Licensed under the MIT License.
2+
3+
namespace SignalR.OpenApi.Sample.Hubs;
4+
5+
/// <summary>
6+
/// Provides the current local date and time.
7+
/// </summary>
8+
public class DateTimeProvider : IDateTimeProvider
9+
{
10+
/// <inheritdoc/>
11+
public DateTimeOffset Now() => DateTimeOffset.Now;
12+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Copyright (c) SignalR.OpenApi Contributors. Licensed under the MIT License.
2+
3+
namespace SignalR.OpenApi.Sample.Hubs;
4+
5+
/// <summary>
6+
/// Provides the current date and time.
7+
/// </summary>
8+
public interface IDateTimeProvider
9+
{
10+
/// <summary>
11+
/// Gets the current local date and time.
12+
/// </summary>
13+
/// <returns>The current local date and time as a <see cref="DateTimeOffset"/>.</returns>
14+
DateTimeOffset Now();
15+
}

samples/SignalR.OpenApi.Sample/Hubs/ReplyToMessageExamplesProvider.cs

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,22 @@ namespace SignalR.OpenApi.Sample.Hubs;
99
/// </summary>
1010
public class ReplyToMessageExamplesProvider : ISignalROpenApiExamplesProvider<ReplyToMessageRequest>
1111
{
12+
private readonly IDateTimeProvider dateTimeProvider;
13+
14+
/// <summary>
15+
/// Initializes a new instance of the <see cref="ReplyToMessageExamplesProvider"/> class.
16+
/// </summary>
17+
/// <param name="dateTimeProvider">The date and time provider.</param>
18+
public ReplyToMessageExamplesProvider(IDateTimeProvider dateTimeProvider)
19+
{
20+
this.dateTimeProvider = dateTimeProvider;
21+
}
22+
1223
/// <inheritdoc/>
1324
public IEnumerable<SignalROpenApiExample<ReplyToMessageRequest>> GetExamples()
1425
{
26+
var now = this.dateTimeProvider.Now();
27+
1528
yield return new SignalROpenApiExample<ReplyToMessageRequest>(
1629
"ReplyToGreeting",
1730
new ReplyToMessageRequest
@@ -20,13 +33,13 @@ public IEnumerable<SignalROpenApiExample<ReplyToMessageRequest>> GetExamples()
2033
{
2134
User = "Alice",
2235
Message = "Hello, everyone!",
23-
Timestamp = new DateTimeOffset(2026, 2, 15, 10, 0, 0, TimeSpan.Zero),
36+
Timestamp = now,
2437
},
2538
Reply = new ChatMessage
2639
{
2740
User = "Bob",
2841
Message = "Hi Alice, welcome!",
29-
Timestamp = new DateTimeOffset(2026, 2, 15, 10, 1, 0, TimeSpan.Zero),
42+
Timestamp = now,
3043
},
3144
})
3245
{
@@ -41,13 +54,13 @@ public IEnumerable<SignalROpenApiExample<ReplyToMessageRequest>> GetExamples()
4154
{
4255
User = "Bob",
4356
Message = "What time is the meeting?",
44-
Timestamp = new DateTimeOffset(2026, 2, 15, 14, 0, 0, TimeSpan.Zero),
57+
Timestamp = now,
4558
},
4659
Reply = new ChatMessage
4760
{
4861
User = "Alice",
4962
Message = "It starts at 3 PM.",
50-
Timestamp = new DateTimeOffset(2026, 2, 15, 14, 2, 0, TimeSpan.Zero),
63+
Timestamp = now,
5164
},
5265
})
5366
{

samples/SignalR.OpenApi.Sample/Program.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
var builder = WebApplication.CreateBuilder(args);
88

9+
builder.Services.AddSingleton<IDateTimeProvider, DateTimeProvider>();
910
builder.Services.AddSignalR(options =>
1011
{
1112
options.EnableDetailedErrors = true;

src/SignalR.OpenApi.SwaggerUi/Resources/signalr-openapi-plugin.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -622,7 +622,8 @@ var SignalROpenApiPlugin = function (system) {
622622
return React.createElement(Original, props);
623623
};
624624
},
625-
// Hide "No parameters" section for SignalR operations
625+
// Hide "No parameters" message for SignalR operations while
626+
// preserving the "Try it out" button that lives in this component.
626627
parameters: function (Original, system) {
627628
return function (props) {
628629
var React = system.React;
@@ -633,7 +634,9 @@ var SignalROpenApiPlugin = function (system) {
633634
var params = props.parameters;
634635
var count = params ? (params.size != null ? params.size : params.length) : 0;
635636
if (count === 0) {
636-
return null;
637+
return React.createElement("div", { className: "signalr-no-params" },
638+
React.createElement(Original, props)
639+
);
637640
}
638641
}
639642

src/SignalR.OpenApi.SwaggerUi/Resources/signalr-openapi.css

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,12 @@
5858
background: #e02020 !important;
5959
}
6060

61+
/* Hide the "No parameters" message for SignalR operations
62+
while keeping the "Try it out" button visible. */
63+
.signalr-no-params .opblock-description-wrapper .opblock-description {
64+
display: none;
65+
}
66+
6167
/* Client event log panel */
6268
.signalr-event-panel {
6369
font-family: sans-serif;

src/SignalR.OpenApi/Generation/SignalROpenApiDocumentGenerator.cs

Lines changed: 12 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Reflection;
66
using System.Text.Json;
77
using System.Text.Json.Serialization;
8+
using Microsoft.Extensions.DependencyInjection;
89
using Microsoft.Extensions.Options;
910
using Microsoft.OpenApi.Models;
1011
using SignalR.OpenApi.Examples;
@@ -1263,20 +1264,14 @@ private Dictionary<string, OpenApiExample> ResolveExamples(Type providerType)
12631264
{
12641265
var result = new Dictionary<string, OpenApiExample>();
12651266

1266-
// Try to resolve from DI first, then fall back to Activator.CreateInstance
1267-
object? provider = null;
1267+
// Create the provider using ActivatorUtilities, which resolves constructor
1268+
// dependencies from DI while allowing types that are not registered themselves.
1269+
object provider;
12681270
try
12691271
{
1270-
provider = this.serviceProvider.GetService(providerType);
1272+
provider = ActivatorUtilities.CreateInstance(this.serviceProvider, providerType);
12711273
}
1272-
catch (InvalidOperationException)
1273-
{
1274-
// Provider not registered in DI
1275-
}
1276-
1277-
provider ??= Activator.CreateInstance(providerType);
1278-
1279-
if (provider is null)
1274+
catch (Exception)
12801275
{
12811276
return result;
12821277
}
@@ -1339,19 +1334,12 @@ private Dictionary<string, OpenApiExample> ResolveExamplesForDerivedType(Type pr
13391334
{
13401335
var result = new Dictionary<string, OpenApiExample>();
13411336

1342-
object? provider = null;
1337+
object provider;
13431338
try
13441339
{
1345-
provider = this.serviceProvider.GetService(providerType);
1346-
}
1347-
catch (InvalidOperationException)
1348-
{
1349-
// Provider not registered in DI
1340+
provider = ActivatorUtilities.CreateInstance(this.serviceProvider, providerType);
13501341
}
1351-
1352-
provider ??= Activator.CreateInstance(providerType);
1353-
1354-
if (provider is null)
1342+
catch (Exception)
13551343
{
13561344
return result;
13571345
}
@@ -1438,19 +1426,12 @@ private Dictionary<string, OpenApiExample> ResolveExamplesForDerivedType(Type pr
14381426

14391427
private object? GetFirstExampleValueFromProvider(Type providerType, Type? filterType = null)
14401428
{
1441-
object? provider = null;
1429+
object provider;
14421430
try
14431431
{
1444-
provider = this.serviceProvider.GetService(providerType);
1445-
}
1446-
catch (InvalidOperationException)
1447-
{
1448-
// Provider not registered in DI
1432+
provider = ActivatorUtilities.CreateInstance(this.serviceProvider, providerType);
14491433
}
1450-
1451-
provider ??= Activator.CreateInstance(providerType);
1452-
1453-
if (provider is null)
1434+
catch (Exception)
14541435
{
14551436
return null;
14561437
}

test/SignalR.OpenApi.Tests/SignalROpenApiDocumentGeneratorTests.cs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// Copyright (c) SignalR.OpenApi Contributors. Licensed under the MIT License.
22

3+
using Microsoft.Extensions.DependencyInjection;
34
using Microsoft.Extensions.Options;
45
using Microsoft.VisualStudio.TestTools.UnitTesting;
56
using SignalR.OpenApi.Discovery;
@@ -861,6 +862,49 @@ public void GenerateDocument_PolymorphicSubEndpoint_FormSchemaHasFilteredPropert
861862
Assert.IsNull(circleFormSchema.Properties["kind"].Example, "ReadOnly discriminator should not have an example.");
862863
}
863864

865+
/// <summary>
866+
/// Verifies that examples providers with constructor dependencies are resolved via DI.
867+
/// </summary>
868+
[TestMethod]
869+
public void GenerateDocument_ExamplesProviderWithDependencyInjection()
870+
{
871+
var services = new ServiceCollection();
872+
services.AddSingleton<ITestValueProvider>(new TestValueProvider("InjectedProduct"));
873+
using var serviceProvider = services.BuildServiceProvider();
874+
875+
var options = new SignalROpenApiOptions
876+
{
877+
Assemblies = [typeof(DiExampleHub).Assembly],
878+
};
879+
880+
var opts = Options.Create(options);
881+
var discoverer = new ReflectionHubDiscoverer(opts);
882+
var generator = new SignalROpenApiDocumentGenerator(opts, serviceProvider);
883+
884+
var hubs = discoverer.DiscoverHubs();
885+
var doc = generator.GenerateDocument(hubs);
886+
887+
var submitRequest = doc.Paths["/hubs/DiExample/SubmitRequest"]
888+
.Operations[Microsoft.OpenApi.Models.OperationType.Post];
889+
890+
Assert.IsNotNull(submitRequest.RequestBody, "SubmitRequest should have a request body.");
891+
892+
var jsonContent = submitRequest.RequestBody.Content["application/json"];
893+
Assert.IsNotNull(jsonContent.Examples, "Request body should have examples.");
894+
Assert.IsTrue(jsonContent.Examples.ContainsKey("DiExample"), "Should contain 'DiExample' example.");
895+
896+
var diExample = jsonContent.Examples["DiExample"];
897+
Assert.AreEqual("Example from DI provider", diExample.Summary);
898+
899+
// Verify the injected value was used
900+
Assert.IsNotNull(diExample.Value, "Example value should not be null.");
901+
var exampleObj = diExample.Value as Microsoft.OpenApi.Any.OpenApiObject;
902+
Assert.IsNotNull(exampleObj, "Example value should be an OpenApiObject.");
903+
var nameValue = exampleObj["name"] as Microsoft.OpenApi.Any.OpenApiString;
904+
Assert.IsNotNull(nameValue, "Name property should exist.");
905+
Assert.AreEqual("InjectedProduct", nameValue.Value, "Name should be the injected value.");
906+
}
907+
864908
private static (ReflectionHubDiscoverer Discoverer, SignalROpenApiDocumentGenerator Generator) CreateServices(
865909
Action<SignalROpenApiOptions>? configure = null)
866910
{
@@ -880,4 +924,16 @@ private sealed class EmptyServiceProvider : IServiceProvider
880924
{
881925
public object? GetService(Type serviceType) => null;
882926
}
927+
928+
private sealed class TestValueProvider : ITestValueProvider
929+
{
930+
private readonly string value;
931+
932+
public TestValueProvider(string value)
933+
{
934+
this.value = value;
935+
}
936+
937+
public string GetValue() => this.value;
938+
}
883939
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Copyright (c) SignalR.OpenApi Contributors. Licensed under the MIT License.
2+
3+
using Microsoft.AspNetCore.SignalR;
4+
using SignalR.OpenApi.Examples;
5+
6+
namespace SignalR.OpenApi.Tests.TestHubs;
7+
8+
/// <summary>
9+
/// A hub demonstrating DI-based example provider attributes.
10+
/// </summary>
11+
public class DiExampleHub : Hub
12+
{
13+
/// <summary>
14+
/// Submits a request using a DI-based examples provider.
15+
/// </summary>
16+
/// <param name="request">The request to submit.</param>
17+
/// <returns>The request identifier.</returns>
18+
[SignalROpenApiRequestExamples(typeof(DiOrderRequestExamplesProvider))]
19+
public Task<string> SubmitRequest(DiRequest request)
20+
{
21+
return Task.FromResult("REQ-001");
22+
}
23+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Copyright (c) SignalR.OpenApi Contributors. Licensed under the MIT License.
2+
3+
using SignalR.OpenApi.Examples;
4+
5+
namespace SignalR.OpenApi.Tests.TestHubs;
6+
7+
/// <summary>
8+
/// An examples provider that requires a dependency injected via the constructor.
9+
/// </summary>
10+
public class DiOrderRequestExamplesProvider : ISignalROpenApiExamplesProvider<DiRequest>
11+
{
12+
private readonly ITestValueProvider valueProvider;
13+
14+
/// <summary>
15+
/// Initializes a new instance of the <see cref="DiOrderRequestExamplesProvider"/> class.
16+
/// </summary>
17+
/// <param name="valueProvider">The test value provider.</param>
18+
public DiOrderRequestExamplesProvider(ITestValueProvider valueProvider)
19+
{
20+
this.valueProvider = valueProvider;
21+
}
22+
23+
/// <inheritdoc/>
24+
public IEnumerable<SignalROpenApiExample<DiRequest>> GetExamples()
25+
{
26+
yield return new SignalROpenApiExample<DiRequest>(
27+
"DiExample",
28+
new DiRequest { Name = this.valueProvider.GetValue() })
29+
{
30+
Summary = "Example from DI provider",
31+
};
32+
}
33+
}

0 commit comments

Comments
 (0)