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
- Prefix the name with a letter (e.g. 'Type2faConfig') before calling the service.
- Sanitize user input with name.ToSafeName() and ensure the first character is a letter.
- Let the service generate the name by passing null and only supplying displayName.
- 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
- Sanitize names with ToSafeName() and prefix a letter if needed
- Let the service generate names from the display name when unsure
- Enforce naming rules in import/provisioning scripts
- Reject digit- or symbol-leading names in UI validation
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
- Content type name contains invalid characters
- The 'displayName' can't be null or empty.
- File path must be a non-empty string.
- Unable to create a safe index prefix for AI Search…
- Content part name must start with a letter
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)