Leonard
Leonard

Reputation: 3092

Asynchronous MVC controller and HttpTaskAsyncHandler

I'm trying to implemented a custom HttpTaskAsyncHandler for my custom content management solution. The idea is to route /100/my-little-pony to /Details/my-little-pony. I hope to achieve this with the following HttpTaskAsyncHandler:

public override async Task ProcessRequestAsync(HttpContext context)
{
    try
    {
        var id = GetContentIdFromRouteData();

        // Retrieve the content identified by the specified ID
        var contentRepository = new ContentRepository();
        var content = await contentRepository.GetAsync(id);

        if (content == null)
            throw new ContentNotFoundException(id);

        // Initialize an instance of the content controller
        var factory = ControllerBuilder.Current.GetControllerFactory();
        var controller = (IContentController) factory.CreateController(_requestContext, content.ControllerName);
        if (controller == null)
            throw new ControllerNotFoundException(content.ControllerName);

        try
        {
            // Retrieve all content type values and pass them on the the method for index pages
            var action = _requestContext.RouteData.GetRequiredString("action");
            if (action == "Index")
            {
                ContentType data = null;
                if (controller.ContentType != null)
                {
                    data = BusinessHost.Resolve<ContentType>(controller.ContentType);
                    data.Values = content.Parameters.ToDictionary(p => p.Name, p => p.Value);
                }
                _requestContext.RouteData.Values.Add("data", data);
            }

            var values = _requestContext.RouteData.Values;
            values.Add("name", content.Name);
            values.Add("controllerId", id);
            values.Add("controller", content.ControllerName);

            controller.Execute(_requestContext);
        }
        finally
        {
            factory.ReleaseController(controller);
        }
    }
    catch (ContentNotFoundException ex)
    {
        Trace.TraceWarning($"404: {ex.Message}");
        _requestContext.HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
    }
}

This works wonderfully well for synchronous requests, but when I try to invoke asynchronous methods ...

@using (Html.BeginForm("Save", Html.ControllerId(), FormMethod.Post, new { @class = "form-horizontal" }))

... and this being the method ...

[HttpPost]
public async Task<ActionResult> Save(NewsViewModel model)
{ }

Edit I've changed the name of the method to Save as Async isn't inferred, I receive a new error:

The asynchronous action method 'Login' returns a Task, which cannot be executed synchronously.

Upvotes: 2

Views: 558

Answers (2)

Leonard
Leonard

Reputation: 3092

There's more to MvcHandler than meets the eye, and I've come to the realization that one cannot simply hope to replicate it with a clean conscience. I've therefore decided to change the way I approach this problem entirely: instead of trying to implement my own MvcHandler, I extend my IRouteHandler instead.

This is my solution:

private static ConcurrentDictionary<string, Type> _contentTypes = new ConcurrentDictionary<string, Type>();

public IHttpHandler GetHttpHandler(RequestContext requestContext)
{
    // Retrieve the page ID from the route data
    var id = GetContentIdFromRouteData(requestContext);

    // Retrieve the content identified by the specified ID
    var contentRepository = new ContentRepository();
    var content = contentRepository.Get(id);

    if (content == null)
        throw new ContentNotFoundException(id);

    // Retrieve all content type values and pass them on the the method for index pages
    var action = requestContext.RouteData.GetRequiredString("action");
    if (action == "Index")
    {
        var data = CreateContentType(requestContext, content);
        requestContext.RouteData.Values.Add("data", data);
    }

    var values = requestContext.RouteData.Values;
    values.Add("name", content.Name);
    values.Add("controllerId", id);
    values.Add("controller", content.ControllerName);

    return new MvcHandler(requestContext);
}

private static int GetContentIdFromRouteData(RequestContext context)
{
    var idString = context.RouteData.GetRequiredString("id");
    int id;

    if (!int.TryParse(idString, out id))
        throw new ArgumentException("Content can't be loaded due to an invalid route parameter.", "id");

    return id;
}

private static ContentType CreateContentType(RequestContext context, Content content)
{
    Type type;
    if (!_contentTypes.ContainsKey(content.ControllerName) || 
        !_contentTypes.TryGetValue(content.ControllerName, out type))
    {
        var factory = ControllerBuilder.Current.GetControllerFactory();
        var controller = (IContentController)factory.CreateController(context, content.ControllerName);

        if (controller == null)
            throw new ControllerNotFoundException(content.ControllerName);

        type = controller.ContentType;
        factory.ReleaseController(controller);

        _contentTypes.TryAdd(content.ControllerName, type);                
    }

    ContentType data = null;
    if (type != null)
    {
        data = BusinessHost.Resolve<ContentType>(type);
        data.Values = content.Parameters.ToDictionary(p => p.Name, p => p.Value);
    }

    return data;
}

Upvotes: 0

Alexei Levenkov
Alexei Levenkov

Reputation: 100555

Action name is SaveAsync, but code that refers to it uses Save as the name. There is no magical renaming for any actions, including async once.

Your options:

  • use SaveAsync to refer to the action
  • use ActionName attribute to rename action
  • rename method to Save (but that would be against convention that all async methods have ...Async suffix)

Side note: using routing may be better option for redirects than some custom handler.

Upvotes: 2

Related Questions