ORTA
.NET Entegrasyonu (Production Patterns)
Production .NET uygulamalarında Elasticsearch client'ı doğru yapılandırmak, DI, resilience, ve error handling kritiktir.
.NET Client (Self-Managed)
// === DI Registration (Program.cs) ===
builder.Services.AddSingleton<ElasticsearchClient>(sp =>
{
var config = sp.GetRequiredService<IOptions<ElasticConfig>>().Value;
var pool = config.Nodes.Length > 1
? new StaticNodePool(config.Nodes.Select(n => new Uri(n)))
: new SingleNodePool(new Uri(config.Nodes[0]));
var settings = new ElasticsearchClientSettings(pool)
.Authentication(new ApiKey(config.ApiKey))
.DefaultIndex(config.DefaultIndex)
.RequestTimeout(TimeSpan.FromSeconds(config.TimeoutSeconds))
.MaxRetries(3)
.MaxRetryTimeout(TimeSpan.FromSeconds(30))
.DisableDirectStreaming(config.EnableDebug)
.OnRequestCompleted(details =>
{
if (details.HttpStatusCode >= 400)
{
var logger = sp.GetRequiredService<ILogger<ElasticsearchClient>>();
logger.LogWarning("ES request failed: {Method} {Path} -> {Status}",
details.HttpMethod, details.Uri?.PathAndQuery, details.HttpStatusCode);
}
});
return new ElasticsearchClient(settings);
});
// === Configuration ===
public class ElasticConfig
{
public string[] Nodes { get; set; } = ["http://localhost:9200"];
public string ApiKey { get; set; } = "";
public string DefaultIndex { get; set; } = "products";
public int TimeoutSeconds { get; set; } = 10;
public bool EnableDebug { get; set; } = false;
}
// === Generic Repository Pattern ===
public class ElasticRepository<T> where T : class
{
private readonly ElasticsearchClient _client;
private readonly string _indexName;
private readonly ILogger _logger;
public ElasticRepository(ElasticsearchClient client, string indexName, ILogger logger)
{
_client = client;
_indexName = indexName;
_logger = logger;
}
public async Task<T?> GetByIdAsync(string id)
{
var response = await _client.GetAsync<T>(id, g => g.Index(_indexName));
if (!response.IsValidResponse)
{
if (response.ApiCallDetails.HttpStatusCode == 404) return null;
_logger.LogError("ES Get failed: {Debug}", response.DebugInformation);
throw new ElasticException("Get failed", response.ElasticsearchServerError);
}
return response.Source;
}
public async Task<PagedResult<T>> SearchAsync(
Action<SearchRequestDescriptor<T>> configure,
int page = 0, int size = 20)
{
var response = await _client.SearchAsync<T>(s =>
{
s.Index(_indexName).From(page * size).Size(size);
configure(s);
});
if (!response.IsValidResponse)
throw new ElasticException("Search failed", response.ElasticsearchServerError);
return new PagedResult<T>(
response.Documents.ToList(),
response.Total,
page, size);
}
public async Task IndexAsync(T document, string id)
{
var response = await _client.IndexAsync(document, i => i
.Index(_indexName).Id(id));
if (!response.IsValidResponse)
throw new ElasticException("Index failed", response.ElasticsearchServerError);
}
}
public record PagedResult<T>(List<T> Items, long Total, int Page, int Size);
Örnek: Bir SaaS platformunda ElasticRepository
ElasticsearchClient SINGLETON olmalıdır! Her request'te yeni client oluşturmak connection pool'u bozar ve socket exhaustion'a yol açar. DI'da AddSingleton kullanın.
Elastic Cloud Bağlantısı
.NET Client (Elastic Cloud)
// === Elastic Cloud connection (cloud-id + API key) ===
// Elastic Cloud console'dan cloud-id ve API key alın
builder.Services.AddSingleton<ElasticsearchClient>(sp =>
{
var config = sp.GetRequiredService<IOptions<ElasticCloudConfig>>().Value;
// Cloud ID: "deployment-name:base64-encoded-data"
var settings = new ElasticsearchClientSettings(config.CloudId,
new ApiKey(config.ApiKey))
.DefaultIndex(config.DefaultIndex)
.RequestTimeout(TimeSpan.FromSeconds(10))
.MaxRetries(3);
return new ElasticsearchClient(settings);
});
public class ElasticCloudConfig
{
// Cloud ID format: "my-deployment:dXMtY2VudHJhbC0xLmd..."
public string CloudId { get; set; } = "";
public string ApiKey { get; set; } = "";
public string DefaultIndex { get; set; } = "products";
}
// appsettings.json:
// {
// "ElasticCloud": {
// "CloudId": "my-app:dXMtY2VudHJhbC0xLmdjcC5jbG91ZC5lcy5pbzo0NDMkZTBl...",
// "ApiKey": "VnVhQ2ZIY0JDZGJrUW0...",
// "DefaultIndex": "products"
// }
// }
Self-managed vs Cloud: Self-managed'da StaticNodePool + new Uri() kullanırsınız. Elastic Cloud'da ise CloudId + ApiKey yeterli — TLS, load balancing, node discovery otomatik. Aynı ElasticsearchClient API'si her iki ortamda da çalışır.
Elastic Serverless (Preview): Elastic Cloud Serverless, cluster yönetimi gerektirmeyen fully-managed seçenektir. Aynı .NET client, Serverless endpoint'ine de bağlanır — sadece endpoint URL + API key yeterli. Şu an preview aşamasında; production workload'ları için Elastic Cloud (hosted) tercih edin.
Polly Resilience Patterns
.NET Client (Polly + HttpClientFactory)
// === Polly Resilience Pipeline (Microsoft.Extensions.Resilience) ===
// NuGet: Microsoft.Extensions.Http.Resilience 9.x
// Program.cs — Resilient HTTP yapılandırması
builder.Services.AddHttpClient("elasticsearch")
.ConfigureHttpClient(c => c.BaseAddress = new Uri("https://es-cluster:9200"))
.AddResilienceHandler("es-pipeline", builder =>
{
// Circuit Breaker: 5 hata → 30s devre dışı
builder.AddCircuitBreaker(new HttpCircuitBreakerStrategyOptions
{
FailureRatio = 0.5,
SamplingDuration = TimeSpan.FromSeconds(10),
MinimumThroughput = 5,
BreakDuration = TimeSpan.FromSeconds(30),
ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
.HandleResult(r => r.StatusCode == System.Net.HttpStatusCode.TooManyRequests
|| r.StatusCode >= System.Net.HttpStatusCode.InternalServerError)
});
// Retry: exponential backoff + jitter
builder.AddRetry(new HttpRetryStrategyOptions
{
MaxRetryAttempts = 3,
Delay = TimeSpan.FromMilliseconds(500),
BackoffType = DelayBackoffType.Exponential,
UseJitter = true,
ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
.HandleResult(r => r.StatusCode == System.Net.HttpStatusCode.TooManyRequests
|| (int)r.StatusCode >= 500)
});
// Timeout: per-request
builder.AddTimeout(TimeSpan.FromSeconds(10));
});
// === ElasticsearchClient ile Polly (transport-level) ===
builder.Services.AddSingleton<ElasticsearchClient>(sp =>
{
var httpFactory = sp.GetRequiredService<IHttpClientFactory>();
var config = sp.GetRequiredService<IOptions<ElasticConfig>>().Value;
var settings = new ElasticsearchClientSettings(
new StaticNodePool(config.Nodes.Select(n => new Uri(n))))
.Authentication(new ApiKey(config.ApiKey))
.RequestTimeout(TimeSpan.FromSeconds(10))
.MaxRetries(3)
.MaxRetryTimeout(TimeSpan.FromSeconds(30))
.DeadTimeout(TimeSpan.FromSeconds(60)) // Dead node re-check interval
.MaxDeadTimeout(TimeSpan.FromMinutes(5)); // Max backoff for dead nodes
return new ElasticsearchClient(settings);
});
// === Health check with circuit state awareness ===
public class ResilientElasticHealthCheck : IHealthCheck
{
private readonly ElasticsearchClient _client;
private readonly ResiliencePipelineProvider<string> _pipelines;
public ResilientElasticHealthCheck(
ElasticsearchClient client,
ResiliencePipelineProvider<string> pipelines)
{
_client = client;
_pipelines = pipelines;
}
public async Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context, CancellationToken ct = default)
{
try
{
var response = await _client.PingAsync(ct);
return response.IsValidResponse
? HealthCheckResult.Healthy()
: HealthCheckResult.Unhealthy("ES ping failed");
}
catch (Exception ex) when (ex.Message.Contains("circuit"))
{
return HealthCheckResult.Unhealthy("Circuit breaker OPEN — ES overloaded");
}
}
}
ES client'ın kendi retry mekanizması var — MaxRetries(3) ayarı transport layer'da çalışır. Polly ek bir katman olarak HTTP seviyesinde circuit breaker + jittered retry ekler. İkisini birlikte kullanın: ES client retry geçici DNS/network hatalarını, Polly ise 429/5xx pattern'lerini yakalar.