OrchardCMS/OrchardCore · error · ArgumentException

Content type name must start with a letter

Error message

Content type name must start with a letter

What it means

When an explicit technical name is provided to AddTypeAsync, ContentDefinitionService enforces that it starts with an ASCII letter (so it is a valid .NET/YesSql-safe identifier) and is a safe name. A name whose first character is not an ASCII letter throws ArgumentException. This keeps content type names valid for code generation and display-name classifying.

Solutions

  1. Prefix the name with a letter (e.g. 'Type2faConfig') before calling the service.
  2. Sanitize user input with name.ToSafeName() and ensure the first character is a letter.
  3. Let the service generate the name by passing null and only supplying displayName.
  4. Validate with char.IsAsciiLetter(name[0]) in your own form validation before calling.

Example fix

// before
await _contentDefinitionService.AddTypeAsync("2faConfig", "2FA Config");
// after
var safeName = "2faConfig".ToSafeName();
var name = char.IsAsciiLetter(safeName[0]) ? safeName : "C" + safeName;
await _contentDefinitionService.AddTypeAsync(name, "2FA Config");
Defensive patterns

Strategy: validation

Validate before calling

if (string.IsNullOrEmpty(name) || !char.IsAsciiLetter(name[0]))
    throw new ArgumentException("Content type name must start with a letter.", nameof(name));
if (!string.Equals(name, name.ToSafeName(), StringComparison.OrdinalIgnoreCase))
    throw new ArgumentException("Content type name contains invalid characters.", nameof(name));

Type guard

static bool IsValidTypeName(string? name) => !string.IsNullOrEmpty(name) && char.IsAsciiLetter(name[0]) && name.Equals(name.ToSafeName(), StringComparison.OrdinalIgnoreCase);

Try / catch

try { await _service.AddTypeAsync(name, displayName); }
catch (ArgumentException ex) when (ex.ParamName == nameof(name))
{
    ModelState.AddModelError(nameof(name), "Type names must start with a letter and contain only safe characters.");
    return View(model);
}

Prevention

When it happens

Trigger: Calling AddTypeAsync with a technical name beginning with a digit, underscore, or non-ASCII character — e.g. AddTypeAsync("2faConfig", "2FA Config") or names from user input without sanitization.

Common situations: Importing type definitions from external systems with different naming rules; scripts generating type names from numbers or localized strings; user-supplied names passed straight through without ToSafeName.

Understand the failure class

Background: "invalid id" errors: invalid identifier format — why libraries reject IDs before lookup, and how to fix them — this error's family across 37 libraries.

Related errors


AI-assisted analysis of OrchardCMS/OrchardCore@4306c0717f (2026-09-13). Data as JSON: /api/errors/8b141cdff2fc30d6. Report an issue: GitHub.

Appendix: source

Thrown at src/OrchardCore.Modules/OrchardCore.ContentTypes/Services/ContentDefinitionService.cs:64

        S = stringLocalizer;
    }

    public async Task<ContentTypeDefinition> AddTypeAsync(string name, string displayName)
    {
        if (string.IsNullOrWhiteSpace(displayName))
        {
            throw new ArgumentException($"The '{nameof(displayName)}' can't be null or empty.", nameof(displayName));
        }

        if (string.IsNullOrWhiteSpace(name))
        {
            name = await GenerateContentTypeNameFromDisplayNameAsync(displayName);
        }
        else
        {
            if (!char.IsAsciiLetter(name[0]))
            {
                throw new ArgumentException("Content type name must start with a letter", nameof(name));
            }
            if (!string.Equals(name, name.ToSafeName(), StringComparison.OrdinalIgnoreCase))
            {
                throw new ArgumentException("Content type name contains invalid characters", nameof(name));
            }
        }

        while (await _contentDefinitionManager.LoadTypeDefinitionAsync(name) is not null)
        {
            name = VersionName(name);
        }

        var contentTypeDefinition = new ContentTypeDefinition(name, displayName);

        await _contentDefinitionManager.StoreTypeDefinitionAsync(contentTypeDefinition);

        // Ensure it has its own part.
        await _contentDefinitionManager.AlterTypeDefinitionAsync(name, builder => builder.WithPart(name));

View on GitHub (pinned to 4306c0717f)