0

I have a controller with 4 post methods all with the same name but with different parameter types as follows. (Please note I am just testing here, so ignore the body of each method and the fact that I am not using ValidateAntiForgeryToken yet.)

[HttpPost]
public IActionResult CreateHealthCheck(FileHealthCheckOptions model)
{
    var json = JsonConvert.SerializeObject(model);
    return View("Index");
}


[HttpPost]
public IActionResult CreateHealthCheck(HTTPHealthCheckOptions model)
{
    var json = JsonConvert.SerializeObject(model);
    return View("Index");
}


[HttpPost]
public IActionResult CreateHealthCheck(PingHealthCheckOptions model)
{
    var json = JsonConvert.SerializeObject(model);
    return View("Index");
}


[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult CreateHealthCheck(SqlServerHealthCheckOptions model)
{
    var json = JsonConvert.SerializeObject(model);
    return View("Index");
}

My View includes the following

@using ADLK.AppManager.Domain.Models.HealthChecks
@model ADLK.AppManager.Domain.Interfaces.IHealthCheckOptions


@section Scripts
{
    <script src="~/js/app/health/createhealthcheck.js"></script>
}

<h4>Create Health Check</h4>

@{
    ViewBag.Title = "Application Manager - Test";
    var modelType = $"{Model.GetType().Name}".Replace("Options", "");
}

<input type="hidden" id="healthchecktype"/>
<select id="healthcheckselect" class="form-select-sm mt-2">
    @foreach (var item in ViewBag.AvailableHealthChecks)
    {
        var selected = (modelType == item);
        <option class="small" selected="@selected" value="@item">@item</option>
    }
</select>


<div class="row">
    <form asp-action="CreateHealthCheck">
        <div asp-validation-summary="ModelOnly" class="text-danger"></div>
        
        <div class="col-4 mt-4">
            <label asp-for="@Model.Name"></label>
            <input asp-for="@Model.Name" autocomplete="off" class="form-control" />
            <span asp-validation-for="@Model.Name" class="text-danger"></span>
        </div>
        . . . . .

When I submit my view, I get the error '

The request matched multiple endpoints

Perhaps there's a way to route based on the parameters? I don't know. I'd like to keep the post method name the same so that my view is clean if possible.

Each HealthCheckOptions class, is based on an IHealthCheckOptions interface. I tried setting up a single post method thus

[HttpPost]
public IActionResult CreateHealthCheck(IHealthCheckOptions model)
{
    var json = JsonConvert.SerializeObject(model);
    return View("Index");
}

but when I submit to that from my view, I get the error

Could not create an instance of type '...'. Model bound complex types must not be abstract or value types and must have a parameterless constructor

Is there a better way to do this? I'd like to implement a solution whereby I could add various new HealthCheckOptions in the future and have the Asp.Net app work for these without having to change the view. Any help greatly appreciated.

Kieran
  • 186
  • 2
  • 13
  • Does this answer your question? [Can you overload controller methods in ASP.NET MVC?](https://stackoverflow.com/questions/436866/can-you-overload-controller-methods-in-asp-net-mvc) – mlibby Apr 14 '23 at 15:03
  • Thanks for the reply. Unfortunately no, I don't think this solves the problem. I would still end up with 4 different endpoints as far as I can see. – Kieran Apr 14 '23 at 15:12
  • You simply cannot overload endpoints like this and you can't have an abstract class as a parameter because the model binder needs to bind to a concrete class. To have only a single endpoint, you can have a "super-class" that contains all possible properties and then put a property in that class that indicates which type of HealthCheckOptions this request is for. – mlibby Apr 14 '23 at 15:23
  • Thanks again. I'm not sure I understand the super class idea. Also I think the idea would negate what I am trying to do as it means I would have to update that class each time I added a new HealthCheckOption class. Basically I'm trying to adhere to the open/closed principal. I just thought that mvc would be able to bind based on the parameter type. If it absolutely can't then I may have to revert to writing a different view for each HealthCheckOption. Doesn't feel right somehow. – Kieran Apr 14 '23 at 15:36
  • 1
    I'm not aware of any way the request handler could figure out from just the form data of your POST request which of your model classes to use... Another way to solve would be to have a single controller method that doesn't have any arguments. Then inspect the form data in `this.Request.Form` and call private methods in your controller that handle each different HealthCheckOption model. Do you know the type of HealthCheckOption when you are rendering that form? Your view could modify the form's action and to use a separate route for each type. – mlibby Apr 14 '23 at 16:18

1 Answers1

1

Why you dont try to change action name attribute

[HttpPost("FileHealth"]
public IActionResult CreateHealthCheck(FileHealthCheckOptions model)
{
    var json = JsonConvert.SerializeObject(model);
    return View("Index");
}

[HttpPost("HTTPHealth")]
public IActionResult CreateHealthCheck(HTTPHealthCheckOptions model)
{
    var json = JsonConvert.SerializeObject(model);
    return View("Index");
}

or if you need the same name, leave just one action

[HttpPost]
public IActionResult CreateHealthCheck(JObject jObj)
{
   if(jObj["type"]=="FileHealth")
    return CreateHealthCheck(jObj.ToObject<FileHealthCheckOptions>())
 
   if(jObj["type"]=="...") ...

    var json = JsonConvert.SerializeObject(model);
    return View("Index");
}

and change all another actions to methods

[NonAction]
private IActionResult CreateHealthCheck(FileHealthCheckOptions model)
Serge
  • 40,935
  • 4
  • 18
  • 45
  • Serge, Many thanks for that. Yes I've dabbled with that. Again it kinda breaks my goal of adhering to open/closed principal. I guess no matter what, I simply cannot abstract the creation of HealthCheckOptions in mvc. I'm going to look at naming conventions instead. e.g. in my javascript, I'm trying things like var viewtype = $("#healthcheckselect").val(); var actionname = 'Create${viewtype}'; $("#createform").attr('action', actionname); – Kieran Apr 14 '23 at 16:15
  • All your methods are polymorphism and they use open closed principal. You should be happy. But you don't mess methods and routes. A route for mvc is just an url string, it doesn't know what is inside of the body. And when you have several actions but only one url for all of them what action should be called? This is why adding an extra route attribute instead of else if will follow your open close principal – Serge Apr 14 '23 at 16:34