Reputation: 20987
I have a Web Api Model Binder which looks like so:
public class KeyModelBinder : IModelBinder
{
public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
{
//...
}
}
I'm trying to write a rule to make it easier to test. I've found a function that works with MVC's Model Binders here:
But when attempting to convert to use webApi I cannot figure out how to populate the value provider
public TModel BindModel<TBinder, TModel>(NameValueCollection formCollection, TBinder binder)
where TBinder : IModelBinder
{
var valueProvider = new NameValueCollectionValueProvider(formCollection, null);
var dataProvider = new DataAnnotationsModelMetadataProvider();
var modelMetadata = dataProvider.GetMetadataForType(null, typeof(TModel));
var bindingContext = new ModelBindingContext
{
ModelName = typeof(TModel).Name,
ValueProvider = valueProvider,
ModelMetadata = modelMetadata
};
binder.BindModel(null, bindingContext);
return (TModel)bindingContext.ModelMetadata.Model;
}
NameValueCollection Only Exist in MVC, How do I create a value provider for Web-Api
Upvotes: 1
Views: 842
Reputation: 982
johnny 5's answer is correct, but, at least for me, it was hard to understand how to use it... so... consider the answer to this question which introduces a binder that we would like to unit test:
Passing UTC DateTime to Web API HttpGet Method results in local time
This merely converts all date times to a DateTimeKind.Utc. To unit test it, we need some fake URI (does not have to be in any way real):
const string MOCK_URL = "http://localhost/api/george";
and then a unit test using his BindModelFromGet method, like so:
[Test]
public void should_convert_datetime_to_utc()
{
var bar = new UtcDateTimeModelBinder();
var dateTime = BindModelFromGet<UtcDateTimeModelBinder, DateTime>
("fred", "?fred=2019-08-12 00:00:00Z", bar);
Assert.That(dateTime.Kind, Is.EqualTo(DateTimeKind.Utc));
}
Things to note:
Additionally, if you are interested in testing error conditions (you should be), change the last line of his function to:
throw new Exception(bindingContext.ModelState[modelName].Errors[0].ErrorMessage);
and test like this:
[Test]
public void should_handle_bad_dates()
{
var bar = new UtcDateTimeModelBinder();
var ex = Assert.Throws<Exception>(() => BindModelFromGet<UtcDateTimeModelBinder, DateTime>
("fred", "?fred=NotADate", bar));
Assert.That(ex.Message, Is.EqualTo("Cannot convert value to Utc DateTime"));
}
That will handle simple validation problems, but do note that if the binder adds multiple errors or generally doesn't work like the simple date converter, you'll need to do more work.
Upvotes: 2
Reputation: 20987
It doesn't make to test model binders with out using the default value providers. So I wrote my bind model based on the expected rule. In this case I only needed to test gets
public TModel BindModelFromGet<TBinder, TModel>(string modelName, string queryString, TBinder binder)
where TBinder : IModelBinder
{
var httpControllerContext = new HttpControllerContext();
httpControllerContext.Request = new HttpRequestMessage(HttpMethod.Get, MOCK_URL + queryString);
var bindingContext = new ModelBindingContext();
var dataProvider = new DataAnnotationsModelMetadataProvider();
var modelMetadata = dataProvider.GetMetadataForType(null, typeof(TModel));
var httpActionContext = new HttpActionContext();
httpActionContext.ControllerContext = httpControllerContext;
var provider = new QueryStringValueProvider(httpActionContext, CultureInfo.InvariantCulture);
bindingContext.ModelMetadata = modelMetadata;
bindingContext.ValueProvider = provider;
bindingContext.ModelName = modelName;
if (binder.BindModel(httpActionContext, bindingContext))
{
return (TModel)bindingContext.Model;
}
throw new Exception("Model was not bindable");
}
If you want this to work for post you take in a string of jsonValues modify the httpControllerContext like so:
httpControllerContext.Request = new HttpRequestMessage(HttpMethod.Post, "");
httpControllerContext.Request.Content = new ObjectContent<object>(jsonValues, new JsonMediaTypeFormatter());
then you just need to use the proper ValueProvider (I didn't do the research on how since it's unneeded for me).
Upvotes: 2