Skip to content
This repository has been archived by the owner on Dec 14, 2018. It is now read-only.

Is there a way to bind the request data manually instead of being in the parameters list? #4811

Closed
mrahhal opened this issue Jun 4, 2016 · 9 comments
Labels
Milestone

Comments

@mrahhal
Copy link

mrahhal commented Jun 4, 2016

I have an action that handles different kinds (but similar) requests. So I need the request data to bind to different models depending on several external inputs. Is there a way to do that (so that the model isn't in the action's parameters list but bound manually)?

@dougbu
Copy link
Member

dougbu commented Jun 6, 2016

@mrahhal have a look at the TryUpdateModelAsync(...) overloads on the ControllerBase class.

@mrahhal
Copy link
Author

mrahhal commented Jun 6, 2016

@dougbu tried it. Maybe it doesn't work with data in the request's body or something? It says it gets data from the current controller's IValueProvider so I'm not sure. Is there a demo or an example in the tests?

Closest thing I'm finding on the subject is implementing my own IModelBinderConvention or the like but this really didn't turn out well for my use case. Manual binding to a model is all I need, and I think it's not that uncommon.

@dougbu
Copy link
Member

dougbu commented Jun 6, 2016

@mrahhal what exactly did you try?

@mrahhal
Copy link
Author

mrahhal commented Jun 6, 2016

The simplest case, a json post request:

class TestModel
{
    public string Some { get; set; }
}

[HttpPost("test")]
public async Task<IActionResult> Test()
{
    var model = new TestModel();
    await TryUpdateModelAsync(model); // => true
    return Ok(model);
}

HTTP POST /test with body {"Some":"Foo"} results in Some always being null.

@Eilon
Copy link
Member

Eilon commented Jun 6, 2016

I think in this case you need to read the Request's Body property and pass that to the serializer (e.g. Json.NET).

I think that TryUpdateModel will always use model binding and never user formatters/serializers.

@mrahhal, out of curiosity, what is the reason you want to do this manually instead of letting MVC do the work?

@Eilon Eilon added the question label Jun 6, 2016
@Eilon Eilon added this to the Discussions milestone Jun 6, 2016
@mrahhal
Copy link
Author

mrahhal commented Jun 6, 2016

@Eilon I have an endpoint that updates a user's profile (which is another domain model that is different from AppUser). Users can be of different kinds and each have a reference to different domain models as the profile. I want to handle the update from only one endpoint (and the action knows what model to really bind to and then what domain model to update depending on my AppUser.UserKind).

Right now I'm providing one endpoint for each user kind, so:

[HttpPut("~/api/users/{userName}/profile/kind1")]
public IActionResult PutProfileKind1(string userName, Kind1Model model) {...}

[HttpPut("~/api/users/{userName}/profile/kind2")]
public IActionResult PutProfileKind2(string userName, Kind2Model model) {...}

I'd really like to make this into a single endpoint /api/users/{userName}/profile if I'm able to manually bind to either Kind1Model or Kind2Model inside the action. The client shouldn't be restricted to how the backend does its work (and right now, binding the model is the only thing forcing me to split the endpoints like this).

So the result that I'd like to be able to do is this:

[HttpPut("~/api/users/{userName}/profile")]
public IActionResult PutProfile(string userName)
{
    var user = GetUser(userName);
    if (user.Kind == UserKind.Kind1)
    {
        // Something like this:
        var model = new Kind1Model();
        ManualBind(model);
        // check ModelState.IsValid ...
        Update(user, model);
    }
    else if (user.Kind == UserKind.Kind2)
    {
        var model = new Kind2Model();
        ManualBind(model);
        // check ModelState.IsValid ...
        Update(user, model);
    }
    // ...
}

If there is really no way to do such manual binding I can think of a lot of different scenarios where this might be really useful. Well, I can always read the request's body manually but I lose some of the binding's features (ModelState.IsValid for example).

@dougbu
Copy link
Member

dougbu commented Jun 6, 2016

@mrahhal the following should do the trick but #4652 (fixed in 1.0.0) and FromBodyAttribute restrictions will get in your way.

[FromBody]
class TestModel
{
    public string Some { get; set; }
}

Instead

public TestModelBindingMetadataProvider : IBindingMetadataProvider
{
    public void CreateBindingMetadata(BindingMetadataProviderContext context)
    {
        if (context == null)
        {
            throw new ArgumentNullException(nameof(context));
        }

        if (context.Key.Name == null && context.Key.ModelType == typeof(TestModel))
        {
            context.BindingMetadata.BindingSource = BindingSource.Body;
        }
    }
}

The first option works in current nightlies, not RC2.

@mrahhal
Copy link
Author

mrahhal commented Jun 7, 2016

@dougbu so I register TestModelBindingMetadataProvider, and then use TryUpdateModelAsync? I tried this but all the same, didn't work at all.

@dougbu
Copy link
Member

dougbu commented Jun 7, 2016

@mrahhal please provide a complete repro project, preferably as a GitHub repo.

@mrahhal mrahhal closed this as completed Dec 21, 2016
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

No branches or pull requests

3 participants