Skip to content

Commit

Permalink
Implement and test TryAddOptions
Browse files Browse the repository at this point in the history
  • Loading branch information
StefanGreve committed Oct 26, 2024
1 parent 95e97fa commit 357321e
Show file tree
Hide file tree
Showing 9 changed files with 226 additions and 33 deletions.
16 changes: 8 additions & 8 deletions AdvancedSystems.Core.Tests/AdvancedSystems.Core.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,20 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="8.0.8" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="8.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.0-release-24373-02" />
<PackageReference Include="Moq" Version="4.20.70" />
<PackageReference Include="XUnit" Version="2.9.0" />
<PackageReference Include="XUnit.Analyzers" Version="1.16.0-pre.22">
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="8.0.10" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="8.0.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="XUnit" Version="2.9.2" />
<PackageReference Include="XUnit.Analyzers" Version="1.16.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="XUnit.Runner.Console" Version="2.9.0">
<PackageReference Include="XUnit.Runner.Console" Version="2.9.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="XUnit.Runner.VisualStudio" Version="3.0.0-pre.24">
<PackageReference Include="XUnit.Runner.VisualStudio" Version="3.0.0-pre.42">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
using System.Collections.Generic;

using AdvancedSystems.Core.DependencyInjection;

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;

using Xunit;

namespace AdvancedSystems.Core.Tests.DependencyInjection;

public sealed class ServiceCollectionExtensionsHelperTests
{
public record MyOptions
{
public string? ConnectionString { get; set; }

public string? Port { get; set; }
}

#region Tests

/// <summary>
/// Tests that <seealso cref="ServiceCollectionExtensions.TryAddOptions{TOptions}(IServiceCollection, IConfigurationSection)"/>
/// configures options from configuration sections only once.
/// </summary>
[Fact]
public void TestTryAddOptions_FromConfiguration()
{
// Arrange
var services = new ServiceCollection();
var expectedOption = new MyOptions
{
ConnectionString = "localhost",
Port = "443"
};

var configurationSection = new ConfigurationBuilder()
.AddInMemoryCollection(
[
new KeyValuePair<string, string?>($"{nameof(MyOptions)}:{nameof(MyOptions.ConnectionString)}", expectedOption.ConnectionString),
new KeyValuePair<string, string?>($"{nameof(MyOptions)}:{nameof(MyOptions.Port)}", expectedOption.Port),
])
.Build()
.GetSection(nameof(MyOptions));

// Act
services.TryAddOptions<MyOptions>(configurationSection);
services.TryAddOptions<MyOptions>(configurationSection);
var serviceProvider = services.BuildServiceProvider();
var actualOption = serviceProvider.GetService<IOptions<MyOptions>>();
var optionsMonitor = serviceProvider.GetRequiredService<IOptionsMonitor<MyOptions>>();

// Assert
Assert.Multiple(() =>
{
Assert.NotNull(actualOption);
Assert.Equal(expectedOption.ConnectionString, actualOption.Value.ConnectionString);
Assert.Equal(expectedOption.Port, actualOption.Value.Port);
Assert.Single(services, service => service.ServiceType == typeof(IConfigureOptions<MyOptions>));
});
}

/// <summary>
/// Tests that <seealso cref="ServiceCollectionExtensions.TryAddOptions{TOptions}(IServiceCollection, System.Action{TOptions})"/>
/// configures options from the action builder only once.
/// </summary>
[Fact]
public void TestTryAddOptions_FromActionsBuilder()
{
// Arrange
var services = new ServiceCollection();
var expectedOption = new MyOptions
{
ConnectionString = "localhost",
Port = "443"
};
int counter = 0;

// Act
services.TryAddOptions<MyOptions>(options =>
{
counter++;
options.ConnectionString = expectedOption.ConnectionString;
options.Port = expectedOption.Port;
});

services.TryAddOptions<MyOptions>(_ => counter++);
services.TryAddOptions<MyOptions>(_ => counter++);
services.TryAddOptions<MyOptions>(_ => counter++);

var serviceProvider = services.BuildServiceProvider();
var actualOption = serviceProvider.GetService<IOptions<MyOptions>>();

Assert.Multiple(() =>
{
Assert.NotNull(actualOption);
Assert.Equal(1, counter);
Assert.Equal(expectedOption.ConnectionString, actualOption.Value.ConnectionString);
Assert.Equal(expectedOption.Port, actualOption.Value.Port);
});
}

#endregion
}
9 changes: 6 additions & 3 deletions AdvancedSystems.Core.Tests/Services/CachingServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,12 @@ public async Task TestCachingRoundtrip_HappyPath()
Person? actual = await this._fixture.CachingService.GetAsync(key, PersonContext.Default.Person, CancellationToken.None);

// Assert
Assert.NotNull(actual);
Assert.Equal(expected.FirstName, actual?.FirstName);
Assert.Equal(expected.LastName, actual?.LastName);
Assert.Multiple(() =>
{
Assert.NotNull(actual);
Assert.Equal(expected.FirstName, actual?.FirstName);
Assert.Equal(expected.LastName, actual?.LastName);
});
this._fixture.SerializationService.VerifyAll();
this._fixture.DistributedCache.Verify(service => service.SetAsync(It.IsAny<string>(), It.IsAny<byte[]>(), It.IsAny<DistributedCacheEntryOptions>(), It.IsAny<CancellationToken>()), Times.Once);

Expand Down
23 changes: 13 additions & 10 deletions AdvancedSystems.Core.Tests/Services/CompressionServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,16 +42,19 @@ public void TestCompressionRoundtrip()
var expandedBuffer = this._sut.CompressionService.Expand(compressedBuffer);

// Assert
Assert.NotEmpty(compressedBuffer);
Assert.True(buffer.Length > compressedBuffer.Length);
Assert.Equal(buffer, expandedBuffer);
this._sut.Logger.Verify(x => x.Log(
LogLevel.Trace,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains(Enum.GetName(compressionLevel)!)),
It.IsAny<Exception>(),
It.Is<Func<It.IsAnyType, Exception?, string>>((v, t) => true))
);
Assert.Multiple(() =>
{
Assert.NotEmpty(compressedBuffer);
Assert.True(buffer.Length > compressedBuffer.Length);
Assert.Equal(buffer, expandedBuffer);
this._sut.Logger.Verify(x => x.Log(
LogLevel.Trace,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains(Enum.GetName(compressionLevel)!)),
It.IsAny<Exception>(),
It.Is<Func<It.IsAnyType, Exception?, string>>((v, t) => true))
);
});
}

[Fact]
Expand Down
20 changes: 13 additions & 7 deletions AdvancedSystems.Core.Tests/Services/MessageBusTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,13 @@ public async Task TestPublishSubscribeRoundtrip_HappyPath()
bool wasRemoved = messageBus.Unregister(channelName);

// Assert
Assert.True(wasCreated);
Assert.True(wasRemoved);
Assert.NotNull(actual);
Assert.Equal(expected.Id, actual.Id);
Assert.Multiple(() =>
{
Assert.True(wasCreated);
Assert.True(wasRemoved);
Assert.NotNull(actual);
Assert.Equal(expected.Id, actual.Id);
});
}

[Fact]
Expand All @@ -67,9 +70,12 @@ public async Task TestPublishSubscribeRoundtrip_UnhappyPath()
bool wasRemoved = messageBus.Unregister(channelName);

// Assert
Assert.True(wasCreated);
Assert.True(wasRemoved);
Assert.Null(actual);
Assert.Multiple(() =>
{
Assert.True(wasCreated);
Assert.True(wasRemoved);
Assert.Null(actual);
});
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,11 @@ public void TestSerializationRoundtrip()
var actual = this._sut.SerializationService.Deserialize(serialized, PersonContext.Default.Person);

// Assert
Assert.NotNull(actual);
Assert.Equal(expected, actual);
Assert.Multiple(() =>
{
Assert.NotNull(actual);
Assert.Equal(expected, actual);
});
}

[Fact]
Expand Down
6 changes: 4 additions & 2 deletions AdvancedSystems.Core/AdvancedSystems.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@
<ItemGroup>
<PackageReference Include="AdvancedSystems.Core.Abstractions" Version="8.0.0-alpha.9" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0-rc.2.24473.5" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.0-rc.2.24473.5" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Options.DataAnnotations" Version="8.0.0" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
using System;
using System.Linq;

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;

namespace AdvancedSystems.Core.DependencyInjection;

public static partial class ServiceCollectionExtensions
{
/// <summary>
/// Registers and binds <typeparamref name="TOptions"/> to the underlying <paramref name="services"/> collection
/// if it has not already been registered, and binds the options to values from the given <paramref name="configurationSection"/>.
/// </summary>
/// <typeparam name="TOptions">
/// The type of the options to register and configure.
/// </typeparam>
/// <param name="services">
/// The service collection containing the service.
/// </param>
/// <param name="configurationSection">
/// A section of the application configuration.
/// </param>
/// <returns>
/// The value of <paramref name="services"/>.
/// </returns>
public static IServiceCollection TryAddOptions<TOptions>(this IServiceCollection services, IConfigurationSection configurationSection) where TOptions : class
{
bool hasOptions = services.Any(service => service.ServiceType == typeof(IConfigureOptions<TOptions>));

if (!hasOptions)
{
services.AddOptions<TOptions>()
.Bind(configurationSection)
.ValidateDataAnnotations()
.ValidateOnStart();
}

return services;
}

/// <summary>
///
/// </summary>
/// <typeparam name="TOptions">
/// The type of the options to register and configure.
/// </typeparam>
/// <param name="services">
/// The service collection containing the service.
/// </param>
/// <param name="configureOptions">
/// An action to configure the options of type <typeparamref name="TOptions"/>.
/// </param>
/// <returns>
/// The value of <paramref name="services"/>.
/// </returns>
public static IServiceCollection TryAddOptions<TOptions>(this IServiceCollection services, Action<TOptions> configureOptions) where TOptions : class, new()
{
services.TryAddSingleton(_ =>
{
var options = new TOptions();
configureOptions(options);
return Options.Create(options);
});

return services;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

namespace AdvancedSystems.Core.DependencyInjection;

public static class ServiceCollectionExtensions
public static partial class ServiceCollectionExtensions
{
public static IServiceCollection AddCachingService(this IServiceCollection services)
{
Expand Down

0 comments on commit 357321e

Please sign in to comment.