You can register IApplicationModelProvider to inform MVC about those actions. But to play well with other parts of MVC, we also need to annotate method with metadata. Unfortunately metadata extraction routines are packed inside DefaultApplicationModelProvider and marked as internal. Here we are reusing DefaultApplicationModelProvider to populate metadata via reflection.
internal sealed class ControllerDefaultInterfaceMethodActionModelProvider : IApplicationModelProvider
{
private readonly IServiceProvider _serviceProvider;
private readonly Type _type;
private IApplicationModelProvider? _defaultModelProvider;
private IApplicationModelProvider DefaultModelProvider => _defaultModelProvider
??= _serviceProvider.GetServices<IApplicationModelProvider>()
.First(x => x.GetType() == _type);
public ControllerDefaultInterfaceMethodActionModelProvider(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
// Microsoft.AspNetCore.Mvc.ApplicationModels.DefaultApplicationModelProvider;
_type = Type.GetType("Microsoft.AspNetCore.Mvc.ApplicationModels.DefaultApplicationModelProvider, Microsoft.AspNetCore.Mvc.Core")!;
Debug.Assert(_type != null);
}
public void OnProvidersExecuted(ApplicationModelProviderContext context) { }
public void OnProvidersExecuting(ApplicationModelProviderContext context)
{
const BindingFlags bindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
// internal ActionModel? CreateActionModel(TypeInfo typeInfo, MethodInfo methodInfo)
var createActionModelParams = new object [2];
var createActionModel = _type.GetMethod("CreateActionModel", bindingFlags)!;
// internal ParameterModel CreateParameterModel(ParameterInfo parameterInfo)
var createParameterModelParams = new object [1];
var createParameterModel = _type.GetMethod("CreateParameterModel", bindingFlags)!;
foreach (ControllerModel controllerModel in context.Result.Controllers)
{
var controllerType = controllerModel.ControllerType;
createActionModelParams[0] = controllerType;
foreach (Type @interface in controllerType.ImplementedInterfaces)
{
var mapping = controllerType.GetInterfaceMap(@interface);
for (var i = 0; i < mapping.InterfaceMethods.Length; ++i)
{
MethodInfo interfaceMethod = mapping.InterfaceMethods[i];
MethodInfo targetMethod = mapping.TargetMethods[i];
// check is method implemented by interface itself
if (targetMethod != interfaceMethod)
continue;
// based on https://github.com/dotnet/aspnetcore/blob/d3b7623a90d79719c0efe5fa0098f698176efa16/src/Mvc/Mvc.Core/src/ApplicationModels/DefaultApplicationModelProvider.cs#L65-L96
// You can also register interface properties via CreatePropertyModel, but I don't think it's a good idea
// Also you can augment `controllerModel` based on some attributes on `@interface` type (mainly controllerModel.Filters)
createActionModelParams[1] = interfaceMethod;
var actionModel = (ActionModel?)createActionModel.Invoke(DefaultModelProvider, createActionModelParams);
if (actionModel == null)
continue;
actionModel.Controller = controllerModel;
controllerModel.Actions.Add(actionModel);
foreach (var parameterInfo in actionModel.ActionMethod.GetParameters())
{
createParameterModelParams[0] = parameterInfo;
var parameterModel = (ParameterModel?)createParameterModel.Invoke(DefaultModelProvider, createParameterModelParams);
if (parameterModel != null)
{
parameterModel.Action = actionModel;
actionModel.Parameters.Add(parameterModel);
}
}
}
}
}
}
public int Order => DefaultModelProvider.Order + 1;
}
IActionDescriptorProvider.IActionDescriptorCollectionProvider. ControllerActionDescriptorProvider which isIActionDescriptorProvideris alsointernal sealedIActionDescriptorProvideris an interface. You can implement your own concrete class.