Event subscriber name does not match the configured template

Properties
LC0098 Info Naming Code Fix Ignore Obsolete

Event subscribers are the connective tissue between publishers and the code that reacts to them. When every subscriber in a codebase follows the same naming convention — source object, event, optional field — it becomes immediately clear what a subscriber does without reading its body. A subscriber named HandleSomething tells you nothing; one named "Sales Header_OnAfterDeleteEvent" tells you everything.

LC0098 validates subscriber names against a single configurable template. The default template is:

{Event Source}_{Event Name}[_{Element Name}]

This is byte-for-byte the identifier the AL Language extension’s “Find Event” feature generates for a new subscriber. Subscribers created via the tooling default already satisfy the rule with zero configuration.

Example

The following subscriber has a name that does not encode the source object or event:

codeunit 50100 MySubscriber
{
    [EventSubscriber(ObjectType::Table, Database::"Sales Header", OnAfterDeleteEvent, '', false, false)]
    local procedure HandleSalesHeaderDelete(var Rec: Record "Sales Header"; RunTrigger: Boolean) // Event subscriber 'HandleSalesHeaderDelete' does not match the expected name '"Sales Header_OnAfterDeleteEvent"'. [LC0098]
    begin
    end;
}

Rename it to match the template:

codeunit 50100 MySubscriber
{
    [EventSubscriber(ObjectType::Table, Database::"Sales Header", OnAfterDeleteEvent, '', false, false)]
    local procedure "Sales Header_OnAfterDeleteEvent"(var Rec: Record "Sales Header"; RunTrigger: Boolean)
    begin
    end;
}

The double-quotes are added automatically when the derived identifier contains characters that require AL identifier quoting (spaces, ., /, &, %, …).

Example with element name

For field-level events (e.g. OnAfterValidateEvent), the fourth attribute argument names the field. The optional group [_{Element Name}] adds an underscore-separated field name only when the element name is present:

codeunit 50100 MySubscriber
{
    [EventSubscriber(ObjectType::Table, Database::"Sales Header", OnAfterValidateEvent, "Posting Date", false, false)]
    local procedure "Sales Header_OnAfterValidateEvent_Posting Date"(var Rec: Record "Sales Header"; var xRec: Record "Sales Header")
    begin
    end;
}

The same subscriber without a field filter ('') produces the shorter name without the underscore suffix:

codeunit 50100 MySubscriber
{
    [EventSubscriber(ObjectType::Table, Database::"Sales Header", OnAfterValidateEvent, '', false, false)]
    local procedure "Sales Header_OnAfterValidateEvent"(var Rec: Record "Sales Header"; var xRec: Record "Sales Header")
    begin
    end;
}

Template syntax

A template is a string that mixes literal characters and token placeholders. Literal characters are emitted as-is. Placeholders are replaced by the corresponding value from the [EventSubscriber] attribute, rendered in the style the placeholder’s own casing selects.

Token placeholders

The placeholder name determines which value to insert; the style of the name determines the output style:

PlaceholderTokenOutput styleExample ("Sales Header")
{Event Source}Source object nameRaw (verbatim)Sales Header
{EventSource}Source object namePascalCaseSalesHeader
{eventSource}Source object namecamelCasesalesHeader
{event_source}Source object namesnake_casesales_header
{event-source}Source object namekebab-casesales-header
{Event Name}Event procedure nameRaw (verbatim)OnAfterDeleteEvent
{EventName}Event procedure namePascalCaseOnAfterDeleteEvent
{eventName}Event procedure namecamelCaseonAfterDeleteEvent
{event_name}Event procedure namesnake_caseon_after_delete_event
{event-name}Event procedure namekebab-caseon-after-delete-event
{Element Name}Field/element nameRaw (verbatim)Posting Date
{ElementName}Field/element namePascalCasePostingDate
{elementName}Field/element namecamelCasepostingDate
{element_name}Field/element namesnake_caseposting_date
{element-name}Field/element namekebab-caseposting-date

Raw placeholders ({Event Source}, {Event Name}, {Element Name}) emit the input value verbatim: no word splitting, no case transform, no acronym-aware rendering. This is what the default template uses so its output aligns with the AL Language extension’s tooling.

Any other text in the template is treated as a literal character and emitted unchanged. Unknown {...} sequences are also passed through literally.

Optional groups

Wrap part of the template in [...] to emit it only when all token placeholders inside it resolve to a non-empty value. This prevents orphaned separators when the element name is absent.

Without optional group — empty element name leaves a trailing underscore:

TemplateElementNameResult
{Event Source}_{EventName}_{Element Name}``Sales Header_OnAfterDeleteEvent_
{Event Source}_{EventName}_{Element Name}Posting DateSales Header_OnAfterDeleteEvent_Posting Date

With optional group — the separator is emitted only when an element name is present:

TemplateElementNameResult
{Event Source}_{Event Name}[_{Element Name}]``Sales Header_OnAfterDeleteEvent
{Event Source}_{Event Name}[_{Element Name}]Posting DateSales Header_OnAfterDeleteEvent_Posting Date

Optional groups can contain any combination of literals and token placeholders. A [ inside an optional group is treated as a literal character (nesting is not supported).

Word splitting (non-raw styles only)

For any of the PascalCase / camelCase / snake_case / kebab-case placeholders, token values are split into words before the case transform is applied:

  1. Split on whitespace and the punctuation characters _ - . / & ( ) + %
  2. Split further on PascalCase and camelCase boundaries (uppercase letter after lowercase, or at the start of an all-caps run followed by lowercase)

The % delimiter drops the character entirely, so "Line Discount %" renders as LineDiscount (not LineDiscount% or LineDiscountPct).

The words are then rendered in the requested style, so the output is deterministic regardless of how the source name is written:

InputWordsPascalCasesnake_case
"Sales Header"Sales, HeaderSalesHeadersales_header
"Gen. Journal Template"Gen, Journal, TemplateGenJournalTemplategen_journal_template
"G/L Account"G, L, AccountGLAccountg_l_account
"Sales & Receivables Setup"Sales, Receivables, SetupSalesReceivablesSetupsales_receivables_setup
OnAfterDeleteEventOn, After, Delete, EventOnAfterDeleteEventon_after_delete_event

Raw-style placeholders skip word splitting entirely and emit the input verbatim.

Acronyms and casing (non-raw styles only)

For PascalCase and camelCase (non-first) positions, each word is rendered to a canonical preferred form. The renderer follows an “original casing wins” rule: as long as the source word carries any uppercase character, its casing is preserved and the acronym registry does not change the preferred form. Only all-lowercase source words fall back to the registry to recover a canonical casing.

By default the diagnostic accepts only the preferred form. When KnownAcronyms pins an alternate casing on the same case-insensitive key as an uppercase source word, that variant becomes an additionally accepted spelling — the CodeFix still suggests the preferred (original-wins) form. See Alternative: project-specific acronyms below.

Word typePreferred rendering
ID (any casing)IdID is an abbreviation per C# guideline, not an acronym
Two-letter all-uppercase word (IO, DX, AG, GL, IC, UI, …)Kept uppercase (IOLog, not IoLog)
Word contains any uppercase character (VAT, Sales, Http, XYZ)First letter uppercased, remainder untouched — original casing wins
Word is all-lowercase and matches a registered acronym (vat, json, lcy, acme)Canonical casing from the registry (Vat, Json, Lcy, Acme)
Word is all-lowercase and not registered (amount, header)First letter uppercased, remainder as-is (Amount, Header)

camelCase first word is unconditionally fully lowercased per C# convention (onAfterDeleteEvent, httpsEndpoint, idBadge). Snake and kebab styles are always fully lowercased.

Example — field "LCY Amount" under a fully cased template On{EventSource}_{EventName}_{ElementName} accepts:

  • OnMyTable_OnAfterValidateEvent_LCYAmount ✅ (source LCY is uppercase — original casing wins, preferred spelling)
  • OnMyTable_OnAfterValidateEvent_LcyAmountunless "KnownAcronyms": ["Lcy"] is configured — then it is additionally accepted alongside LCY (see below )
  • OnMyTable_OnAfterValidateEvent_LcYAmount ❌ (never accepted — third variant)

Example — field "lcy amount" (all-lowercase source) under the same template accepts:

  • OnMyTable_OnAfterValidateEvent_LcyAmount ✅ (registry entry Lcy supplies the canonical casing)
  • OnMyTable_OnAfterValidateEvent_LCYAmount ❌ (registry canonical wins over EnsureUpperFirst fallback)

Recognized acronyms

The following acronyms ship as the built-in default list and always match case-insensitively:

Aad, Abc, Acy, Adcs, Api, Arc, Ascii, Ato, Bcc, Bic, Blob, BoM, Bom, Bop, Bwr, Cal, Cds, Cogs, Crm, Csv, Dach, Dtd, Dvr, Ecsl, Emu, Eori, Fefo, Gln, Gtin, Guid, Html, Iban, Id, Iso, Isv, Json, Kpi, Lcid, Lcy, Lid, Mps, Mrp, Nav, Ocr, Oob, Pbix, Pdf, Pfx, Qbd, Sepa, Sic, Sid, Sift, Sku, Smtp, Sqm, Swift, Uid, UoM, Uom, Ups, Uri, Url, Urs, Utc, Utf, Vat, Wip, Wms, Xml, Xsd, Ytd.

Two-letter uppercase abbreviations are handled by the general 2-letter rule above and are deliberately not part of this list. A few keys ship with more than one built-in variant (for example BoM/Bom and UoM/Uom). For uppercase-carrying source words the original casing always wins, so every listed variant is additionally accepted regardless of order; the first-listed spelling is used only when the source word is all-lowercase, in which case the renderer picks it as the canonical fallback.

Project-specific additions are supplied via the KnownAcronyms setting — the user list is merged into the built-in defaults, and any user entry for a given case-insensitive key fully replaces the built-in variants for that key.

Configuration

Set SubscriberNamingPattern in alcops.json:

{
    "SubscriberNamingPattern": "{Event Source}_{Event Name}[_{Element Name}]"
}

When the property is absent, null, or whitespace, the built-in default {Event Source}_{Event Name}[_{Element Name}] is used.

Alternative: On prefix with PascalCase source

Teams that prefer to omit the raw source name and use a compact PascalCase identifier:

{
    "SubscriberNamingPattern": "On{EventSource}_{EventName}[_{ElementName}]"
}

This produces OnSalesHeader_OnAfterDeleteEvent_PostingDate — no quoting required, and EventName PascalCase is a no-op because AL event identifiers already are PascalCase.

Alternative: camelCase source and event

{
    "SubscriberNamingPattern": "on{EventSource}_{EventName}[_{ElementName}]"
}

This produces onSalesHeader_OnAfterDeleteEvent — only the first token is lowercased; subsequent tokens keep their own placeholder casing.

Alternative: snake_case throughout

{
    "SubscriberNamingPattern": "on_{event_source}_{event_name}[_{element_name}]"
}

This produces on_sales_header_on_after_delete_event_posting_date — all tokens in snake_case with a leading on_ prefix.

Alternative: project-specific acronyms

Add domain-specific acronyms (partner prefixes, product names, or industry abbreviations not in the built-in list) via the KnownAcronyms array:

{
    "KnownAcronyms": ["Acme", "MyCo", "GDPR"]
}

Entries are matched case-insensitively; the first entry supplied for a given case-insensitive key defines the preferred canonical casing emitted in the diagnostic message. The user list is merged with the built-in defaults; when the user supplies any entries for a given case-insensitive key, those entries fully replace the built-in variants for that key.

KnownAcronyms serves two purposes:

  1. Preferred casing for all-lowercase sources. When the source word is all-lowercase, the registry supplies the preferred canonical casing.

    • Field "acme product" + "KnownAcronyms": ["Acme"]AcmeProduct (registry canonicalises the lowercase source).
    • Field "acme product" without "KnownAcronyms": ["Acme"]AcmeProduct (EnsureUpperFirst fallback: first letter up, remainder as-is).
    • If multiple configured entries map to the same case-insensitive key (for example "Http" and "HTTP"), the first entry is canonical and subsequent entries become additionally accepted variants ("KnownAcronyms": ["Http", "HTTP"]http renders as Http, and HTTP is additionally accepted for uppercase-carrying sources; reversed order renders as HTTP and additionally accepts Http).
  2. Additionally accepted variants for uppercase-carrying sources. When the source word already carries uppercase and the registry contains one or more differently cased entries on the same case-insensitive key, all such entries are additionally accepted alongside the original spelling — the CodeFix always continues to suggest the original casing.

    • Subscriber to OnAfterCalcOverdueBalanceLCY + "KnownAcronyms": ["Lcy"] → both ...OnAfterCalcOverdueBalanceLCY (preferred, original-wins) and ...OnAfterCalcOverdueBalanceLcy (registered variant) are accepted; any third spelling such as ...LcYAmount still triggers a diagnostic. The CodeFix rewrites to the LCY form.
    • Field "ACME Product" + "KnownAcronyms": ["Acme"] → both ACMEProduct (preferred) and AcmeProduct (registered variant) are accepted; the CodeFix suggests ACMEProduct.
    • The built-in default list also ships multi-variant keys (for example BoM/Bom and UoM/Uom); every listed variant is additionally accepted whenever the source word carries uppercase, without any project-specific configuration.

The rationale: the preferred form is always aligned with the source object/field casing, so Microsoft- or partner-owned identifiers are never re-cast. The accepted-variant mechanism is an opt-in escape hatch that lets teams acknowledge project-specific acronym spellings without giving up that guarantee.

The KnownAcronyms setting is shared infrastructure — future analyzers that render or validate identifiers derived from natural-language input use the same list.

Exceptions

The rule silently skips a subscriber when any of the following holds:

  • The source object cannot be resolved (e.g. a numeric object ID with no matching object in the compilation).
  • The event name is missing or empty.
  • The method is marked Obsolete.
  • AL304 length guard: the canonical name would exceed 120 characters — the AL identifier length limit. Suggesting such a name would move the violation from LC0098 into AL304 , so the rule stays quiet instead.
  • Collision guard: a sibling method in the same containing type already carries the canonical name, or another event subscriber in that type would compute to the same canonical name. Applying the CodeFix would then produce a duplicate-identifier compile error, so LC0098 stays quiet on both. Two subscribers to the same event inside one codeunit is a legal, not uncommon pattern. The guard is deliberately conservative: any sibling name clash suppresses, even when signatures technically differ and would compile as overloads — renaming into an overload set changes semantics and confuses readers.

See also

  • LC0092 — Naming pattern: general regex-based naming for procedures and other symbols. LC0092 and LC0098 are orthogonal — structural template (LC0098) vs. character-class regex (LC0092) — and their settings are decoupled. A subscriber that violates both receives two independent diagnostics.
  • LC0028 — Identifiers in event subscribers: event and element names must use identifier syntax, not string literals.