Cache more things in HandlerMethodDescriptor

Add tests for DefaultPageHandlerMethodSelector
This commit is contained in:
Pranav K 2017-02-28 17:42:24 -08:00
parent 7b53ba1f6b
commit 4faef7afaf
7 changed files with 727 additions and 97 deletions

View File

@ -4,6 +4,7 @@
using System;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.Extensions.Primitives;
namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
{
@ -12,5 +13,9 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
public MethodInfo Method { get; set; }
public Func<Page, object, Task<IActionResult>> Executor { get; set; }
public string HttpMethod { get; set; }
public StringSegment FormAction { get; set; }
}
}

View File

@ -3,140 +3,106 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
{
public class DefaultPageHandlerMethodSelector : IPageHandlerMethodSelector
{
private const string FormAction = "formaction";
public HandlerMethodDescriptor Select(PageContext context)
{
var handlers = new List<HandlerMethodAndMetadata>(context.ActionDescriptor.HandlerMethods.Count);
for (var i = 0; i < context.ActionDescriptor.HandlerMethods.Count; i++)
var handlers = SelectHandlers(context);
if (handlers == null || handlers.Count == 0)
{
handlers.Add(HandlerMethodAndMetadata.Create(context.ActionDescriptor.HandlerMethods[i]));
return null;
}
for (var i = handlers.Count - 1; i >= 0; i--)
List<HandlerMethodDescriptor> ambiguousMatches = null;
HandlerMethodDescriptor bestMatch = null;
for (var score = 2; score >= 0; score--)
{
var handler = handlers[i];
if (handler.HttpMethod != null &&
!string.Equals(handler.HttpMethod, context.HttpContext.Request.Method, StringComparison.OrdinalIgnoreCase))
for (var i = 0; i < handlers.Count; i++)
{
handlers.RemoveAt(i);
}
}
var formaction = Convert.ToString(context.RouteData.Values["formaction"]);
for (var i = handlers.Count - 1; i >= 0; i--)
{
var handler = handlers[i];
if (handler.Formaction != null &&
!string.Equals(handler.Formaction, formaction, StringComparison.OrdinalIgnoreCase))
{
handlers.RemoveAt(i);
}
}
var ambiguousMatches = (List<HandlerMethodDescriptor>)null;
var best = (HandlerMethodAndMetadata?)null;
for (var i = 2; i >= 0; i--)
{
for (var j = 0; j < handlers.Count; j++)
{
var handler = handlers[j];
if (handler.GetScore() == i)
var handler = handlers[i];
if (GetScore(handler) == score)
{
if (best == null)
if (bestMatch == null)
{
best = handler;
bestMatch = handler;
continue;
}
if (ambiguousMatches == null)
{
ambiguousMatches = new List<HandlerMethodDescriptor>();
ambiguousMatches.Add(best.Value.Handler);
ambiguousMatches.Add(bestMatch);
}
ambiguousMatches.Add(handler.Handler);
ambiguousMatches.Add(handler);
}
}
if (ambiguousMatches != null)
{
throw new InvalidOperationException($"Selecting a handler is ambiguous! Matches: {string.Join(", ", ambiguousMatches)}");
var ambiguousMethods = string.Join(", ", ambiguousMatches.Select(m => m.Method));
throw new InvalidOperationException(Resources.FormatAmbiguousHandler(Environment.NewLine, ambiguousMethods));
}
if (best != null)
if (bestMatch != null)
{
return best.Value.Handler;
return bestMatch;
}
}
return null;
}
// Bad prototype substring implementation :)
private struct HandlerMethodAndMetadata
private static List<HandlerMethodDescriptor> SelectHandlers(PageContext context)
{
public static HandlerMethodAndMetadata Create(HandlerMethodDescriptor handler)
var handlers = context.ActionDescriptor.HandlerMethods;
List<HandlerMethodDescriptor> handlersToConsider = null;
var formAction = Convert.ToString(context.RouteData.Values[FormAction]);
for (var i = 0; i < handlers.Count; i++)
{
var name = handler.Method.Name;
string httpMethod;
if (name.StartsWith("OnGet", StringComparison.Ordinal))
var handler = handlers[i];
if (handler.HttpMethod != null &&
!string.Equals(handler.HttpMethod, context.HttpContext.Request.Method, StringComparison.OrdinalIgnoreCase))
{
httpMethod = "GET";
continue;
}
else if (name.StartsWith("OnPost", StringComparison.Ordinal))
else if (handler.FormAction.HasValue &&
!handler.FormAction.Equals(formAction, StringComparison.OrdinalIgnoreCase))
{
httpMethod = "POST";
}
else
{
httpMethod = null;
continue;
}
var formactionStart = httpMethod?.Length + 2 ?? 0;
var formactionLength = name.EndsWith("Async", StringComparison.Ordinal)
? name.Length - formactionStart - "Async".Length
: name.Length - formactionStart;
if (handlersToConsider == null)
{
handlersToConsider = new List<HandlerMethodDescriptor>();
}
var formaction = formactionLength == 0 ? null : name.Substring(formactionStart, formactionLength);
return new HandlerMethodAndMetadata(handler, httpMethod, formaction);
handlersToConsider.Add(handler);
}
public HandlerMethodAndMetadata(HandlerMethodDescriptor handler, string httpMethod, string formaction)
return handlersToConsider;
}
private static int GetScore(HandlerMethodDescriptor descriptor)
{
if (descriptor.FormAction != null)
{
Handler = handler;
HttpMethod = httpMethod;
Formaction = formaction;
return 2;
}
public HandlerMethodDescriptor Handler { get; }
public string HttpMethod { get; }
public string Formaction { get; }
public int GetScore()
else if (descriptor.HttpMethod != null)
{
if (Formaction != null)
{
return 2;
}
else if (HttpMethod != null)
{
return 1;
}
else
{
return 0;
}
return 1;
}
else
{
return 0;
}
}
}

View File

@ -18,12 +18,12 @@ using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.Evolution;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
{
public class PageActionInvokerProvider : IActionInvokerProvider
{
private static readonly string[] _handlerMethodNames = new string[] { "OnGet", "OnPost" };
private const string PageStartFileName = "_PageStart.cshtml";
private readonly IPageLoader _loader;
private readonly IPageFactoryProvider _pageFactoryProvider;
@ -204,7 +204,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
var pageStartItems = _razorProject.FindHierarchicalItems(descriptor.ViewEnginePath, PageStartFileName);
foreach (var item in pageStartItems)
{
if(item.Exists)
if (item.Exists)
{
var factoryResult = _razorPageFactoryProvider.CreateFactory(item.Path);
if (factoryResult.Success)
@ -220,21 +220,91 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
// Internal for testing.
internal static void PopulateHandlerMethodDescriptors(TypeInfo type, CompiledPageActionDescriptor actionDescriptor)
{
for (var i = 0; i < _handlerMethodNames.Length; i++)
var methods = type.GetMethods();
for (var i = 0; i < methods.Length; i++)
{
var methodName = _handlerMethodNames[i];
var method = type.GetMethod(methodName);
if (method != null && !method.IsGenericMethod)
var method = methods[i];
if (!IsValidHandler(method))
{
actionDescriptor.HandlerMethods.Add(new HandlerMethodDescriptor()
{
Method = method,
Executor = ExecutorFactory.CreateExecutor(actionDescriptor, method),
});
continue;
}
string httpMethod;
int formActionStart;
if (method.Name.StartsWith("OnGet", StringComparison.Ordinal))
{
httpMethod = "GET";
formActionStart = "OnGet".Length;
}
else if (method.Name.StartsWith("OnPost", StringComparison.Ordinal))
{
httpMethod = "POST";
formActionStart = "OnPost".Length;
}
else
{
continue;
}
var formActionLength = method.Name.Length - formActionStart;
if (method.Name.EndsWith("Async", StringComparison.OrdinalIgnoreCase))
{
formActionLength -= "Async".Length;
}
var formAction = new StringSegment(method.Name, formActionStart, formActionLength);
var handlerMethodDescriptor = new HandlerMethodDescriptor
{
Method = method,
Executor = ExecutorFactory.CreateExecutor(actionDescriptor, method),
FormAction = formAction,
HttpMethod = httpMethod,
};
actionDescriptor.HandlerMethods.Add(handlerMethodDescriptor);
}
}
private static bool IsValidHandler(MethodInfo methodInfo)
{
// The SpecialName bit is set to flag members that are treated in a special way by some compilers
// (such as property accessors and operator overloading methods).
if (methodInfo.IsSpecialName)
{
return false;
}
// Overriden methods from Object class, e.g. Equals(Object), GetHashCode(), etc., are not valid.
if (methodInfo.GetBaseDefinition().DeclaringType == typeof(object))
{
return false;
}
if (methodInfo.IsStatic)
{
return false;
}
if (methodInfo.IsAbstract)
{
return false;
}
if (methodInfo.IsConstructor)
{
return false;
}
if (methodInfo.IsGenericMethod)
{
return false;
}
return methodInfo.IsPublic;
}
internal class InnerCache
{
public InnerCache(int version)

View File

@ -106,6 +106,22 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages
return string.Format(CultureInfo.CurrentCulture, GetString("UnsupportedHandlerMethodType"), p0);
}
/// <summary>
/// Multiple handlers matched. The following handlers matched route data and had all constraints satisfied:{0}{0}{1}
/// </summary>
internal static string AmbiguousHandler
{
get { return GetString("AmbiguousHandler"); }
}
/// <summary>
/// Multiple handlers matched. The following handlers matched route data and had all constraints satisfied:{0}{0}{1}
/// </summary>
internal static string FormatAmbiguousHandler(object p0, object p1)
{
return string.Format(CultureInfo.CurrentCulture, GetString("AmbiguousHandler"), p0, p1);
}
/// <summary>
/// Path must be an application relative path that starts with a forward slash '/'.
/// </summary>

View File

@ -135,6 +135,9 @@
<data name="UnsupportedHandlerMethodType" xml:space="preserve">
<value>Unsupported handler method return type '{0}'.</value>
</data>
<data name="AmbiguousHandler" xml:space="preserve">
<value>Multiple handlers matched. The following handlers matched route data and had all constraints satisfied:{0}{0}{1}</value>
</data>
<data name="PathMustBeAnAppRelativePath" xml:space="preserve">
<value>Path must be an application relative path that starts with a forward slash '/'.</value>
</data>

View File

@ -0,0 +1,406 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Reflection;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Primitives;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
{
public class DefaultPageHandlerMethodSelectorTest
{
[Fact]
public void Select_ReturnsNull_WhenNoHandlerMatchesHttpMethod()
{
// Arrange
var descriptor1 = new HandlerMethodDescriptor
{
HttpMethod = "GET"
};
var descriptor2 = new HandlerMethodDescriptor
{
HttpMethod = "POST"
};
var pageContext = new PageContext
{
ActionDescriptor = new CompiledPageActionDescriptor
{
HandlerMethods =
{
descriptor1,
descriptor2,
},
},
RouteData = new RouteData(),
HttpContext = new DefaultHttpContext
{
Request =
{
Method = "PUT"
},
},
};
var selector = new DefaultPageHandlerMethodSelector();
// Act
var actual = selector.Select(pageContext);
// Assert
Assert.Null(actual);
}
[Fact]
public void Select_ReturnsOnlyHandler()
{
// Arrange
var descriptor = new HandlerMethodDescriptor
{
HttpMethod = "GET"
};
var pageContext = new PageContext
{
ActionDescriptor = new CompiledPageActionDescriptor
{
HandlerMethods =
{
descriptor,
},
},
RouteData = new RouteData(),
HttpContext = new DefaultHttpContext
{
Request =
{
Method = "GET"
},
},
};
var selector = new DefaultPageHandlerMethodSelector();
// Act
var actual = selector.Select(pageContext);
// Assert
Assert.Same(descriptor, actual);
}
[Theory]
[InlineData("GET")]
[InlineData("POST")]
public void Select_ReturnsHandlerWithMatchingHttpRequestMethod(string httpMethod)
{
// Arrange
var descriptor1 = new HandlerMethodDescriptor
{
HttpMethod = "PUT",
};
var descriptor2 = new HandlerMethodDescriptor
{
HttpMethod = httpMethod,
};
var pageContext = new PageContext
{
ActionDescriptor = new CompiledPageActionDescriptor
{
HandlerMethods =
{
descriptor1,
descriptor2,
},
},
RouteData = new RouteData(),
HttpContext = new DefaultHttpContext
{
Request =
{
Method = httpMethod,
},
},
};
var selector = new DefaultPageHandlerMethodSelector();
// Act
var actual = selector.Select(pageContext);
// Assert
Assert.Same(descriptor2, actual);
}
[Fact]
public void Select_ReturnsNullWhenNoHandlerMatchesFormAction()
{
// Arrange
var descriptor1 = new HandlerMethodDescriptor
{
HttpMethod = "POST",
FormAction = new StringSegment("Add"),
};
var descriptor2 = new HandlerMethodDescriptor
{
HttpMethod = "POST",
FormAction = new StringSegment("Delete"),
};
var pageContext = new PageContext
{
ActionDescriptor = new CompiledPageActionDescriptor
{
HandlerMethods =
{
descriptor1,
descriptor2,
},
},
RouteData = new RouteData
{
Values =
{
{ "formaction", "update" }
}
},
HttpContext = new DefaultHttpContext
{
Request =
{
Method = "POST"
},
},
};
var selector = new DefaultPageHandlerMethodSelector();
// Act
var actual = selector.Select(pageContext);
// Assert
Assert.Null(actual);
}
[Fact]
public void Select_ReturnsHandlerThatMatchesFormAction()
{
// Arrange
var descriptor1 = new HandlerMethodDescriptor
{
HttpMethod = "POST",
FormAction = new StringSegment("Add"),
};
var descriptor2 = new HandlerMethodDescriptor
{
HttpMethod = "POST",
FormAction = new StringSegment("Delete"),
};
var pageContext = new PageContext
{
ActionDescriptor = new CompiledPageActionDescriptor
{
HandlerMethods =
{
descriptor1,
descriptor2,
},
},
RouteData = new RouteData
{
Values =
{
{ "formaction", "Add" }
}
},
HttpContext = new DefaultHttpContext
{
Request =
{
Method = "Post"
},
},
};
var selector = new DefaultPageHandlerMethodSelector();
// Act
var actual = selector.Select(pageContext);
// Assert
Assert.Same(descriptor1, actual);
}
[Fact]
public void Select_ReturnsHandlerWithMatchingHttpMethodWithoutAFormAction()
{
// Arrange
var descriptor1 = new HandlerMethodDescriptor
{
HttpMethod = "POST",
FormAction = new StringSegment("Subscribe"),
};
var descriptor2 = new HandlerMethodDescriptor
{
HttpMethod = "POST",
};
var pageContext = new PageContext
{
ActionDescriptor = new CompiledPageActionDescriptor
{
HandlerMethods =
{
descriptor1,
descriptor2,
},
},
RouteData = new RouteData
{
Values =
{
{ "formaction", "Add" }
}
},
HttpContext = new DefaultHttpContext
{
Request =
{
Method = "Post"
},
},
};
var selector = new DefaultPageHandlerMethodSelector();
// Act
var actual = selector.Select(pageContext);
// Assert
Assert.Same(descriptor2, actual);
}
[Fact]
public void Select_WithoutFormAction_ThrowsIfMoreThanOneHandlerMatches()
{
// Arrange
var descriptor1 = new HandlerMethodDescriptor
{
Method = GetType().GetMethod(nameof(Post)),
HttpMethod = "POST",
};
var descriptor2 = new HandlerMethodDescriptor
{
Method = GetType().GetMethod(nameof(PostAsync)),
HttpMethod = "POST",
};
var descriptor3 = new HandlerMethodDescriptor
{
HttpMethod = "GET",
};
var pageContext = new PageContext
{
ActionDescriptor = new CompiledPageActionDescriptor
{
HandlerMethods =
{
descriptor1,
descriptor2,
descriptor3,
},
},
RouteData = new RouteData(),
HttpContext = new DefaultHttpContext
{
Request =
{
Method = "Post"
},
},
};
var selector = new DefaultPageHandlerMethodSelector();
// Act & Assert
var ex = Assert.Throws<InvalidOperationException>(() => selector.Select(pageContext));
var methods = descriptor1.Method + ", " + descriptor2.Method;
var message = "Multiple handlers matched. The following handlers matched route data and had all constraints satisfied:" +
Environment.NewLine + Environment.NewLine + methods;
Assert.Equal(message, ex.Message);
}
[Fact]
public void Select_WithFormAction_ThrowsIfMoreThanOneHandlerMatches()
{
// Arrange
var descriptor1 = new HandlerMethodDescriptor
{
Method = GetType().GetMethod(nameof(Post)),
HttpMethod = "POST",
FormAction = new StringSegment("Add"),
};
var descriptor2 = new HandlerMethodDescriptor
{
Method = GetType().GetMethod(nameof(PostAsync)),
HttpMethod = "POST",
FormAction = new StringSegment("Add"),
};
var descriptor3 = new HandlerMethodDescriptor
{
HttpMethod = "GET",
};
var pageContext = new PageContext
{
ActionDescriptor = new CompiledPageActionDescriptor
{
HandlerMethods =
{
descriptor1,
descriptor2,
descriptor3,
},
},
RouteData = new RouteData
{
Values =
{
{ "formaction", "Add" }
}
},
HttpContext = new DefaultHttpContext
{
Request =
{
Method = "Post"
},
},
};
var selector = new DefaultPageHandlerMethodSelector();
// Act & Assert
var ex = Assert.Throws<InvalidOperationException>(() => selector.Select(pageContext));
var methods = descriptor1.Method + ", " + descriptor2.Method;
var message = "Multiple handlers matched. The following handlers matched route data and had all constraints satisfied:" +
Environment.NewLine + Environment.NewLine + methods;
Assert.Equal(message, ex.Message);
}
public void Post()
{
}
public void PostAsync()
{
}
}
}

View File

@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
@ -382,6 +383,11 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
Assert.Equal(typeof(InheritsMethods), handler.Method.DeclaringType);
},
(handler) =>
{
Assert.Equal("OnGet", handler.Method.Name);
Assert.Equal(typeof(TestSetPageModel), handler.Method.DeclaringType);
},
(handler) =>
{
Assert.Equal("OnPost", handler.Method.Name);
Assert.Equal(typeof(TestSetPageModel), handler.Method.DeclaringType);
@ -389,7 +395,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
}
[Fact]
public void PopulateHandlerMethodDescriptors_ProtectedMethodsNotFound()
public void PopulateHandlerMethodDescriptors_IgnoresNonPublicMethods()
{
// Arrange
var descriptor = new PageActionDescriptor()
@ -432,6 +438,113 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
Assert.Empty(actionDescriptor.HandlerMethods);
}
[Fact]
public void PopulateHandlerMethodDescriptors_IgnoresStaticMethods()
{
// Arrange
var descriptor = new PageActionDescriptor()
{
RelativePath = "Path1",
FilterDescriptors = new FilterDescriptor[0],
ViewEnginePath = "/Views/Index.cshtml"
};
var modelTypeInfo = typeof(PageModelWithStaticHandler).GetTypeInfo();
var expected = modelTypeInfo.GetMethod(nameof(PageModelWithStaticHandler.OnGet), BindingFlags.Public | BindingFlags.Instance);
var actionDescriptor = new CompiledPageActionDescriptor(descriptor)
{
ModelTypeInfo = modelTypeInfo,
PageTypeInfo = typeof(object).GetTypeInfo(),
};
// Act
PageActionInvokerProvider.PopulateHandlerMethodDescriptors(modelTypeInfo, actionDescriptor);
// Assert
Assert.Collection(actionDescriptor.HandlerMethods,
handler => Assert.Same(expected, handler.Method));
}
[Fact]
public void PopulateHandlerMethodDescriptors_IgnoresAbstractMethods()
{
// Arrange
var descriptor = new PageActionDescriptor()
{
RelativePath = "Path1",
FilterDescriptors = new FilterDescriptor[0],
ViewEnginePath = "/Views/Index.cshtml"
};
var modelTypeInfo = typeof(PageModelWithAbstractMethod).GetTypeInfo();
var expected = modelTypeInfo.GetMethod(nameof(PageModelWithAbstractMethod.OnGet));
var actionDescriptor = new CompiledPageActionDescriptor(descriptor)
{
ModelTypeInfo = modelTypeInfo,
PageTypeInfo = typeof(object).GetTypeInfo(),
};
// Act
PageActionInvokerProvider.PopulateHandlerMethodDescriptors(modelTypeInfo, actionDescriptor);
// Assert
Assert.Collection(actionDescriptor.HandlerMethods,
handler => Assert.Same(expected, handler.Method));
}
[Fact]
public void PopulateHandlerMethodDescriptors_DiscoversMethodsWithFormActions()
{
// Arrange
var descriptor = new PageActionDescriptor()
{
RelativePath = "Path1",
FilterDescriptors = new FilterDescriptor[0],
ViewEnginePath = "/Views/Index.cshtml"
};
var modelTypeInfo = typeof(PageModelWithFormActions).GetTypeInfo();
var actionDescriptor = new CompiledPageActionDescriptor(descriptor)
{
ModelTypeInfo = modelTypeInfo,
PageTypeInfo = typeof(object).GetTypeInfo(),
};
// Act
PageActionInvokerProvider.PopulateHandlerMethodDescriptors(modelTypeInfo, actionDescriptor);
// Assert
Assert.Collection(actionDescriptor.HandlerMethods.OrderBy(h => h.Method.Name),
handler =>
{
Assert.Same(modelTypeInfo.GetMethod(nameof(PageModelWithFormActions.OnGet)), handler.Method);
Assert.Equal("GET", handler.HttpMethod);
Assert.Equal(0, handler.FormAction.Length);
Assert.NotNull(handler.Executor);
},
handler =>
{
Assert.Same(modelTypeInfo.GetMethod(nameof(PageModelWithFormActions.OnPostAdd)), handler.Method);
Assert.Equal("POST", handler.HttpMethod);
Assert.Equal("Add", handler.FormAction.ToString());
Assert.NotNull(handler.Executor);
},
handler =>
{
Assert.Same(modelTypeInfo.GetMethod(nameof(PageModelWithFormActions.OnPostAddCustomer)), handler.Method);
Assert.Equal("POST", handler.HttpMethod);
Assert.Equal("AddCustomer", handler.FormAction.ToString());
Assert.NotNull(handler.Executor);
},
handler =>
{
Assert.Same(modelTypeInfo.GetMethod(nameof(PageModelWithFormActions.OnPostDeleteAsync)), handler.Method);
Assert.Equal("POST", handler.HttpMethod);
Assert.Equal("Delete", handler.FormAction.ToString());
Assert.NotNull(handler.Executor);
});
}
[Fact]
public void PopulateHandlerMethodDescriptors_AllowOnlyOneMethod()
{
@ -635,6 +748,57 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
}
}
private class PageModelWithStaticHandler
{
public static void OnGet(string name)
{
}
public void OnGet()
{
}
}
private abstract class PageModelWithAbstractMethod
{
public abstract void OnPost(string name);
public void OnGet()
{
}
}
private class PageModelWithFormActions
{
public void OnGet()
{
}
public void OnPostAdd()
{
}
public void OnPostAddCustomer()
{
}
public void OnPostDeleteAsync()
{
}
protected void OnPostDelete()
{
}
}
private class ProtectedModel
{
protected void OnGet()