Analyzer::CSharp::Common
Constants
ASP.NET Core and classic ASP.NET MVC 5 spell a controller almost
identically — public class UserController : Controller with [HttpGet]
action attributes — and both analyzers are handed every .cs file in the
scan. In a solution holding both (a migration in progress, the shape noir
gets pointed at) each read the other's controllers: 35 phantom
cs_aspnet_core_mvc endpoints out of the classic fixture, 6 the other way,
several with the wrong route template applied on the way out.
The namespaces are unmistakable and mutually exclusive — a type can only come from one of them — so each analyzer skips a file that names the other's. Nothing positive is required, so a helper file that names neither is still analyzed by both, exactly as before.
An attribute list ([FromBody], [property: JsonPropertyName("x")]),
not an array rank (int[], int[,]).
A public auto-property (public string Name { get; set; }), its name in
group 1 — how request DTOs declare their bindable fields.
After a declared type's name: its generic parameters and primary
constructor / positional-record parameters, then : and the first
base-list entry, which for a class or record is its base class.
Request access in a model binder's source: the value posted under the parameter's own name, a value provider, or the raw request.
A Carter module is either class X : ICarterModule or
class X : CarterModule — the latter being Carter's abstract base, which
adds a constructor base path (: base("/directors")) and the request
filters. Both shapes own their AddRoutes body, so the Carter analyzer
claims them and the minimal-API analyzer skips them.
\bCarterModule\b alone does not match inside ICarterModule (I and C
are both word characters), hence the explicit optional I.
[FromRoute(Name = "organizationId")] Guid sponsoringOrgId binds the
route value organizationId; the C# identifier is only the local name.
Reporting the identifier invents a parameter the client cannot send and
hides the one it must.
A one-element collection type a JSON array deserializes into, or an array of one: the element type is group 1 or 2.
A [JsonIgnore] that also skips the member on read (Condition = WhenWritingNull only affects writing).
Concrete framework types that are always resolved from DI / the request pipeline rather than bound from user input.
Deepest base-class chain followed.
Longest method signature build_signature reads.
---- compile-time string values ----------------------------------------
Route templates and area names are often not literals at the call site:
pattern: $"{lang}/login/", [Area(AreaNames.ADMIN)],
$"{{{RouteValue.SeName}}}". csharp_string_value reads the value an
expression has when it is fixed by the source: a literal, an
interpolation of readable holes, a local or const/static string, a
Type.Member constant the scan declares, or a parameterless string
method's single return (or an empty fall-through after
setting-gated early returns). Anything else (a parameter, a
concatenation, a ternary, a call with arguments) is nil, and the caller
keeps its old behaviour.
A file's namespace X.Y; / namespace X.Y { and its using X.Y;
imports (an alias or using static imports no namespace).
A public property with the attributes above it ([JsonIgnore],
[FromHeader(Name = "X-Tenant")]): {attributes, type, name, "{"}, the
last group marking where the accessor list opens.
A public set/init accessor: what a binder or serializer writes.
Opts a member without a public setter into deserialization.
IFormFile/IFormFileCollection/IFormCollection are interfaces but
bind from the request body (file upload / form), not from DI — keep
them as request inputs even though they match the interface rule below.
High-precision suffixes that mark a type as a dependency-injected
collaborator. Deliberately conservative: suffixes that collide with
common domain/entity names (e.g. Client, Provider, Factory) are
left out so request DTOs aren't dropped by mistake. Interface-typed
DI is caught separately by the I<Pascal> rule.
Handler and DataSource joined the list after a minimal-API sweep:
Bitwarden injects AccessRequestEndpointsHandler handler straight into its
handler delegates and Carter's sample injects EndpointDataSource, both of
which surfaced as query parameters. Neither suffix names a request DTO in
practice. Db is the DbContext naming of Microsoft's own minimal-API
samples (TodoDb : DbContext, injected as TodoDb db).
A block opener that is not a member body: locals of the enclosing member stay visible inside it.
A type declaration's name, for the cross-file type index.
Class methods
The request source a [FromX] attribute binds a parameter from.
Heuristic for whether a parameter's type names a dependency-injected service (DbContext, repository, MediatR sender, mapper, …) rather than a value bound from the request. ASP.NET Core can't be statically resolved against its DI container, so we lean on near-universal naming conventions in real code:
- Interfaces (
I+ PascalCase) are never deserialized from a request body and never bound from the query string — they're DI or special pipeline types. The[a-z]third-char guard keeps acronym value types likeIPAddress(I-P-A) out of the net. - A small set of concrete framework types and service suffixes.
Returns false for the form-upload interfaces, which are request inputs.
Standard .NET test-source conventions:
/test/and/tests/parent directories — Microsoft's own repos park unit + integration tests undersrc/<Project>/test/...(aspnetcore) ortests/...(smaller solutions)./testassets/— aspnetcore's helper-controller convention for spinning up a real server inside the test harness.Tests.cs/Test.csfilename (.vbalike) — xUnit / NUnit / MSTest suffix convention.
dotnet/aspnetcore alone parks ~3,600 phantom endpoints under
src/Mvc/test/... and similar trees. Production code never
adopts any of these.
Takes the scan-base-relative path (Analyzer#base_relative_path),
never the absolute one. The conventions describe a location inside
the solution, so on an absolute path a test/ directory above the
scan base suppressed the whole project.
The JSON camelCase naming policy (System.Text.Json and Json.NET agree):
lower the leading run of capitals, except the one that starts the next
word, so OTP → otp, URLValue → urlValue, Id → id.
The C# name a member definition declares ([Attr] string Name = "x").
The longest project root containing path, or nil when the file sits
outside every discovered project (no .csproj in the scan at all, the
shape most fixtures and single-file samples have).
Directories that own a .csproj, longest first. A .NET project is the
unit that owns a routing table: MapControllerRoute in one project's
Program.cs says nothing about a controller compiled into a sibling
project, even though both sit under one configured scan base.
Strips a route-template placeholder down to its bare parameter name:
{id:int} → id, {slug?} → slug, {*catchAll} → catchAll,
{id=5} → id (a default value), {**slug:regex(a=b)} → slug.
The =default form was previously kept verbatim, so a template like
/items/{id=5} emitted a parameter literally named id=5.