Table of Contents

Naming Policies

Naming policies in Cratis Applications control how collection names and property names are transformed when serializing to MongoDB. This ensures consistent naming conventions across your database.

Overview

Collection names and their members are named based on a naming policy (INamingPolicy). The framework provides flexible configuration options and built-in policies for common scenarios.

Default Behavior

The default naming policy does not alter the input, giving you the names exactly as defined in your types:

public class UserAccount
{
    public string UserName { get; set; }       // Stored as: "UserName"
    public string EmailAddress { get; set; }   // Stored as: "EmailAddress"
    public DateTime CreatedDate { get; set; }  // Stored as: "CreatedDate"
}

Built-in Naming Policies

Camel Case Policy

The most common naming policy is camel case, which converts PascalCase property names to camelCase:

builder.UseCratisMongoDB(configureMongoDB: builder => 
    builder.WithCamelCaseNamingPolicy());

With camel case policy:

public class UserAccount
{
    public string UserName { get; set; }       // Stored as: "userName"
    public string EmailAddress { get; set; }   // Stored as: "emailAddress"
    public DateTime CreatedDate { get; set; }  // Stored as: "createdDate"
}

Custom Naming Policies

You can create your own naming policy by implementing INamingPolicy:

public class SnakeCaseNamingPolicy : INamingPolicy
{
    public string ConvertName(string name)
    {
        if (string.IsNullOrEmpty(name))
            return name;
            
        var result = new StringBuilder();
        for (int i = 0; i < name.Length; i++)
        {
            if (i > 0 && char.IsUpper(name[i]))
                result.Append('_');
            result.Append(char.ToLower(name[i]));
        }
        return result.ToString();
    }
}

Registering Custom Policies

You can register your custom naming policy in two ways:

By Type

builder.UseCratisMongoDB(configureMongoDB: builder => 
    builder.WithNamingPolicy<SnakeCaseNamingPolicy>());

By Instance

If your naming policy requires constructor parameters:

var namingPolicy = new CustomNamingPolicy(prefix: "app_", suffix: "_v1");

builder.UseCratisMongoDB(configureMongoDB: builder => 
    builder.WithNamingPolicy(namingPolicy));

How Naming Policies Work

Convention Integration

Naming policies are applied through the NamingPolicyNameConvention, which is automatically registered as a convention pack. This convention:

  1. Applies to all members: Processes every property and field in your classes
  2. Uses configured policy: Applies the naming policy you've configured
  3. Integrates with filtering: Respects convention pack filters and ignore attributes

Property Name Transformation

The convention applies to:

  • Public properties: All public get/set properties
  • Public fields: Public field members (if configured)
  • Nested objects: Properties within embedded documents
  • Collection elements: Properties of objects within arrays

Configuration Examples

Multiple Policies

You can create policies that combine multiple transformations:

public class CompoundNamingPolicy : INamingPolicy
{
    private readonly INamingPolicy[] _policies;
    
    public CompoundNamingPolicy(params INamingPolicy[] policies)
    {
        _policies = policies;
    }
    
    public string ConvertName(string name)
    {
        return _policies.Aggregate(name, (current, policy) => policy.ConvertName(current));
    }
}

// Usage
var policy = new CompoundNamingPolicy(
    new CamelCaseNamingPolicy(),
    new PrefixNamingPolicy("data_")
);

builder.UseCratisMongoDB(configureMongoDB: builder => 
    builder.WithNamingPolicy(policy));

Conditional Policies

Create policies that apply different rules based on the property name:

public class ConditionalNamingPolicy : INamingPolicy
{
    public string ConvertName(string name)
    {
        // Don't transform ID fields
        if (name.EndsWith("Id", StringComparison.OrdinalIgnoreCase))
            return name.ToLower();
            
        // Use camel case for everything else
        return char.ToLower(name[0]) + name[1..];
    }
}

Convention Pack Integration

The naming policy is implemented as a MongoDB convention pack, which means:

Automatic Application

// This is registered automatically during setup
ConventionRegistry.Register(
    NamingPolicyNameConvention.ConventionName,
    new ConventionPack { new NamingPolicyNameConvention() },
    type => /* filter logic */
);

Filtering Support

You can control which types the naming policy applies to using convention pack filters:

public class NoNamingPolicyForDTOs : ICanFilterMongoDBConventionPacksForType
{
    public bool ShouldInclude(string conventionPackName, IConventionPack conventionPack, Type type)
    {
        if (conventionPackName == NamingPolicyNameConvention.ConventionName)
        {
            return !type.Name.EndsWith("DTO");
        }
        return true;
    }
}

Ignore Naming Conventions

For specific types that shouldn't use naming policies, use the IgnoreConventions attribute:

[IgnoreConventions(NamingPolicyNameConvention.ConventionName)]
public class LegacyDocument
{
    public string UserName { get; set; }     // Stored as: "UserName" (unchanged)
    public string EmailAddr { get; set; }    // Stored as: "EmailAddr" (unchanged)
}

Impact on Queries

Remember that naming policies affect how you write queries:

With Camel Case Policy

// Property defined as: public string UserName { get; set; }
// Stored in MongoDB as: "userName"

// Query using the MongoDB field name
var filter = Builders<User>.Filter.Eq("userName", "john.doe");

// Or use expression trees (automatically converted)
var users = await collection
    .Find(u => u.UserName == "john.doe")  // Automatically converted to "userName"
    .ToListAsync();

Error Handling

Missing Policy Configuration

If no naming policy is configured, the framework will throw a descriptive error:

NamingPolicyNotConfigured: No naming policy has been configured. 
Use WithNamingPolicy<T>() or WithNamingPolicy(instance) to configure one.

Null or Empty Names

Naming policies should handle edge cases:

public string ConvertName(string name)
{
    if (string.IsNullOrWhiteSpace(name))
        return name;  // Return unchanged for invalid input
        
    // Your transformation logic
    return TransformName(name);
}

Best Practices

Consistency

Choose one naming convention and apply it consistently across your application:

// Good: Consistent camelCase
{
    "userId": "123",
    "userName": "john",
    "createdAt": "2024-01-15T10:30:00Z"
}

// Avoid: Mixed conventions
{
    "userId": "123",
    "UserName": "john",
    "created_at": "2024-01-15T10:30:00Z"
}

Consider External Systems

If you're integrating with external systems that expect specific naming conventions, align your policy accordingly:

// For systems expecting snake_case
builder.WithNamingPolicy<SnakeCaseNamingPolicy>();

// For JavaScript/JSON APIs expecting camelCase  
builder.WithCamelCaseNamingPolicy();

Performance Considerations

Naming policies are called for every property during serialization setup, so keep them efficient:

// Good: Simple, efficient transformation
public string ConvertName(string name) => 
    char.ToLower(name[0]) + name[1..];

// Avoid: Complex regex or multiple string operations
public string ConvertName(string name) => 
    Regex.Replace(name, "([A-Z])", "_$1").ToLower().Trim('_');

Next Steps