Upon initialisation, a Razor Pages web-app builds a collection of PageApplicationModel instances, which describe the Razor Pages from the web-app and their associated handler methods.
To understand more about how this works, have a look at the source for the DefaultPageApplicationModelProvider's PopulateHandlerMethods method:
internal void PopulateHandlerMethods(PageApplicationModel pageModel)
{
var methods = pageModel.HandlerType.GetMethods();
for (var i = 0; i < methods.Length; i++)
{
var handler = _pageApplicationModelPartsProvider
.CreateHandlerModel(methods[i]);
if (handler != null)
{
pageModel.HandlerMethods.Add(handler);
}
}
}
Here, we can see that the framework enumerates the Razor Page's class's methods and calls the DefaultPageApplicationModelPartsProvider's CreateHandlerModel method for each of these. CreateHandlerModel determines if the method is a handler (e.g. it's public, not static) and then parses the method name to determine its HTTP method, handler name, etc. This parsing takes place in TryParseHandlerMethod:
internal static bool TryParseHandlerMethod(
string methodName, out string httpMethod, out string handler)
{
httpMethod = null;
handler = null;
// Handler method names always start with "On"
if (!methodName.StartsWith("On") || methodName.Length <= "On".Length)
{
return false;
}
// Now we parse the method name according to our conventions to
// determine the required HTTP method and optional 'handler name'.
// Valid names look like:
// - OnGet
// - OnPost
// - OnFooBar
// - OnTraceAsync
// - OnPostEditAsync
var start = "On".Length;
var length = methodName.Length;
if (methodName.EndsWith("Async", StringComparison.Ordinal))
{
length -= "Async".Length;
}
if (start == length)
{
// There are no additional characters. This is "On" or "OnAsync".
return false;
}
// The http method follows "On" and is required to be at least one
// character. We use casing to determine where it ends.
var handlerNameStart = start + 1;
for (; handlerNameStart < length; handlerNameStart++)
{
if (char.IsUpper(methodName[handlerNameStart]))
{
break;
}
}
httpMethod = methodName.Substring(start, handlerNameStart - start);
// The handler name follows the http method and is optional.
// It includes everything up to the end excluding the "Async" suffix
// (if present).
handler = handlerNameStart == length
? null
: methodName.Substring(handlerNameStart, length - handlerNameStart);
return true;
}
This code does a good job of explaining itself, but ultimately it parses out the HTTP method and an optional handler name.
Finally, the framework creates an instance of PageHandlerModel to hold the extracted information. With this information in place, the routing system is able to select a handler based on an incoming request. This selection logic gets handled by the DefaultPageHandlerMethodSelector class.
The DefaultPageApplicationModelPartsProvider class implements the IPageApplicationModelPartsProvider interface and gets resolved using DI. You could create your own implementation of IPageApplicationModelPartsProvider and replace the default implementation, which would allow you to perform your own method name parsing, for example.
To use a custom implementation, e.g. MyCustomPageApplicationModelPartsProvider, add something like the following to ConfigureServices, ideally before the call to AddRazorPages:
services.AddSingleton<IPageApplicationModelPartsProvider,
MyCustomPageApplicationModelPartsProvider>();
In addition to explaining how the framework finds the handlers, the answer also should explain how does the framework define one file as a code-behind for another.
The connection between a page and its PageModel is made using the @model directive in the .cshtml file. e.g. In Index.cshtml you'll see @model IndexModel. You can rename Index.cshtml.cs to SomethingElse.cs and it'll still work, so the file-naming is more of a convention.