Skip to content

Commit e2b2d30

Browse files
committed
DI: Register elastic client
1 parent 044cba3 commit e2b2d30

12 files changed

+394
-0
lines changed
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net6.0</TargetFramework>
5+
<LangVersion>11.0</LangVersion>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<Nullable>enable</Nullable>
8+
<AnalysisLevel>7-all</AnalysisLevel>
9+
<GenerateDocumentationFile>true</GenerateDocumentationFile>
10+
</PropertyGroup>
11+
12+
<PropertyGroup>
13+
<Authors>Kyle McClellan</Authors>
14+
<Copyright>%A9 2023 Kyle McClellan</Copyright>
15+
<PackageLicenseExpression>MIT</PackageLicenseExpression>
16+
<Description>An extension of Elastic.Clients.Elasticsearch for use with Microsoft.Extensions.DependencyInjection (and friends).</Description>
17+
<PackageTags>elasticsearch;elastic;search;dependencyinjection;di;ioc</PackageTags>
18+
<PackageProjectUrl>https://github.com/kmcclellan/elasticstretch</PackageProjectUrl>
19+
<PackageReleaseNotes>https://github.com/kmcclellan/elasticstretch/releases/v$(Version)</PackageReleaseNotes>
20+
<PackageReadmeFile>README.md</PackageReadmeFile>
21+
<PackageIcon>icon.png</PackageIcon>
22+
<IncludeSymbols>true</IncludeSymbols>
23+
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
24+
<EmbedUntrackedSources>true</EmbedUntrackedSources>
25+
<PublishRepositoryUrl>true</PublishRepositoryUrl>
26+
</PropertyGroup>
27+
28+
<ItemGroup>
29+
<PackageReference Include="Elastic.Clients.Elasticsearch" Version="*" />
30+
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="*" />
31+
<PackageReference Include="Microsoft.Extensions.Options" Version="*" />
32+
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="*" />
33+
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="*" PrivateAssets="All" />
34+
</ItemGroup>
35+
36+
<ItemGroup>
37+
<None Include="..\README.md;..\icon.png" Pack="true" PackagePath="\" />
38+
</ItemGroup>
39+
40+
</Project>
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
namespace Elastic.Clients.Elasticsearch;
2+
3+
using Elastic.Clients.Elasticsearch.Options;
4+
using Microsoft.Extensions.DependencyInjection;
5+
using Microsoft.Extensions.DependencyInjection.Extensions;
6+
using Microsoft.Extensions.Options;
7+
8+
/// <summary>
9+
/// Extensions of <see cref="IServiceCollection"/> for the Elasticsearch client.
10+
/// </summary>
11+
public static class ElasticstretchServiceCollectionExtensions
12+
{
13+
/// <summary>
14+
/// Adds a singleton <see cref="ElasticsearchClient"/> to the services.
15+
/// </summary>
16+
/// <remarks>
17+
/// Relevant options/configuration:
18+
/// <list type="bullet">
19+
/// <item><see cref="ElasticsearchNodeOptions"/>, bound to <c>Elasticsearch</c></item>
20+
/// <item>
21+
/// <see cref="ElasticsearchCredentialOptions"/>, bound to <c>Elasticsearch:Credentials</c>
22+
/// </item>
23+
/// <item><see cref="ElasticsearchClientOptions"/></item>
24+
/// </list>
25+
/// </remarks>
26+
/// <param name="services">The service collection.</param>
27+
/// <param name="configureSettings">A delegate to configure the client settings.</param>
28+
/// <returns>The same services, for chaining.</returns>
29+
public static IServiceCollection AddElasticsearchClient(
30+
this IServiceCollection services,
31+
Action<ElasticsearchClientSettings>? configureSettings = null)
32+
{
33+
services.AddOptions();
34+
35+
TryConfigure<ElasticsearchCredentialOptions, ConfigureCredentialsFromConfig>(services);
36+
TryConfigure<ElasticsearchNodeOptions, ConfigureNodesFromConfig>(services);
37+
TryConfigure<ElasticsearchNodeOptions, ConfigureNodesFromCredentials>(services);
38+
TryConfigure<ElasticsearchClientOptions, ConfigureClientFromNodes>(services);
39+
40+
services.TryAddSingleton(
41+
x => new ElasticsearchClient(
42+
x.GetRequiredService<IOptions<ElasticsearchClientOptions>>().Value.ToSettings()));
43+
44+
if (configureSettings != null)
45+
{
46+
services.Configure<ElasticsearchClientOptions>(x => x.ConfigureSettings += configureSettings);
47+
}
48+
49+
return services;
50+
}
51+
52+
static void TryConfigure<TOptions, TSetup>(IServiceCollection services)
53+
where TOptions : class
54+
where TSetup : class, IConfigureOptions<TOptions>
55+
{
56+
services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<TOptions>, TSetup>());
57+
}
58+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
using System.Diagnostics.CodeAnalysis;
2+
3+
[assembly: SuppressMessage(
4+
"Performance",
5+
"CA1812:Avoid uninstantiated internal classes",
6+
Scope = "type",
7+
Target = "~T:Elastic.Clients.Elasticsearch.Options.ConfigureCredentialsFromConfig")]
8+
9+
[assembly: SuppressMessage(
10+
"Performance",
11+
"CA1812:Avoid uninstantiated internal classes",
12+
Scope = "type",
13+
Target = "~T:Elastic.Clients.Elasticsearch.Options.ConfigureNodesFromConfig")]
14+
15+
[assembly: SuppressMessage(
16+
"Performance",
17+
"CA1812:Avoid uninstantiated internal classes",
18+
Scope = "type",
19+
Target = "~T:Elastic.Clients.Elasticsearch.Options.ConfigureNodesFromCredentials")]
20+
21+
[assembly: SuppressMessage(
22+
"Performance",
23+
"CA1812:Avoid uninstantiated internal classes",
24+
Scope = "type",
25+
Target = "~T:Elastic.Clients.Elasticsearch.Options.ConfigureClientFromNodes")]
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
namespace Elastic.Clients.Elasticsearch.Options;
2+
3+
using Microsoft.Extensions.Options;
4+
5+
sealed class ConfigureClientFromNodes
6+
: ConfigureOptionsFromOptions<ElasticsearchClientOptions, ElasticsearchNodeOptions>
7+
{
8+
public ConfigureClientFromNodes(IOptionsFactory<ElasticsearchNodeOptions> optionsFactory)
9+
: base(optionsFactory)
10+
{
11+
}
12+
13+
protected override void Configure(ElasticsearchClientOptions options, ElasticsearchNodeOptions dependency)
14+
{
15+
if (dependency.CredentialsHeader != null)
16+
{
17+
options.ConfigureSettings += x => x.Authentication(dependency.CredentialsHeader);
18+
}
19+
20+
var fallback = options.NodePool;
21+
options.NodePool = () => dependency.CreatePool() ?? fallback();
22+
}
23+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
namespace Elastic.Clients.Elasticsearch.Options;
2+
3+
using Microsoft.Extensions.Configuration;
4+
using Microsoft.Extensions.Options;
5+
6+
sealed class ConfigureCredentialsFromConfig : ConfigureFromConfigurationOptions<ElasticsearchCredentialOptions>
7+
{
8+
public ConfigureCredentialsFromConfig(IConfiguration config)
9+
: base(config.GetSection(ConfigurationPath.Combine("Elasticsearch", "Credentials")))
10+
{
11+
}
12+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
namespace Elastic.Clients.Elasticsearch.Options;
2+
3+
using Microsoft.Extensions.Configuration;
4+
using Microsoft.Extensions.Options;
5+
6+
sealed class ConfigureNodesFromConfig : ConfigureFromConfigurationOptions<ElasticsearchNodeOptions>
7+
{
8+
public ConfigureNodesFromConfig(IConfiguration config)
9+
: base(config.GetSection("Elasticsearch"))
10+
{
11+
}
12+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
namespace Elastic.Clients.Elasticsearch.Options;
2+
3+
using Microsoft.Extensions.Options;
4+
5+
sealed class ConfigureNodesFromCredentials
6+
: ConfigureOptionsFromOptions<ElasticsearchNodeOptions, ElasticsearchCredentialOptions>
7+
{
8+
public ConfigureNodesFromCredentials(IOptionsFactory<ElasticsearchCredentialOptions> optionsFactory)
9+
: base(optionsFactory)
10+
{
11+
}
12+
13+
protected override void Configure(ElasticsearchNodeOptions options, ElasticsearchCredentialOptions dependency)
14+
{
15+
var credentials = dependency.CreateHeader();
16+
17+
if (credentials != null)
18+
{
19+
options.CredentialsHeader = credentials;
20+
}
21+
}
22+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
namespace Elastic.Clients.Elasticsearch.Options;
2+
3+
using Microsoft.Extensions.Options;
4+
5+
internal abstract class ConfigureOptionsFromOptions<TOptions, TDep> : IConfigureNamedOptions<TOptions>
6+
where TOptions : class
7+
where TDep : class
8+
{
9+
private readonly IOptionsFactory<TDep> optionsFactory;
10+
11+
public ConfigureOptionsFromOptions(IOptionsFactory<TDep> optionsFactory)
12+
{
13+
this.optionsFactory = optionsFactory;
14+
}
15+
16+
public void Configure(string? name, TOptions options)
17+
{
18+
Configure(options, optionsFactory.Create(name ?? Options.DefaultName));
19+
}
20+
21+
public void Configure(TOptions options)
22+
{
23+
throw new NotSupportedException();
24+
}
25+
26+
protected abstract void Configure(TOptions options, TDep dependency);
27+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
namespace Elastic.Clients.Elasticsearch.Options;
2+
3+
using Elastic.Clients.Elasticsearch.Serialization;
4+
using Elastic.Transport;
5+
6+
/// <summary>
7+
/// A model to configure Elasticsearch clients using the .NET options pattern.
8+
/// </summary>
9+
public class ElasticsearchClientOptions
10+
{
11+
/// <summary>
12+
/// Gets or sets the factory for the client node pool.
13+
/// </summary>
14+
/// <remarks>
15+
/// Default uses <c>http://localhost:9200</c>.
16+
/// </remarks>
17+
public Func<NodePool> NodePool { get; set; } = () => new SingleNodePool(new("http://localhost:9200"));
18+
19+
/// <summary>
20+
/// Gets or sets the factory for the underlying transport connection.
21+
/// </summary>
22+
public Func<TransportClient> Connection { get; set; } = () => new HttpTransportClient();
23+
24+
/// <summary>
25+
/// Gets or sets the factory for the client source serializer.
26+
/// </summary>
27+
public Func<IElasticsearchClientSettings, Serializer> SourceSerializer { get; set; }
28+
= settings => new DefaultSourceSerializer(settings, x => { });
29+
30+
/// <summary>
31+
/// Gets or sets the mapper for property serialization.
32+
/// </summary>
33+
public IPropertyMappingProvider PropertyMappingProvider { get; set; } = new DefaultPropertyMappingProvider();
34+
35+
/// <summary>
36+
/// Gets or sets the delegate to configure additional client settings.
37+
/// </summary>
38+
public Action<ElasticsearchClientSettings>? ConfigureSettings { get; set; }
39+
40+
/// <summary>
41+
/// Converts the options to Elastic client settings.
42+
/// </summary>
43+
/// <returns>The client settings.</returns>
44+
public IElasticsearchClientSettings ToSettings()
45+
{
46+
var settings = new ElasticsearchClientSettings(
47+
NodePool(),
48+
Connection(),
49+
(x, y) => SourceSerializer(y),
50+
PropertyMappingProvider);
51+
52+
ConfigureSettings?.Invoke(settings);
53+
return settings;
54+
}
55+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
namespace Elastic.Clients.Elasticsearch.Options;
2+
3+
using Elastic.Transport;
4+
5+
/// <summary>
6+
/// Options for authenticating with Elasticsearch nodes.
7+
/// </summary>
8+
public class ElasticsearchCredentialOptions
9+
{
10+
/// <summary>
11+
/// Gets or sets the ID of the API key for authentication, if any.
12+
/// </summary>
13+
public string? ApiKeyId { get; set; }
14+
15+
/// <summary>
16+
/// Gets or sets the API key for authentication, if any.
17+
/// </summary>
18+
public string? ApiKey { get; set; }
19+
20+
/// <summary>
21+
/// Attempts to create an Elasticsearch authorization header from the options.
22+
/// </summary>
23+
/// <returns>
24+
/// An HTTP header containing the credentials, or <see langword="null"/> if no credentials could be created.
25+
/// </returns>
26+
public AuthorizationHeader? CreateHeader()
27+
{
28+
if (ApiKey != null)
29+
{
30+
return ApiKeyId != null ? new Base64ApiKey(ApiKeyId, ApiKey) : new ApiKey(ApiKey);
31+
}
32+
33+
return null;
34+
}
35+
}

0 commit comments

Comments
 (0)