Configuring the mapping system
This article covers startup configuration for the object mapping system: naming convention translation, the fluent builder API, custom mapper implementations, and type converters.
For day-to-day usage and a feature overview, see Mapping query results to objects.
Naming convention translation
By default the mapper matches C# property names to record field names by an exact, case-sensitive comparison.
If your database uses a different naming convention — for example camelCase fields vs. PascalCase properties —
you can configure automatic translation once at startup using
RecordObjectMapping.TranslateIdentifiers.
The most common case (C# PascalCase or camelCase → database camelCase) requires no arguments:
// At startup, before any queries are run
RecordObjectMapping.TranslateIdentifiers();
After this call, a property named FirstName automatically looks up the record field firstName, and so on.
Choosing a convention
Use the overloads that accept
IdentifierCaseConvention and/or
FieldCaseConvention for other
combinations. IdentifierCaseConvention describes how C# identifiers (property and parameter names) are
split into tokens; FieldCaseConvention describes how those tokens are joined to form a database field
name for record lookup.
| Scenario | C# convention | DB convention | Example call |
|---|---|---|---|
| Typical C# property names (PascalCase or camelCase) | CSharpIdentifier (built into the parameterless overload) |
camelCase |
TranslateIdentifiers() |
| Typical C# property names | CSharpIdentifier (built into the overload) |
snake_case |
TranslateIdentifiers(FieldCaseConvention.SnakeCase) |
| PascalCase property names | PascalCase |
SCREAMING_SNAKE |
TranslateIdentifiers(IdentifierCaseConvention.PascalCase, FieldCaseConvention.ScreamingSnakeCase) |
| camelCase property names | camelCase |
kebab-case |
TranslateIdentifiers(IdentifierCaseConvention.CamelCase, FieldCaseConvention.KebabCase) |
IdentifierCaseConvention values: CSharpIdentifier, CamelCase, PascalCase, SnakeCase,
ScreamingSnakeCase, KebabCase.
FieldCaseConvention values: CamelCase, PascalCase, SnakeCase, ScreamingSnakeCase, KebabCase.
Applying translation to Cypher parameters
When a C# object is passed as a query parameter, each property name becomes a Cypher parameter key. Pass
translateCypherParameters: true so those keys are produced with the same C# identifier → database field
name translation as record mapping:
RecordObjectMapping.TranslateIdentifiers(translateCypherParameters: true);
This keeps naming consistent when reading results and when binding parameters, without extra attributes.
Bypassing translation for specific members
Properties and parameters decorated with
[MappingSource] are treated as explicit
database field names and bypass global translation entirely.
Custom convention translator
If none of the built-in conventions cover your naming scheme, compose
IIdentifierParser<T> and
IFieldFormatter<T> and register them with
TranslateIdentifiers<TParseResult>(...).
ConventionTranslator<T> implements
IConventionTranslator and is what the
built-in overloads use internally; you only need that type if you want to wrap or test translation in isolation.
Example: every column in the database uses a db_ prefix and snake_case, while C# properties stay in normal
PascalCase (PersonId → db_person_id). Delegate tokenization and casing to the built-in snake-case path,
then add the prefix:
using System.Collections.Generic;
using Neo4j.Driver.Mapping;
using Neo4j.Driver.Mapping.ConventionTranslation;
public sealed class DbPrefixedSnakeCaseFormatter : IFieldFormatter<IEnumerable<string>>
{
private readonly IFieldFormatter<IEnumerable<string>> _inner =
new StandardCaseFormatter(FieldCaseConvention.SnakeCase);
public string Format(IEnumerable<string> data) => "db_" + _inner.Format(data);
}
// At startup:
RecordObjectMapping.TranslateIdentifiers(
new StandardCaseParser(IdentifierCaseConvention.CSharpIdentifier),
new DbPrefixedSnakeCaseFormatter());
Fluent builder API
The fluent builder is the recommended way to configure multiple type mappings without implementing
IRecordMapper<T> by hand.
Implementing a mapping provider
Create a class that implements
IMappingProvider and call
IMappingRegistry.RegisterMapping<T>
for each type:
public class MyMappingProvider : IMappingProvider
{
public void CreateMappers(IMappingRegistry registry)
{
registry.RegisterMapping<Person>(b => b
.UseDefaultMapping()
.Map(p => p.Labels, "person", MappingSource.NodeLabel));
registry.RegisterMapping<Address>(b => b
.Map(a => a.Street, "street")
.Map(a => a.City, "city")
.Map(a => a.PostCode, "postcode"));
}
}
Register the provider once at startup:
RecordObjectMapping.RegisterProvider<MyMappingProvider>();
// or, if construction arguments are needed:
RecordObjectMapping.RegisterProvider(new MyMappingProvider(someArg));
Builder methods
UseDefaultMapping()
Applies the default mapper first and then lets subsequent Map(...) calls override specific properties:
registry.RegisterMapping<Person>(b => b
.UseDefaultMapping()
.Map(p => p.Labels, "person", MappingSource.NodeLabel));
Map(property, recordKey, ...)
Maps a specific property from a named record field, with optional MappingSource, inline converter, and
optionality flag:
registry.RegisterMapping<Movie>(b => b
.Map(m => m.Title, "title")
.Map(m => m.ReleaseYear, "released")
.Map(m => m.Genre, "genre", optional: true));
Map(property, Func<IRecord, object>)
Maps a property from an arbitrary function over the record, giving full access to all fields:
registry.RegisterMapping<Movie>(b => b
.Map(m => m.DisplayTitle, r => $"{r["title"].As<string>()} ({r["released"].As<int>()})"));
MapWholeObject(Func<IRecord, T>)
Replaces the default construction logic entirely. Use when the object's constructor arguments require complex derivation from the record:
registry.RegisterMapping<Address>(b => b
.MapWholeObject(r => new Address(
r["street"].As<string>(),
r["city"].As<string>(),
r["postcode"].As<string>())));
Do not combine MapWholeObject with UseDefaultMapping — MapWholeObject replaces the entire construction
pipeline.
Custom mapper (IRecordMapper<T>)
For complete programmatic control, implement
IRecordMapper<T> and register it directly:
public class PersonMapper : IRecordMapper<Person>
{
public Person Map(IRecord record)
{
return new Person
{
Name = record["name"].As<string>(),
Age = record["age"].As<int>()
};
}
}
// At startup:
RecordObjectMapping.Register(new PersonMapper());
The registered mapper replaces the default mapper for Person globally. Prefer this approach when the
mapping logic is complex, needs to be tested in isolation, or needs to share state across records.
Type converters
Register a global conversion function when a record field value's runtime type does not match the target
property type and the driver's built-in type coercion (via
ValueExtensions.As<T>) does not handle it:
// Convert a string record value to a Uri
RecordObjectMapping.RegisterTypeConverter<string, Uri>(s => new Uri(s));
Type converters apply globally to all type mappings. If multiple converters are registered for the same pair of types, the most recently registered one wins.
Built-in converters
The following converters are registered automatically:
string→Guid(viaGuid.Parse)Vector→List<T>,IList<T>,IEnumerable<T>,IReadOnlyList<T>,IReadOnlyCollection<T>,ICollection<T>for supported vector types
Configuration order
Call all startup configuration before running any queries. The recommended order is:
RecordObjectMapping.TranslateIdentifiers(...)— if using convention translationRecordObjectMapping.RegisterProvider(...)— for fluent builder mappingsRecordObjectMapping.Register(...)— for hand-writtenIRecordMapper<T>implementationsRecordObjectMapping.RegisterTypeConverter(...)— for custom type conversions
All configuration is global and thread-safe for concurrent reads after initial setup.
See also
- Mapping query results to objects — feature overview, attributes, and common usage patterns