Loading

Code-driven mappings

Field attributes handle most mapping declarations. For custom analyzers, runtime fields, dynamic templates, or field overrides, implement IConfigureElasticsearch<T>.

public interface IConfigureElasticsearch<TDocument> where TDocument : class
{
    AnalysisBuilder ConfigureAnalysis(AnalysisBuilder analysis);
    MappingsBuilder<TDocument> ConfigureMappings(MappingsBuilder<TDocument> mappings);
    IReadOnlyDictionary<string, string>? IndexSettings { get; }
}
		
Member What it produces
ConfigureAnalysis Custom analyzers, tokenizers, filters merged into the settings component template
ConfigureMappings Field overrides, dynamic templates, runtime fields merged into the mappings component template
IndexSettings Additional key-value pairs in the settings JSON (e.g. index.default_pipeline)

The source generator discovers configuration using this priority chain (first match wins):

Priority Source
1 (highest) Configuration class specified via Configuration = typeof(...) on the target attribute
2 (lowest) The document type itself implementing IConfigureElasticsearch<T>
Tip

This is the recommended approach. It keeps your document type clean and allows reuse across multiple contexts.

Reference a dedicated configuration class via the Configuration property on the target attribute:

[ElasticsearchMappingContext]
[Index<Product>(Name = "products", Configuration = typeof(ProductConfig))]
public static partial class MyContext;

public class ProductConfig : IConfigureElasticsearch<Product>
{
    public AnalysisBuilder ConfigureAnalysis(AnalysisBuilder analysis) =>
        analysis
            .Analyzer("product_name_analyzer", a => a
                .Custom()
                .Tokenizer("standard")
                .Filters("lowercase", "asciifolding"))
            .Normalizer("lowercase_normalizer", n => n
                .Custom()
                .Filters("lowercase"));

    public MappingsBuilder<Product> ConfigureMappings(MappingsBuilder<Product> mappings) =>
        mappings.Name(f => f.Analyzer("product_name_analyzer"));

    public IReadOnlyDictionary<string, string>? IndexSettings =>
        new Dictionary<string, string>
        {
            ["index.default_pipeline"] = "product-enrichment"
        };
}
		

The document type can implement the interface directly when configuration is inherent to the type:

public class LogEntry : IConfigureElasticsearch<LogEntry>
{
    [Timestamp]
    public DateTimeOffset Timestamp { get; set; }

    [Text]
    public string Message { get; set; }

    [Keyword]
    public string Level { get; set; }

    public AnalysisBuilder ConfigureAnalysis(AnalysisBuilder analysis) =>
        analysis
            .Analyzer("log_message_analyzer", a => a
                .Custom()
                .Tokenizer("standard")
                .Filters("lowercase", "stop"));

    public MappingsBuilder<LogEntry> ConfigureMappings(MappingsBuilder<LogEntry> mappings) =>
        mappings.Message(f => f.Analyzer("log_message_analyzer"));

    public IReadOnlyDictionary<string, string>? IndexSettings => null;
}
		

The source generator emits strongly-typed extension methods for each property on your document type. These let you override field settings without using strings:

public MappingsBuilder<Product> ConfigureMappings(MappingsBuilder<Product> mappings) =>
    mappings
        .Name(f => f.Analyzer("product_name_analyzer"))
        .Category(f => f.Normalizer("lowercase_normalizer"))
        .Description(f => f.Analyzer("english").SearchAnalyzer("english"));
		

Each generated method corresponds to a property on the document class.

Configuration classes can delegate to shared factory methods. The source generator follows the call graph to discover analysis component names:

public static class SharedAnalysisFactory
{
    public static AnalysisBuilder BuildStandardAnalysis(AnalysisBuilder analysis) =>
        analysis
            .Normalizer("keyword_normalizer", n => n.Custom().Filters("lowercase"))
            .Analyzer("starts_with_analyzer", a => a
                .Custom()
                .Tokenizer("starts_with_tokenizer")
                .Filters("lowercase"))
            .Tokenizer("starts_with_tokenizer", t => t
                .EdgeNGram()
                .MinGram(1)
                .MaxGram(20));
}

public class ProductConfig : IConfigureElasticsearch<Product>
{
    public AnalysisBuilder ConfigureAnalysis(AnalysisBuilder analysis) =>
        SharedAnalysisFactory.BuildStandardAnalysis(analysis);

    public MappingsBuilder<Product> ConfigureMappings(MappingsBuilder<Product> mappings) => mappings;
    public IReadOnlyDictionary<string, string>? IndexSettings => null;
}
		

The generator traces calls transitively and emits strongly-typed accessors for all discovered names. See strongly-typed analysis names for details.

When using Variant to register the same document type multiple times, each variant can have its own configuration class:

[ElasticsearchMappingContext]
[Index<Article>(Name = "articles-lexical", Configuration = typeof(ArticleLexicalConfig))]
[Index<Article>(Name = "articles-semantic", Variant = "Semantic", Configuration = typeof(ArticleSemanticConfig))]
public static partial class ArticleContext;