Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added HostApplicationBuilder extensions #8466

Merged
merged 3 commits into from
Jun 5, 2023
Merged
Show file tree
Hide file tree
Changes from 2 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
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Linq;
using Microsoft.Extensions.DependencyInjection;
using Orleans.Hosting;
using Orleans.Runtime;
Expand All @@ -10,6 +11,37 @@ namespace Microsoft.Extensions.Hosting
/// </summary>
public static class OrleansClientGenericHostExtensions
{
private static readonly Type MarkerType = typeof(OrleansBuilderMarker);

/// <summary>
/// Configures the host app builder to host an Orleans client.
/// </summary>
/// <param name="hostAppBuilder">The host app builder.</param>
/// <param name="configureDelegate">The delegate used to configure the client.</param>
/// <returns>The host builder.</returns>
/// <remarks>
/// Calling this method multiple times on the same <see cref="IClientBuilder"/> instance will result in one client being configured.
/// However, the effects of <paramref name="configureDelegate"/> will be applied once for each call.
/// Note that this method shouldn't be used in conjunction with HostApplicationBuilder.UseOrleans, since UseOrleans includes a client automatically.
/// </remarks>
/// <exception cref="ArgumentNullException"><paramref name="hostAppBuilder"/> was null or <paramref name="configureDelegate"/> was null.</exception>
public static HostApplicationBuilder UseOrleansClient(
this HostApplicationBuilder hostAppBuilder,
Action<IClientBuilder> configureDelegate)
{
ArgumentNullException.ThrowIfNull(hostAppBuilder);
ArgumentNullException.ThrowIfNull(configureDelegate);

if (hostAppBuilder.Services.Any(s => s.ServiceType.Equals(MarkerType)))
{
throw GetOrleansSiloAddedException();
}

hostAppBuilder.Services.AddOrleansClient(configureDelegate);

return hostAppBuilder;
}

/// <summary>
/// Configures the host builder to host an Orleans client.
/// </summary>
Expand All @@ -22,8 +54,8 @@ public static class OrleansClientGenericHostExtensions
/// Note that this method should not be used in conjunction with IHostBuilder.UseOrleans, since UseOrleans includes a client automatically.
/// </remarks>
/// <exception cref="ArgumentNullException"><paramref name="hostBuilder"/> was null or <paramref name="configureDelegate"/> was null.</exception>
public static IHostBuilder UseOrleansClient(this IHostBuilder hostBuilder, Action<IClientBuilder> configureDelegate)
=> hostBuilder.UseOrleansClient((_, clientBuilder) => configureDelegate(clientBuilder));
public static IHostBuilder UseOrleansClient(this IHostBuilder hostBuilder, Action<IClientBuilder> configureDelegate) =>
hostBuilder.UseOrleansClient((_, clientBuilder) => configureDelegate(clientBuilder));

/// <summary>
/// Configures the host builder to host an Orleans client.
Expand All @@ -39,14 +71,16 @@ public static IHostBuilder UseOrleansClient(this IHostBuilder hostBuilder, Actio
/// <exception cref="ArgumentNullException"><paramref name="hostBuilder"/> was null or <paramref name="configureDelegate"/> was null.</exception>
public static IHostBuilder UseOrleansClient(this IHostBuilder hostBuilder, Action<HostBuilderContext, IClientBuilder> configureDelegate)
{
if (hostBuilder == null) throw new ArgumentNullException(nameof(hostBuilder));
if (configureDelegate == null) throw new ArgumentNullException(nameof(configureDelegate));
ArgumentNullException.ThrowIfNull(hostBuilder);
ArgumentNullException.ThrowIfNull(configureDelegate);

if (hostBuilder.Properties.ContainsKey("HasOrleansSiloBuilder"))
{
throw GetOrleansSiloAddedException();
}

hostBuilder.Properties["HasOrleansClientBuilder"] = "true";

return hostBuilder.ConfigureServices((ctx, services) => configureDelegate(ctx, AddOrleansClient(services)));
}

Expand All @@ -64,54 +98,54 @@ public static IHostBuilder UseOrleansClient(this IHostBuilder hostBuilder, Actio
/// <exception cref="ArgumentNullException"><paramref name="services"/> was null or <paramref name="configureDelegate"/> was null.</exception>
public static IServiceCollection AddOrleansClient(this IServiceCollection services, Action<IClientBuilder> configureDelegate)
{
if (configureDelegate == null) throw new ArgumentNullException(nameof(configureDelegate));
ArgumentNullException.ThrowIfNull(configureDelegate);

var clientBuilder = AddOrleansClient(services);

configureDelegate(clientBuilder);
return services;
}

private static IClientBuilder AddOrleansClient(IServiceCollection services)
{
IClientBuilder clientBuilder = default;
foreach (var descriptor in services)
foreach (var descriptor in services.Where(d => d.ServiceType.Equals(MarkerType)))
{
if (descriptor.ServiceType.Equals(typeof(OrleansBuilderMarker)))
var instance = (OrleansBuilderMarker)descriptor.ImplementationInstance;
clientBuilder = instance.BuilderInstance switch
{
var instance = (OrleansBuilderMarker)descriptor.ImplementationInstance;
clientBuilder = instance.Instance switch
{
IClientBuilder existingBuilder => existingBuilder,
_ => throw GetOrleansSiloAddedException()
};
}
IClientBuilder existingBuilder => existingBuilder,
_ => throw GetOrleansSiloAddedException()
};
}

if (clientBuilder is null)
{
clientBuilder = new ClientBuilder(services);
services.Add(new(typeof(OrleansBuilderMarker), new OrleansBuilderMarker(clientBuilder)));
services.AddSingleton(new OrleansBuilderMarker(clientBuilder));
}

return clientBuilder;
}

private static OrleansConfigurationException GetOrleansSiloAddedException() => new("Do not use UseOrleans with UseOrleansClient. If you want a client and server in the same process, only UseOrleans is necessary and the UseOrleansClient call can be removed.");
private static OrleansConfigurationException GetOrleansSiloAddedException() =>
new("Do not use UseOrleans with UseOrleansClient. If you want a client and server in the same process, only UseOrleans is necessary and the UseOrleansClient call can be removed.");
}

/// <summary>
/// Marker type used for storing a builder in a service collection.
/// </summary>
internal sealed class OrleansBuilderMarker
{
/// <summary>
/// Marker type used for storing a client builder in a service collection.
/// Initializes a new instance of the <see cref="OrleansBuilderMarker"/> class.
/// </summary>
internal class OrleansBuilderMarker
{
/// <summary>
/// Initializes a new instance of the <see cref="OrleansBuilderMarker"/> class.
/// </summary>
/// <param name="builderInstance">The builder instance.</param>
public OrleansBuilderMarker(object builderInstance) => Instance = builderInstance;

/// <summary>
/// Gets the builder instance.
/// </summary>
public object Instance { get; }
}
/// <param name="builderInstance">The builder instance.</param>
public OrleansBuilderMarker(object builderInstance) => BuilderInstance = builderInstance;

/// <summary>
/// Gets the builder instance.
/// </summary>
public object BuilderInstance { get; }
}
}
2 changes: 1 addition & 1 deletion src/Orleans.Core/Orleans.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
<PackageReference Include="Microsoft.Extensions.ObjectPool" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" />
<PackageReference Include="Microsoft.Extensions.Logging" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Hosting" />
<PackageReference Include="Newtonsoft.Json" />
<PackageReference Include="System.Memory.Data" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,54 @@
using System;
using System.Linq;
using Microsoft.Extensions.DependencyInjection;
using Orleans;
using Orleans.Hosting;
using Orleans.Runtime;
using static Microsoft.Extensions.Hosting.OrleansClientGenericHostExtensions;

namespace Microsoft.Extensions.Hosting
{
/// <summary>
/// Extension methods for <see cref="IHostBuilder"/>.
/// </summary>
public static class GenericHostExtensions
public static class OrleansSiloGenericHostExtensions
{
private static readonly Type MarkerType = typeof(OrleansBuilderMarker);

/// <summary>
/// Configures the host app builder to host an Orleans silo.
/// </summary>
/// <param name="hostAppBuilder">The host app builder.</param>
/// <returns>The host builder.</returns>
public static HostApplicationBuilder UseOrleans(
this HostApplicationBuilder hostAppBuilder) =>
hostAppBuilder.UseOrleans(_ => { });

/// <summary>
/// Configures the host builder to host an Orleans silo.
/// </summary>
/// <param name="hostAppBuilder">The host app builder.</param>
/// <param name="configureDelegate">The delegate used to configure the silo.</param>
/// <returns>The host builder.</returns>
/// <remarks>
/// Calling this method multiple times on the same <see cref="HostApplicationBuilder"/> instance will result in one silo being configured.
/// However, the effects of <paramref name="configureDelegate"/> will be applied once for each call.
/// </remarks>
public static HostApplicationBuilder UseOrleans(
this HostApplicationBuilder hostAppBuilder,
Action<ISiloBuilder> configureDelegate)
{
ArgumentNullException.ThrowIfNull(hostAppBuilder);
ArgumentNullException.ThrowIfNull(configureDelegate);

if (hostAppBuilder.Services.Any(service => service.ServiceType.Equals(MarkerType)))
{
throw GetOrleansClientAddedException();
}

hostAppBuilder.Services.AddOrleans(configureDelegate);

return hostAppBuilder;
}

/// <summary>
/// Configures the host builder to host an Orleans silo.
/// </summary>
Expand Down Expand Up @@ -40,8 +77,8 @@ public static IHostBuilder UseOrleans(
this IHostBuilder hostBuilder,
Action<HostBuilderContext, ISiloBuilder> configureDelegate)
{
if (hostBuilder is null) throw new ArgumentNullException(nameof(hostBuilder));
if (configureDelegate == null) throw new ArgumentNullException(nameof(configureDelegate));
ArgumentNullException.ThrowIfNull(hostBuilder);
ArgumentNullException.ThrowIfNull(configureDelegate);

if (hostBuilder.Properties.ContainsKey("HasOrleansClientBuilder"))
{
Expand All @@ -67,39 +104,39 @@ public static IServiceCollection AddOrleans(
this IServiceCollection services,
Action<ISiloBuilder> configureDelegate)
{
if (configureDelegate == null) throw new ArgumentNullException(nameof(configureDelegate));
ArgumentNullException.ThrowIfNull(configureDelegate);

var builder = AddOrleans(services);

configureDelegate(builder);

return services;
}

private static ISiloBuilder AddOrleans(IServiceCollection services)
{
ISiloBuilder builder = default;
foreach (var descriptor in services)
foreach (var descriptor in services.Where(d => d.ServiceType.Equals(MarkerType)))
{
if (descriptor.ServiceType.Equals(typeof(OrleansBuilderMarker)))
var marker = (OrleansBuilderMarker)descriptor.ImplementationInstance;
builder = marker.BuilderInstance switch
{
var instance = (OrleansBuilderMarker)descriptor.ImplementationInstance;
builder = instance.Instance switch
{

ISiloBuilder existingBuilder => existingBuilder,
_ => throw GetOrleansClientAddedException()
};
}

ISiloBuilder existingBuilder => existingBuilder,
_ => throw GetOrleansClientAddedException()
};
}

if (builder is null)
{
builder = new SiloBuilder(services);
services.Add(new(typeof(OrleansBuilderMarker), new OrleansBuilderMarker(builder)));
services.AddSingleton(new OrleansBuilderMarker(builder));
}

return builder;
}

private static OrleansConfigurationException GetOrleansClientAddedException() => new("Do not call both UseOrleansClient/AddOrleansClient with UseOrleans/AddOrleans. If you want a client and server in the same process, only UseOrleans/AddOrleans is necessary and the UseOrleansClient/AddOrleansClient call can be removed.");
private static OrleansConfigurationException GetOrleansClientAddedException() =>
new("Do not call both UseOrleansClient/AddOrleansClient with UseOrleans/AddOrleans. If you want a client and server in the same process, only UseOrleans/AddOrleans is necessary and the UseOrleansClient/AddOrleansClient call can be removed.");
}
}
9 changes: 2 additions & 7 deletions test/DefaultCluster.Tests/HostedClientTests.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Orleans;
using Orleans.Concurrency;
using Orleans.Configuration;
using Orleans.Hosting;
using Orleans.Providers;
using Orleans.Runtime;
using Orleans.Streams;
Expand Down Expand Up @@ -39,8 +34,8 @@ public Fixture()
public async Task InitializeAsync()
{
var (siloPort, gatewayPort) = portAllocator.AllocateConsecutivePortPairs(1);
Host = new HostBuilder()
.UseOrleans((ctx, siloBuilder) =>
Host = Microsoft.Extensions.Hosting.Host.CreateApplicationBuilder()
.UseOrleans(siloBuilder =>
{
siloBuilder
.UseLocalhostClustering(siloPort, gatewayPort)
Expand Down
21 changes: 19 additions & 2 deletions test/NonSilo.Tests/ClientBuilderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ public void ClientBuilder_ServiceProviderTest()
Assert.Throws<ArgumentNullException>(() => hostBuilder.ConfigureServices(null));

var registeredFirst = new int[1];

var one = new MyService { Id = 1 };
hostBuilder.ConfigureServices(
services =>
Expand All @@ -196,7 +196,7 @@ public void ClientBuilder_ServiceProviderTest()
var client = host.Services.GetRequiredService<IClusterClient>();
var services = client.ServiceProvider.GetServices<MyService>()?.ToList();
Assert.NotNull(services);

// Both services should be registered.
Assert.Equal(2, services.Count);
Assert.NotNull(services.FirstOrDefault(svc => svc.Id == 1));
Expand Down Expand Up @@ -226,6 +226,23 @@ public void ClientBuilderThrowsDuringStartupIfSiloBuildersAdded()
});
}

[Fact]
public void ClientBuilderWithHotApplicationBuilderThrowsDuringStartupIfSiloBuildersAdded()
{
Assert.Throws<OrleansConfigurationException>(() =>
{
_ = Host.CreateApplicationBuilder()
.UseOrleans(siloBuilder =>
{
siloBuilder.UseLocalhostClustering();
})
.UseOrleansClient(clientBuilder =>
{
clientBuilder.UseLocalhostClustering();
});
});
}

private static void RemoveConfigValidators(IServiceCollection services)
{
var validators = services.Where(descriptor => descriptor.ServiceType == typeof(IConfigurationValidator)).ToList();
Expand Down
17 changes: 17 additions & 0 deletions test/NonSilo.Tests/SiloBuilderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,23 @@ public void SiloBuilderThrowsDuringStartupIfClientBuildersAdded()
});
}

[Fact]
public void SiloBuilderWithHotApplicationBuilderThrowsDuringStartupIfClientBuildersAdded()
{
Assert.Throws<OrleansConfigurationException>(() =>
{
_ = Host.CreateApplicationBuilder()
.UseOrleansClient(clientBuilder =>
{
clientBuilder.UseLocalhostClustering();
})
.UseOrleans(siloBuilder =>
{
siloBuilder.UseLocalhostClustering();
});
});
}

private class FakeHostEnvironmentStatistics : IHostEnvironmentStatistics
{
public long? TotalPhysicalMemory => 0;
Expand Down