This repository has been archived by the owner on Dec 14, 2018. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 2.1k
/
HeaderModelBinder.cs
242 lines (206 loc) · 9.3 KB
/
HeaderModelBinder.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Diagnostics;
using System.Globalization;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding.Internal;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{
/// <summary>
/// An <see cref="IModelBinder"/> which binds models from the request headers when a model
/// has the binding source <see cref="BindingSource.Header"/>.
/// </summary>
public class HeaderModelBinder : IModelBinder
{
private readonly ILogger _logger;
/// <summary>
/// <para>This constructor is obsolete and will be removed in a future version. The recommended alternative
/// is the overload that takes an <see cref="ILoggerFactory"/> and an <see cref="IModelBinder"/>.</para>
/// <para>Initializes a new instance of <see cref="HeaderModelBinder"/>.</para>
/// </summary>
[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()
: this(NullLoggerFactory.Instance)
{
}
/// <summary>
/// Initializes a new instance of <see cref="HeaderModelBinder"/>.
/// </summary>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
public HeaderModelBinder(ILoggerFactory loggerFactory)
{
_logger = loggerFactory.CreateLogger<HeaderModelBinder>();
}
/// <summary>
/// Initializes a new instance of <see cref="HeaderModelBinder"/>.
/// </summary>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
/// <param name="innerModelBinder">The <see cref="IModelBinder"/> which does the actual
/// binding of values.</param>
public HeaderModelBinder(ILoggerFactory loggerFactory, IModelBinder innerModelBinder)
{
if (loggerFactory == null)
{
throw new ArgumentNullException(nameof(loggerFactory));
}
if (innerModelBinder == null)
{
throw new ArgumentNullException(nameof(innerModelBinder));
}
_logger = loggerFactory.CreateLogger<HeaderModelBinder>();
InnerModelBinder = innerModelBinder;
}
// to enable unit testing
internal IModelBinder InnerModelBinder { get; }
/// <inheritdoc />
public async Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
{
throw new ArgumentNullException(nameof(bindingContext));
}
_logger.AttemptingToBindModel(bindingContext);
// Property name can be null if the model metadata represents a type (rather than a property or parameter).
var headerName = bindingContext.FieldName;
// 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.
var request = bindingContext.HttpContext.Request;
if (!request.Headers.ContainsKey(headerName))
{
_logger.FoundNoValueInRequest(bindingContext);
}
if (InnerModelBinder == null)
{
BindWithoutInnerBinder(bindingContext);
return;
}
var headerValueProvider = GetHeaderValueProvider(headerName, bindingContext);
// Capture the top level object here as entering nested scope would make it 'false'.
var isTopLevelObject = bindingContext.IsTopLevelObject;
// Create a new binding scope in order to supply the HeaderValueProvider so that the binders like
// SimpleTypeModelBinder can find values from header.
ModelBindingResult result;
using (bindingContext.EnterNestedScope(
bindingContext.ModelMetadata,
fieldName: bindingContext.FieldName,
modelName: bindingContext.ModelName,
model: bindingContext.Model))
{
bindingContext.IsTopLevelObject = isTopLevelObject;
bindingContext.ValueProvider = headerValueProvider;
await InnerModelBinder.BindModelAsync(bindingContext);
result = bindingContext.Result;
}
bindingContext.Result = result;
_logger.DoneAttemptingToBindModel(bindingContext);
}
private HeaderValueProvider GetHeaderValueProvider(string headerName, ModelBindingContext bindingContext)
{
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.
var values = Array.Empty<string>();
if (request.Headers.ContainsKey(headerName))
{
if (bindingContext.ModelMetadata.IsEnumerableType)
{
values = request.Headers.GetCommaSeparatedValues(headerName);
}
else
{
values = new[] { (string)request.Headers[headerName] };
}
}
return new HeaderValueProvider(values);
}
private void BindWithoutInnerBinder(ModelBindingContext bindingContext)
{
var headerName = bindingContext.FieldName;
var request = bindingContext.HttpContext.Request;
object model;
if (bindingContext.ModelType == typeof(string))
{
var value = request.Headers[headerName];
model = (string)value;
}
else if (ModelBindingHelper.CanGetCompatibleCollection<string>(bindingContext))
{
var values = request.Headers.GetCommaSeparatedValues(headerName);
model = GetCompatibleCollection(bindingContext, values);
}
else
{
// An unsupported datatype or a new collection is needed (perhaps because target type is an array) but
// can't assign it to the property.
model = null;
}
if (model == null)
{
// Silently fail if unable to create an instance or use the current instance. Also reach here in the
// typeof(string) case if the header does not exist in the request and in the
// typeof(IEnumerable<string>) case if the header does not exist and this is not a top-level object.
bindingContext.Result = ModelBindingResult.Failed();
}
else
{
bindingContext.ModelState.SetModelValue(
bindingContext.ModelName,
request.Headers.GetCommaSeparatedValues(headerName),
request.Headers[headerName]);
bindingContext.Result = ModelBindingResult.Success(model);
}
_logger.DoneAttemptingToBindModel(bindingContext);
}
private static object GetCompatibleCollection(ModelBindingContext bindingContext, string[] values)
{
// Almost-always success if IsTopLevelObject.
if (!bindingContext.IsTopLevelObject && values.Length == 0)
{
return null;
}
if (bindingContext.ModelType.IsAssignableFrom(typeof(string[])))
{
// Array we already have is compatible.
return values;
}
var collection = ModelBindingHelper.GetCompatibleCollection<string>(bindingContext, values.Length);
for (var i = 0; i < values.Length; i++)
{
collection.Add(values[i]);
}
return collection;
}
private class HeaderValueProvider : IValueProvider
{
private readonly string[] _values;
public HeaderValueProvider(string[] values)
{
Debug.Assert(values != null);
_values = values;
}
public bool ContainsPrefix(string prefix)
{
return _values.Length != 0;
}
public ValueProviderResult GetValue(string key)
{
if (_values.Length == 0)
{
return ValueProviderResult.None;
}
else
{
return new ValueProviderResult(_values, CultureInfo.InvariantCulture);
}
}
}
}
}