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

Add support binding to types other than string or collection of string in HeaderModelBinder #7294

Merged

Conversation

kichalla
Copy link
Member

Related to #5859

/// <param name="innerModelBinder">The <see cref="IModelBinder"/> which does the actual
/// binding of values.</param>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
public HeaderModelBinder(ILoggerFactory loggerFactory, IModelBinder innerModelBinder)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: This HeaderModelBinder now delegates binding to the inner model binder. For users who upgrade to Preview2 and who use the HeaderModelBinderProvider as it is, then this constructor would be used and all is well, but if a user had been explicitly(the parameterless or the one taking in ILoggerFactory) using the HeaderModelBinder constructors, then the InnerModelBinder would be null breaking their application at runtime. We should have a backup (i.e use earlier design) in case the inner binder is null?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep. That's the non-breaking change way to do this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd try to keep 1-2 tests that use the old code path and make the majority of them go down the new code path.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good 👍

// SimpleTypeModelBinder adds a model error when binding to let's say and integer and the model is null.

// Explicitly pass in the header name as the key rather than taking in the model name to look for values
// as otherwise it would be breaking from earlier version where we didn't consider prefixes.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would explain this a bit differently. Using the ModelName directly rather than the model path (full name) isn't just a back-compat concern. It's always the way this is supposed to work. There was never a desire for model binding a header value to compose keys like we do for forms and query strings.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Explicitly pass in the header name

Where is the code that does this? I would expect to see this show up at L113 or so

// can't assign it to the property.
model = null;
}
bindingContext.ValueProvider = headerValueProvider;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you need to 'pop' the value provider or is it done for you?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's done automatically for us. I have a test for this case: https://github.com/aspnet/Mvc/pull/7294/files#diff-7e7d3101aa01d6dab9e9b49c4c3ddf51R124

return new HeaderModelBinder(loggerFactory);
}
else
// Change the binding info to prevent recursion
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is probably worth a bit more explanation given how long we discussed it today :)

{
logger.CannotCreateHeaderModelBinder(context.Metadata.ModelType);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we preserve this logging?

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Core;

namespace Microsoft.AspNetCore.Mvc.ModelBinding.Internal
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make this proper internal? I don't think we should add more stuff like this. It leaks out and causes confusion.

public bool ContainsPrefix(string prefix)
{
// In 2.0 version, HeaderModelBinder didn't consider prefix at all and doing now would break existing
// users, so ignore the key value totally and only rely on the field name
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not just for back compat. It works this way on purpose.

else
{
return new ValueProviderResult(values, _culture);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this code would be a lot simpler if we just passed in a headerName and a string[] values from the calling code. We already figured out whether or not we wanted to split csv headers in the calling code.

}

/// <inheritdoc />
public ValueProviderResult GetValue(string key)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where is key used?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We ignore this entirely on purpose (please check my reply comment in this file)

{
// In 2.0 version, HeaderModelBinder didn't consider prefix at all and doing now would break existing
// users, so ignore the key value totally and only rely on the field name
return _headers.ContainsKey(_headerFieldName);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should be setting up the model binding context such that key is going to be header name, and then checking that it's the appropriate value.

This implementation doesn't really conform to the expectations of the IValueProvider interface.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, you are right and that's how I had it in the beginning, but I changed it because the current HeaderModelBinder sets the key to be the ModelName instead of the header name and changing it might break existing users. So the idea, as you can imagine, is to send the inner binders the model name (so that they can set the model state key as before), but make the header value provider just ignore the full model path.


// Do not set ModelBindingResult to Failed on not finding the value as we want the inner modelbinder to
// do that. This would give a chance to the inner binder to add more useful information. For example,
// SimpleTypeModelBinder adds a model error when binding to let's say and integer and the model is null.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: "an integer"

@@ -490,7 +490,7 @@ static MvcCoreLoggerExtensions()
_cannotCreateHeaderModelBinder = LoggerMessage.Define<Type>(
LogLevel.Debug,
20,
"Could not create a binder for type '{ModelType}' as this binder only supports 'System.String' type or a collection of 'System.String'.");
"Could not create a binder for type '{ModelType}' as this binder only supports simple types (like string, int, bool, enum) or a collection of them.");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or a collection of them

or a collection of simple types

Copy link
Member

@dougbu dougbu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test changes look pretty straightforward. Anything in particular we need to be looking at?

{
if (bindingContext == null)
{
throw new ArgumentNullException(nameof(bindingContext));
}

var request = bindingContext.HttpContext.Request;
_logger.AttemptingToBindModel(bindingContext);

// Property name can be null if the model metadata represents a type (rather than a property or parameter).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds like a corner case where the new implementation may have changed the behaviour. Do we have tests of this scenario?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this actually about? Is it about decorating on a complex type? if yes, we do not support it and I have tests in this PR to check that.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🆗

/// Initializes a new instance of <see cref="HeaderModelBinder"/>.
/// </summary>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
[Obsolete("This constructor is obsolete and will be removed in a future version. The recommended alternative"
+ " is the overload that takes an " + nameof(ILoggerFactory) + " and an " + nameof(IModelBinder) + ".")]
public HeaderModelBinder(ILoggerFactory loggerFactory)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this constructor was added in Preview1. If that's the case, just delete it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point

/// <inheritdoc />
public Task BindModelAsync(ModelBindingContext bindingContext)
public async Task BindModelAsync(ModelBindingContext bindingContext)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No reason for an async state machine when doing the old thing. Suggest making this method non-async. Either call BindWithoutInnerBinder(...) and return Task.CompletedTask or a new method (that is async).

But, don't worry about it if others thing the "old thing" is an extreme corner case.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But, don't worry about it if others thing the "old thing" is an extreme corner case.

Exactly. It would be uncommon for users to have instantiated HMB by itself instead of going through HMBP. So, I will leave the code as it is.

// Do not set ModelBindingResult to Failed on not finding the value in the header as we want the inner
// modelbinder to do that. This would give a chance to the inner binder to add more useful information.
// For example, SimpleTypeModelBinder adds a model error when binding to let's say an integer and the
// model is null.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't this comment belong with the ContainsKey(...) check?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did it here because the typical pattern that i have seen in other binders is that they short-circuit immediately when a value is not provided from the value provider. So, I just kept it here for that reason in case anyone tries to modify this binder in future.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does the typical pattern have to do with the location of this comment? It's talking about the "not finding the value" case which is handled earlier. Move it to be right above the ContainsKey(...) check.

var request = bindingContext.HttpContext.Request;

// Prevent breaking existing users in scenarios where they are binding to a 'string' property
// and expect the whole comma separated string, if any, as a single string and not as a string array.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thought we had code that basically said "if mapping a value provider collection to a string, concatenate the collection's items"? If the difference is only between "X,Y,Z" and "X, Y, Z", we shouldn't bother with this extra bit.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have we considered using a compat switch to drive this new model binding behavior? We wouldn't need this check at this point

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thought we had code that basically said "if mapping a value provider collection to a string, concatenate the collection's items"? If the difference is only between "X,Y,Z" and "X, Y, Z", we shouldn't bother with this extra bit.

The difference is between "X,Y,Z" and "X" (i.e if we always get comma separated values, the STMB would only take the first value in the list of values to bind to a string)

Have we considered using a compat switch to drive this new model binding behavior? We wouldn't need this check at this point

Good point. @dougbu what do you think? If you agree, please give a name for the switch too :-)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dougbu I am interested in this switch because it would give a consistent experience (for example, if a query string had multiple values for a key, we only pick the first one).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. The "entire header string" behaviour is what we want. Leave it alone because headers aren't query string values.
  2. A switch to control whether or not the HMB uses the old code path despite a non-null inner binder might be interesting if anything behaved differently for the previously supported cases i.e. when binding string or collections of string. Is anything visible changing there? I'm hoping not; any visible behaviour change would be unintended.
  3. The other possibility would be to disable binding non-strings / non-string collections when a CompatibitySwitch is set. That might be useful because someone may have implemented a model binding provider that handles, say, ints when the BindingSource is Header. @Eilon should we add a switch for this fairly-extreme customization?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is anything visible changing there? I'm hoping not; any visible behaviour change would be unintended.

I have tests covering the old behavior(2.0) using a non-null InnerBinder. So we are fine.
https://github.com/aspnet/Mvc/pull/7294/files#diff-7e7d3101aa01d6dab9e9b49c4c3ddf51R132

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we think it's the right thing for new 2.1 apps to get a behavior that's different from 2.0, then, yeah, add a compat switch.


// Prevent breaking existing users in scenarios where they are binding to a 'string' property
// and expect the whole comma separated string, if any, as a single string and not as a string array.
string[] values;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May be a small perf win to avoid allocating this array when !request.Headers.ContainsKey(headerName). Just use Array.Empty<string>() in that case.

/// <inheritdoc />
public bool ContainsPrefix(string prefix)
{
return _isHeaderPresent;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is odd and _isHeaderPresent shouldn't be necessary. Just use _values.Length == 0 everywhere.

As-is, ContainsPrefix(...) will return false and GetValue(...) will return a one-element array containing null when assigning to a non-collection. The two methods should do sensible things and be consistent.


// ModelState
Assert.True(modelState.IsValid);
var entry = modelState["prefix.Manufacturer.NoCommaString"];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use helper methods and Assert.Collection(...) to shorten this and make it easier to read.

/// <returns>An <see cref="IModelBinder"/>.</returns>
public virtual IModelBinder CreateBinder(ModelMetadata metadata, BindingInfo bindingInfo)
{
throw new NotImplementedException();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NotSupportedException?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious, why?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to the documentation

In other words, a NotImplementedException exception should be synonymous with "still in development."

var request = bindingContext.HttpContext.Request;

// Prevent breaking existing users in scenarios where they are binding to a 'string' property
// and expect the whole comma separated string, if any, as a single string and not as a string array.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have we considered using a compat switch to drive this new model binding behavior? We wouldn't need this check at this point


// Create a new binding scope in order to supply the HeaderValueProvider so that the binders like
// SimpleTypeModelBinder can find values from header.
ModelBindingResult result;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we assign this inline?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move this and its comment right above the using

context.BindingInfo.BindingSource.CanAcceptDataFrom(BindingSource.Header))
var bindingInfo = context.BindingInfo;
if (bindingInfo.BindingSource == null
|| !bindingInfo.BindingSource.CanAcceptDataFrom(BindingSource.Header))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Formatting, || on the previous line

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought we prefer this one? @dougbu , I recall this feedback was from you

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I remember commenting about keeping : this( and : base( together and on a separate line. On the other hand, binary operators either place seems fair game to me. I might however have commented on consistency within a file or a PR.

Put another way, I don't have a dog in this hunt.

// Since we are delegating the binding of the current model type to other binders, modify the
// binding source of the current model type to a non-FromHeader binding source in order to avoid an
// infinite recursion into this binder provider.
var nonFromHeaderBindingInfo = new BindingInfo(bindingInfo);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nestedBindingInfo?

public void Create_WhenBindingSourceIsFromHeader_ReturnsBinder()
[Theory]
[InlineData(typeof(string))]
[InlineData(typeof(bool))]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't need to be exhaustive. A couple of data types should suffice. One numeric, one enum type, one DateTime(?) and a nullable struct

public Manufacturer Manufacturer { get; set; }
}

private class Manufacturer
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would something like this work:

[FromHeader]
public class RequestMetadata
{
     public string CorrelationId { get; set; }

     public string Date { get; set; }
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, this is not supported and we have tests in HeaderModelBinderProviderTest.cs where it doesn't give a binder as its not a simple type.

@kichalla kichalla changed the title [Design] Add support binding to types other than string or collection of string in HeaderModelBinder Add support binding to types other than string or collection of string in HeaderModelBinder Jan 26, 2018
Copy link
Member Author

@kichalla kichalla left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🆙 📅

{
if (bindingContext == null)
{
throw new ArgumentNullException(nameof(bindingContext));
}

var request = bindingContext.HttpContext.Request;
_logger.AttemptingToBindModel(bindingContext);

// Property name can be null if the model metadata represents a type (rather than a property or parameter).
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this actually about? Is it about decorating on a complex type? if yes, we do not support it and I have tests in this PR to check that.

var request = bindingContext.HttpContext.Request;

// Prevent breaking existing users in scenarios where they are binding to a 'string' property
// and expect the whole comma separated string, if any, as a single string and not as a string array.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dougbu I am interested in this switch because it would give a consistent experience (for example, if a query string had multiple values for a key, we only pick the first one).

}

/// <summary>
/// Initializes a new instance of <see cref="HeaderModelBinder"/>.
/// </summary>
/// <param name="innerModelBinder">The <see cref="IModelBinder"/> which does the actual
/// binding of values.</param>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Order these to match the parameters.


// Create a new binding scope in order to supply the HeaderValueProvider so that the binders like
// SimpleTypeModelBinder can find values from header.
ModelBindingResult result;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move this and its comment right above the using

{
if (bindingContext == null)
{
throw new ArgumentNullException(nameof(bindingContext));
}

var request = bindingContext.HttpContext.Request;
_logger.AttemptingToBindModel(bindingContext);

// Property name can be null if the model metadata represents a type (rather than a property or parameter).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🆗

// binding source of the current model type to a non-FromHeader binding source in order to avoid an
// infinite recursion into this binder provider.
var nestedBindingInfo = new BindingInfo(bindingInfo);
nestedBindingInfo.BindingSource = BindingSource.ModelBinding;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inline this line i.e. use

{
    BindingSource = BindingSource.ModelBinding,
}


namespace Microsoft.AspNetCore.Mvc.ModelBinding.Internal
{
internal class HeaderValueProvider : IValueProvider
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this not private to the HeaderModelBinder? You're not testing this class directly.


private class Manufacturer
{
[FromHeader]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Override some of the field names using [FromHeader("Different name")]

@@ -2701,7 +2701,7 @@ private class LocationInfo

entry = Assert.Single(modelState, e => e.Key == "Info.Value.GpsCoordinates").Value;
Assert.Equal("10,20", entry.AttemptedValue);
Assert.Equal(new[] { "10", "20" }, entry.RawValue);
Assert.Equal("10,20", entry.RawValue);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wait. This implies something is changing that shouldn't be. I'm not that worried about RawValue per se. But, the "are we assigning to a non-collection" checks were intended to keep the behaviour as close as possible to the old ways. Why is this acting differently?

Copy link
Member Author

@kichalla kichalla Jan 27, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is because HMB used to do this earlier https://github.com/aspnet/Mvc/blob/dev/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/HeaderModelBinder.cs#L97 and in the new design individual binder set the result

@kichalla kichalla force-pushed the kichalla/headermodelbinder-change-modelbindingcontext-scope branch from 9a7d54c to a316ee9 Compare January 27, 2018 00:01
@kichalla
Copy link
Member Author

Updated

@kichalla
Copy link
Member Author

🆙 📅

Copy link
Member

@dougbu dougbu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only a few smallish comments…
Also get @pranavkm's opinions.

}
}

return new HeaderValueProvider(CultureInfo.InvariantCulture, values);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't pass in the culture or store it in a field. Use CultureInfo.InvariantCulture when creating the ValueProviderResult.

@@ -487,11 +488,16 @@ static MvcCoreLoggerExtensions()
19,
"Could not bind to model with name '{ModelName}' and type '{ModelType}' as the request did not have a content type of either 'application/x-www-form-urlencoded' or 'multipart/form-data'.");

_cannotCreateHeaderModelBinder = LoggerMessage.Define<Type>(
_cannotCreateHeaderModelBinderCompatVersion_2_0 = LoggerMessage.Define<Type>(
LogLevel.Debug,
20,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This id should be higher than any other value in this file.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am using the same Id because this log message is a temporary one and would be removed in 3.0 version. Also it is mutually exclusive with the other one in this PR. So we should be fine?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what the issue is here - log event ids only need to be unique within a category name. It's always confusing no matter what, though.

}

/// <summary>
/// Initializes a new instance of <see cref="HeaderModelBinder"/>.
/// </summary>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Restore this line.

public HeaderModelBinder()
: this(NullLoggerFactory.Instance)
{
_logger = NullLogger.Instance;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this line, it's redundant.

@@ -77,7 +89,7 @@ public static class ModelBindingTestHelper

return new ParameterBinder(
metadataProvider,
GetModelBinderFactory(metadataProvider, options),
new ModelBinderFactory(metadataProvider, options, services),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check if GetModelBinderFactory(...) is still used. Remove it if not.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, its used in ParameterBinderExtensions.cs file

Copy link
Member Author

@kichalla kichalla left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🆙 📅

@@ -487,11 +488,16 @@ static MvcCoreLoggerExtensions()
19,
"Could not bind to model with name '{ModelName}' and type '{ModelType}' as the request did not have a content type of either 'application/x-www-form-urlencoded' or 'multipart/form-data'.");

_cannotCreateHeaderModelBinder = LoggerMessage.Define<Type>(
_cannotCreateHeaderModelBinderCompatVersion_2_0 = LoggerMessage.Define<Type>(
LogLevel.Debug,
20,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am using the same Id because this log message is a temporary one and would be removed in 3.0 version. Also it is mutually exclusive with the other one in this PR. So we should be fine?

@@ -77,7 +89,7 @@ public static class ModelBindingTestHelper

return new ParameterBinder(
metadataProvider,
GetModelBinderFactory(metadataProvider, options),
new ModelBinderFactory(metadataProvider, options, services),
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, its used in ParameterBinderExtensions.cs file

Copy link
Contributor

@pranavkm pranavkm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me. Please get sign off from @rynowak \ @dougbu before checking this in.

LogLevel.Debug,
20,
"Could not create a binder for type '{ModelType}' as this binder only supports 'System.String' type or a collection of 'System.String'.");

_cannotCreateHeaderModelBinder = LoggerMessage.Define<Type>(
LogLevel.Debug,
20,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this have a different event id? 20's already used no?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had commented on this here https://github.com/aspnet/Mvc/pull/7294/files#r164833556 . Let me know if you agree.

@@ -36,35 +37,117 @@ public HeaderModelBinder()
/// </summary>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
public HeaderModelBinder(ILoggerFactory loggerFactory)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this was newly added for preview1, should we remove this one entirely? Presumably we want users to use the new behavior going forward.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This constructor would be used when users are on 2.0 compat mode. We could have not added this constructor but kept it as they could see logging from the binder.

}

// to enable unit testing
internal IModelBinder InnerModelBinder { get; }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be fine to make this public. Not much of an implementation detail.


var headerValueProvider = GetHeaderValueProvider(headerName, bindingContext);

// Capture the top level object here as entering nested scope would make it 'false' always.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would make it 'false' always


public HeaderValueProvider(string[] values)
{
if (values == null)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could just Debug.Assert this. We're the only ones calling this ctor.

// at runtime due to collections we can't modify.
if (context.Metadata.ModelType == typeof(string) ||
context.Metadata.ElementType == typeof(string))
var options = context.Services.GetRequiredService<IOptions<MvcOptions>>().Value;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we cache the instance of MvcOptions once we've read it?

{
get
{
var guid = Guid.NewGuid();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you use fixed Guid here? Having varying data throws off our CI's test collection (since it gets reported as a different test name for each run

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes! Good catch

Copy link
Member

@dougbu dougbu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make the log ids unique. Then, this looks done.

@kichalla kichalla force-pushed the kichalla/headermodelbinder-change-modelbindingcontext-scope branch from 8f896e2 to 0215740 Compare January 30, 2018 19:47
@kichalla kichalla merged commit 0215740 into dev Jan 30, 2018
@dougbu dougbu deleted the kichalla/headermodelbinder-change-modelbindingcontext-scope branch July 15, 2018 03:44
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants