Skip to content

Commit

Permalink
Stop forcing people to register IConfigurationRoot in DI services
Browse files Browse the repository at this point in the history
This adds an overload of the AddExternalServices method that let you pass an IConfiguration in directly, and marks the old one where it assumes it can dig IConfigurationRoot out of the dependencies as Obsolete.

Also added tests for whole external services functionality, since we didn't have any before.
  • Loading branch information
idg10 committed Feb 3, 2023
1 parent b3da060 commit cdfac80
Show file tree
Hide file tree
Showing 8 changed files with 210 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ public Uri ResolveUrl<TService>(string operationId, params (string Name, object?
Type serviceType = typeof(TService);
if (!this.uriTemplateProviders.TryGetValue(serviceType, out (OpenApiDocumentProvider Provider, string ConfigKey) providerAndKey))
{
throw new ArgumentException($"Service of type '{serviceType.FullName}' registered");
throw new ArgumentException($"Service of type '{serviceType.FullName}' does not seem to have been registered with a call to {nameof(this.AddExternalServiceWithEmbeddedDefinition)}");
}

ResolvedOperationRequestInfo relativeUrl = providerAndKey.Provider.GetResolvedOperationRequestInfo(operationId, parameters);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,34 @@ public static void AddSwaggerEndpoint(this IOpenApiDocuments documents)
documents.Add(SwaggerService.BuildSwaggerDocument());
}

/// <summary>
/// Enables resolution of URLs to external services.
/// </summary>
/// <param name="services">The service collection.</param>
/// <param name="configurationSection">
/// The configuration section in which external service base URLs are configured.
/// </param>
/// <param name="configure">A callback for registering external services.</param>
/// <returns>The service collection, to enable chaining.</returns>
public static IServiceCollection AddExternalServices(
this IServiceCollection services,
IConfiguration configurationSection,
Action<IOpenApiExternalServices> configure)
{
services.AddSingleton(sp =>
{
IOpenApiExternalServices result = new OpenApiExternalServices(
configurationSection,
sp.GetRequiredService<ILogger<OpenApiDocumentProvider>>());

configure(result);

return result;
});

return services;
}

/// <summary>
/// Enables resolution of URLs to external services.
/// </summary>
Expand All @@ -212,6 +240,18 @@ public static void AddSwaggerEndpoint(this IOpenApiDocuments documents)
/// </param>
/// <param name="configure">A callback for registering external services.</param>
/// <returns>The service collection, to enable chaining.</returns>
/// <remarks>
/// This overload dates back to when the only way to obtain an IConfiguration from Azure
/// Functions was via DI. This was problematic because it meant we ended up forcing the
/// configuration object to be put into DI. Although that's fairly common, it's an
/// unpleasant practice because it means we take dependencies on textual settings in
/// IConfiguration intead of statically-typed configuration objects. In this case, the
/// dynamic resolution of configuration by text is unavoidable because of the dynamic
/// nature of the lookup, but we still don't want to force configuration to be made
/// available via DI. So we now provide a preferred overload to which you pass an
/// <see cref="IConfiguration"/>.
/// </remarks>
[Obsolete("The overload that accepts an IConfiguration is preferred.")]
public static IServiceCollection AddExternalServices(
this IServiceCollection services,
string configurationSectionName,
Expand Down
18 changes: 18 additions & 0 deletions Solutions/Menes.Specs/Fakes/ExternalOpenApiService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// <copyright file="ExternalOpenApiService.cs" company="Endjin Limited">
// Copyright (c) Endjin Limited. All rights reserved.
// </copyright>

namespace Menes.Specs.Fakes;

/// <summary>
/// A class representing an OpenApi service for use by the IOpenApiExternalServices tests.
/// </summary>
/// <remarks>
/// The tests never hit any of the endpoints for this service. The class's only purpose in the
/// test is to enable the code under test to locate the OpenAPI YAML. So the only thing we need
/// here is the <see cref="EmbeddedOpenApiDefinitionAttribute"/>.
/// </remarks>
[EmbeddedOpenApiDefinition("Menes.Specs.Fakes.ExternalOpenApiService.yaml")]
public class ExternalOpenApiService : IOpenApiService
{
}
26 changes: 26 additions & 0 deletions Solutions/Menes.Specs/Fakes/ExternalOpenApiService.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
openapi: "3.0.0"
info:
version: 1.0.0
title: Service definition for testing external service resolution
paths:
'/test/{pathElement1}/path/{pathElement2}':
get:
operationId: getWithTwoPathParameters
parameters:
- name: pathElement1
in: path
required: true
schema:
type: string
- name: pathElement2
in: path
required: true
schema:
type: int64
responses:
'200':
description: Accepted
content:
'application/json':
schema:
type: string
15 changes: 15 additions & 0 deletions Solutions/Menes.Specs/Features/ExternalServices.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
Feature: ExternalServices

As a developer writing a web service
I need to be able to generate URLs for other Menes-based services
So that my service can provide client applications with links to relevant entities


Scenario: Pass IConfiguration during registration
Given my configuration contains the setting 'MyExternalService' with the value 'http://example.com:1234'
And I have registered an external service with an embedded definition, passing an IConfiguration directly, and a base URL configuration key of 'MyExternalService'
When I resolve an external service URL for the operation 'getWithTwoPathParameters' with these parameters
| Name | Type | Value |
| pathElement1 | System.String | p1 |
| pathElement2 | System.Int64 | 1234 |
Then the resolved external service URL is 'http://example.com:1234/test/p1/path/1234'
2 changes: 2 additions & 0 deletions Solutions/Menes.Specs/Menes.Specs.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,15 @@
<ProjectReference Include="..\Menes.Hosting.FunctionsWorker\Menes.Hosting.AzureFunctionsWorker.csproj" />
</ItemGroup>
<ItemGroup>
<None Remove="Fakes\ExternalOpenApiService.yaml" />
<None Remove="Steps\TestClasses\OpenApiWebLinkResolverTest.yaml" />
</ItemGroup>
<ItemGroup>
<SpecFlowObsoleteCodeBehindFiles Remove="Features\JsonTypeConversion\ByteArrayOutputParsing - Copy.feature.cs" />
<SpecFlowObsoleteCodeBehindFiles Remove="Features\ParameterBuilders\ArrayInputParsing.feature.cs" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Fakes\ExternalOpenApiService.yaml" />
<EmbeddedResource Include="Steps\TestClasses\OpenApiWebLinkResolverTest.yaml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</EmbeddedResource>
Expand Down
36 changes: 36 additions & 0 deletions Solutions/Menes.Specs/Steps/ConfigurationSteps.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// <copyright file="ConfigurationSteps.cs" company="Endjin Limited">
// Copyright (c) Endjin Limited. All rights reserved.
// </copyright>

namespace Menes.Specs.Steps;

using System;
using System.Collections.Generic;

using Microsoft.Extensions.Configuration;

using TechTalk.SpecFlow;

/// <summary>
/// Steps for working with <see cref="IConfiguration"/>.
/// </summary>
[Binding]
public class ConfigurationSteps
{
private readonly Dictionary<string, string?> settings = new();
private IConfigurationRoot? builtConfiguration = null;

public IConfigurationRoot ConfigurationRoot => this.builtConfiguration ??=
new ConfigurationBuilder().AddInMemoryCollection(this.settings).Build();

[Given("my configuration contains the setting '([^']*)' with the value '([^']*)'")]
public void GivenMyConfigurationContainsTheSettingWithTheValue(string settingName, string settingValue)
{
if (this.builtConfiguration is not null)
{
throw new InvalidOperationException("The IConfigurationRoot has already been created (because some other test step asked for it) so it is too late to add more configuration settings");
}

this.settings.Add(settingName, settingValue);
}
}
72 changes: 72 additions & 0 deletions Solutions/Menes.Specs/Steps/ExternalServicesSteps.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// <copyright file="ExternalServicesSteps.cs" company="Endjin Limited">
// Copyright (c) Endjin Limited. All rights reserved.
// </copyright>

namespace Menes.Specs.Steps;

using System;
using System.Collections.Generic;

using Menes.Specs.Fakes;

using Microsoft.Extensions.DependencyInjection;

using NUnit.Framework;

using TechTalk.SpecFlow;
using TechTalk.SpecFlow.Assist;

/// <summary>
/// Steps for testing <see cref="IOpenApiExternalServices"/>.
/// </summary>
[Binding]
public class ExternalServicesSteps
{
private readonly ConfigurationSteps configuration;
private readonly ServiceCollection services = new();
private IOpenApiExternalServices? externalServices;
private Uri? resolvedUrl;

public ExternalServicesSteps(ConfigurationSteps configuration)
{
this.configuration = configuration;

this.services.AddLogging();
}

[Given("I have registered an external service with an embedded definition, passing an IConfiguration directly, and a base URL configuration key of '([^']*)'")]
public void GivenIHaveRegisteredAnExternalServiceWithAnEmbeddedDefinitionPassingAnIConfigurationAndAConfigurationKey(
string configurationKey)
{
if (this.externalServices is not null)
{
throw new InvalidOperationException("External service registration is already complete");
}

this.services.AddExternalServices(
this.configuration.ConfigurationRoot,
externalServices => externalServices.AddExternalServiceWithEmbeddedDefinition<ExternalOpenApiService>(configurationKey));
}

[When("I resolve an external service URL for the operation '([^']*)' with these parameters")]
public void WhenIResolveAnExternalServiceUrlForTheOperationWithTheseParameters(
string operationId, Table table)
{
List<(string Name, object? Value)> parameters = new();
foreach ((string name, string type, string valueText) in table.CreateSet<(string Name, string Type, string Value)>())
{
object value = Convert.ChangeType(valueText, Type.GetType(type) ?? throw new InvalidOperationException($"Unrecognized type {type}"));
parameters.Add((name, value));
}

this.externalServices ??= this.services.BuildServiceProvider().GetRequiredService<IOpenApiExternalServices>();

this.resolvedUrl = this.externalServices.ResolveUrl<ExternalOpenApiService>(operationId, parameters.ToArray());
}

[Then("the resolved external service URL is '([^']*)'")]
public void ThenTheResolvedExternalServiceURLIs(Uri expectedUrl)
{
Assert.AreEqual(expectedUrl, this.resolvedUrl);
}
}

0 comments on commit cdfac80

Please sign in to comment.