Custom mapping .NET field names

I have an existing ES index, and I need to "dynamically" specify how my .NET field names are mapped should be mapped to the ES/JSON field names.

It seems that I can't easily do this because everything is stuck behind the fluent methods. Even if I were to create my own IClrTypeMapping, every field is stuck behind an Expression:

Is there really no way around this? It seems like I have to either

  1. Ditch NEST altogether
  2. Modify NEST to not require Expressions everywhere.
  3. Dynamically create expressions

All of which I'd really rather not do.

Fluent is nice but there's always the alternative for situations where it isn't suitable. Yet all I can find is being forced to use the fluent methods everywhere.

Take a look at the documentation on Field inference; there are numerous ways available in NEST to achieve this:

If you need to change only a few field names then you can use:

  1. Rename() on ConnectionSettings

    var settings = new ConnectionSettings()
    .InferMappingFor(m => m
    .Rename(p => p.PropertyName, "renamed_property_name")
    );

    var client = new ElasticClient(settings);

  2. Name property on the relevant attribute value

    public class YourDocument
    {
    [Text(Name = "renamed_property_name")]
    public string PropertyName { get;set; }
    }

  3. If using the default serializer (JSON.Net), then using the JsonProperty attribute

    public class YourDocument
    {
    [JsonProperty("renamed_property_name")]
    public string PropertyName { get;set; }
    }

If you need to change all field names according to some convention, then this can be done with .DefaultFieldNameInferrer() on ConnectionSettings

var settings = new ConnectionSettings()
    // upper case all property names. argument is the property name inferred from the POCO
    .DefaultFieldNameInferrer(p => p.ToUpperInvariant();

var client = new ElasticClient(settings);
1 Like

Thank you for taking the time to answer, but I think you misunderstood my question. I was probably unclear.

I'm aware of all three ways. None work for what I need - I can only determine the "new" name by knowing the MemberInfo of the .NET type to be mapped. The mapping is completely dynamic. i.e. I have a dynamically created map of MemberInfo => string (jsonElementName)

  1. Rename() on ConnectionSettings does not work because it's completely restricted to an Expression. As you said, it's only suitable for a few fields.

  2. Since mappings are dynamic, Attributes do not work either. However, I hadn't realized that the serializer is Json.NET. I'll look into changing the mappings there, thank you.

  3. DefaultFieldNameInferrer only provides the original name. The DeclaringType and Type information of the field is completely lost. I need both to infer the name.

For now, I've solved the problem by creating a new ClrTypeMapping with dynamically created Expressions. IMO the use of Expression for the Properties is too restrictive and gives rise to problems like the one I've encountered:

    config.InferMappingFor<MyMappedType>(f => GetMappingFromBson<MyMappedType>("myIndexName", "myESMappingName"));

    private static ClrTypeMapping<T> GetMappingFromBson<T>(string indexName, string mappingName = null) where T: class
    {

        var mapping = new ClrTypeMapping<T> { IndexName = indexName, TypeName = mappingName ?? (typeof(T)).Name};
        var bsonMap = BsonClassMap.LookupClassMap(typeof(T));

        mapping.Properties = new List<IClrTypePropertyMapping<T>>();
        foreach (var m in bsonMap.AllMemberMaps)
        {
            var expr = CreatePropertyExpression<T>(m.MemberInfo);
            var p = new RenamePropertyMapping<T>(expr, m.ElementName);
            mapping.Properties.Add(p);
        }

        return mapping;

    }

    private static Expression<Func<T, object>> CreatePropertyExpression<T>(MemberInfo memberInfo)
    {
        var parameter = Expression.Parameter(typeof(T), "p");
        var member = Expression.MakeMemberAccess(parameter, memberInfo);
        var asObject = Expression.Convert(member, typeof(object));
        return Expression.Lambda<Func<T, object>>(asObject, parameter);
    }

I did misunderstand your question :smile:

The client works on the premise that you have POCOs to represent the documents you have in Elasticsearch.

The advantage of using member access expressions to determine field names from POCO property types is that it provides compile time safety and easy refactorability.

If you don't have POCOs to represent documents and instead need to build a mapping by reflecting over the properties of a type at runtime, you'll need to do more work as you've done or alternatively, you can cast ConnectionSettings to IConnectionSettingsValues in order to access the explicitly implemented PropertyMappings property and add the mappings this way

void Main()
{
	var pool = new SingleNodeConnectionPool(new Uri("http://localhost:9200"));
	var defaultIndex = "default-index";
	var connectionSettings = new ConnectionSettings(pool, new InMemoryConnection())
			.DefaultIndex(defaultIndex);
                
    var connectionSettingsValues = (IConnectionSettingsValues)connectionSettings;

    foreach (var memberInfo in typeof(Document).GetMembers())
    {
        connectionSettingsValues.PropertyMappings.Add(memberInfo, new PropertyMapping { Name = $"foo{memberInfo.Name}" });
    }
                
	var client = new ElasticClient(connectionSettings);

    client.Index(new Document
    {
        Name = "org name",
        Value = 1
    });
}

public class Document
{
    public string Name { get; set; }
    
    public int Value { get; set; }
}

will serialize to

POST http://localhost:9200/default-index/document?pretty=true 
{
  "fooName": "org name",
  "fooValue": 1
}

This topic was automatically closed 28 days after the last reply. New replies are no longer allowed.