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.
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.
Concrete framework types that are always resolved from DI / the request pipeline rather than bound from user input.
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.
Inside a string literal commas/brackets are data, not structure — e.g.
a route literal "/a,b" or a new[] { "GET", "POST" } arg. Single
quotes are NOT a quote style: in C# they wrap a char literal, which
never carries a route, and treating ' as a quote would let an
apostrophe inside a "..."-free fragment swallow the rest of the list.
Empties::DropTrailing, not DropAll: the comma branch pushed
current.to_s.strip with no emptiness guard, so (a, , b) kept its
interior ""; only the tail was guarded by unless tail.empty?.
File-local because no other splitter combines Nest::Angle with
per-kind counters — java/wicket.cr comes closest and shares one depth.
Class methods
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 — 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 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.