Controllers in large ASP.NET applications often accumulate too many responsibilities. Action methods grow long, and the controller becomes a hub for complex logic that is hard to test or reuse. The approach here is similar to what MediatR does — each controller action delegates to its own command class — but with a key difference: the command class extends Controller directly, so it has full access to HttpContext, View(), Json(), and all other controller methods without a wrapper.
Usage
The controller action becomes a one-liner. All logic lives in the command class, which operates as if it were inside the controller:
public class AuthenticateController : Controller
{
private readonly IActionCommandSystemService _actionCommandService;
public AuthenticateController(IActionCommandSystemService actionCommandService)
{
_actionCommandService = actionCommandService;
}
public IActionResult Login()
{
// Delegate all action logic to a command class; expect the typed result
return _actionCommandService
.Execute<AuthenticateLoginGetCommand, ViewResult>(this);
}
}
// The command class — contains what used to be inside the controller action
public class AuthenticateLoginGetCommand : ActionCommandController<ViewResult>
{
public override ViewResult Execute()
{
// this.ControllerContext and this.HttpContext mirror the invoking controller
return View(); // renders the "Login" view
}
}Abstractions
The generic interface defines the contract. The abstract base class inherits both Controller and the interface, giving command classes the full controller surface:
// Core abstractions
public interface IActionCommand<TReturn>
{
TReturn Execute();
}
// Inherits Controller so the command class has full access to View(), Json(),
// ControllerContext, HttpContext, RouteData, etc.
public abstract class ActionCommandController<TReturn> : Controller, IActionCommand<TReturn>
{
public abstract TReturn Execute();
}Command System Service
The service uses IControllerFactory (provided by the framework via DI) to instantiate the command class with the calling controller's context. The command class receives the same HttpContext, route data, and action descriptor as if it had been invoked directly:
public interface IActionCommandSystemService
{
TOut Execute<TIn, TOut>(Controller controller)
where TIn : IActionCommand<TOut>;
}
public class ActionCommandSystemService : IActionCommandSystemService
{
private readonly IControllerFactory _factory;
public ActionCommandSystemService(IControllerFactory factory)
{
_factory = factory;
}
public TOut Execute<TIn, TOut>(Controller controller)
where TIn : IActionCommand<TOut>
{
// Build a ControllerContext that mirrors the calling controller's context
var context = new ControllerContext
{
ActionDescriptor = controller.ControllerContext.ActionDescriptor,
HttpContext = controller.HttpContext,
RouteData = controller.RouteData,
ValueProviderFactories = controller.ControllerContext.ValueProviderFactories,
};
// Tell the factory we want an instance of TIn (the command class)
context.ActionDescriptor.ControllerTypeInfo = typeof(TIn).GetTypeInfo();
var command = _factory.CreateController(context) as IActionCommand<TOut>;
if (command == null)
throw new ApplicationException("Invalid command type: " + typeof(TIn).Name);
return command.Execute();
}
}Registration
// Extension method for clean Startup registration
public static class ServiceExtensions
{
public static IServiceCollection AddActionCommandSystemService(
this IServiceCollection services)
{
return services.AddTransient<IActionCommandSystemService, ActionCommandSystemService>();
}
}
// Startup.cs — ConfigureServices
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.AddActionCommandSystemService();
}