0

How to use default interface methods as MVC controller actions? Since interface methods exists on interface type, they aren't discovered by ASP as actions by default. Example:

public interface IGetEntityControllerMixin<TEntity> : IControllerBase
    where TEntity : class, IEntity, new()
{
    IRepository<TEntity> Repository { get; }

    [HttpGet("{id:int}")]
    public async Task<ActionResult<TEntity>> Get(int id)
    {
        var entity = await Repository.GetByIdAsync(id);

        return entity == null ? NotFound() : Ok(entity);
    }
}
public interface IPagingEntityControllerMixin<TEntity> : IControllerBase
    where TEntity : class, IEntity, new()
{ ... }

[ApiController]
[Route("[controller]")]
public class MyEntityController : ControllerBase,
                                  IGetEntityControllerMixin<MyEntity>,
                                  IPagingEntityControllerMixin<MyEntity>
{
    public IRepository<MyEntity> Repository { get; }
    public MyEntityController(IRepository<MyEntity> repository)
        => Repository = repository;
}
8
  • I have no idea if it will work, but you might want to look at IActionDescriptorProvider. Commented Aug 15, 2022 at 8:45
  • @DiplomacyNotWar its readonly learn.microsoft.com/en-us/dotnet/api/… and default implementatoin is internal github.com/dotnet/aspnetcore/blob/… Commented Aug 15, 2022 at 8:46
  • That's not the same thing at all. Commented Aug 15, 2022 at 8:48
  • @DiplomacyNotWar Ok, sorry I was looking at IActionDescriptorCollectionProvider. ControllerActionDescriptorProvider which is IActionDescriptorProvider is also internal sealed Commented Aug 15, 2022 at 8:57
  • IActionDescriptorProvider is an interface. You can implement your own concrete class. Commented Aug 15, 2022 at 9:00

1 Answer 1

1

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;
}
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.