-
Notifications
You must be signed in to change notification settings - Fork 9.9k
/
ModelBinderFactory.cs
328 lines (272 loc) · 12.2 KB
/
ModelBinderFactory.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
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#nullable enable
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.CompilerServices;
using Microsoft.AspNetCore.Mvc.Core;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Mvc.ModelBinding;
/// <summary>
/// A factory for <see cref="IModelBinder"/> instances.
/// </summary>
public partial class ModelBinderFactory : IModelBinderFactory
{
private readonly IModelMetadataProvider _metadataProvider;
private readonly IModelBinderProvider[] _providers;
private readonly ConcurrentDictionary<Key, IModelBinder> _cache;
private readonly IServiceProvider _serviceProvider;
/// <summary>
/// Creates a new <see cref="ModelBinderFactory"/>.
/// </summary>
/// <param name="metadataProvider">The <see cref="IModelMetadataProvider"/>.</param>
/// <param name="options">The <see cref="IOptions{TOptions}"/> for <see cref="MvcOptions"/>.</param>
/// <param name="serviceProvider">The <see cref="IServiceProvider"/>.</param>
public ModelBinderFactory(
IModelMetadataProvider metadataProvider,
IOptions<MvcOptions> options,
IServiceProvider serviceProvider)
{
_metadataProvider = metadataProvider;
_providers = options.Value.ModelBinderProviders.ToArray();
_serviceProvider = serviceProvider;
_cache = new ConcurrentDictionary<Key, IModelBinder>();
var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
var logger = loggerFactory.CreateLogger<ModelBinderFactory>();
Log.RegisteredModelBinderProviders(logger, _providers);
}
/// <inheritdoc />
public IModelBinder CreateBinder(ModelBinderFactoryContext context)
{
ArgumentNullException.ThrowIfNull(context);
if (_providers.Length == 0)
{
throw new InvalidOperationException(Resources.FormatModelBinderProvidersAreRequired(
typeof(MvcOptions).FullName,
nameof(MvcOptions.ModelBinderProviders),
typeof(IModelBinderProvider).FullName));
}
if (TryGetCachedBinder(context.Metadata, context.CacheToken, out var binder))
{
return binder;
}
// Perf: We're calling the Uncached version of the API here so we can:
// 1. avoid allocating a context when the value is already cached
// 2. avoid checking the cache twice when the value is not cached
var providerContext = new DefaultModelBinderProviderContext(this, context);
binder = CreateBinderCoreUncached(providerContext, context.CacheToken);
if (binder == null)
{
var message = Resources.FormatCouldNotCreateIModelBinder(providerContext.Metadata.ModelType);
throw new InvalidOperationException(message);
}
Debug.Assert(!(binder is PlaceholderBinder));
AddToCache(context.Metadata, context.CacheToken, binder);
return binder;
}
// Called by the DefaultModelBinderProviderContext when we're recursively creating a binder
// so that all intermediate results can be cached.
private IModelBinder CreateBinderCoreCached(DefaultModelBinderProviderContext providerContext, object? token)
{
if (TryGetCachedBinder(providerContext.Metadata, token, out var binder))
{
return binder;
}
// We're definitely creating a binder for an non-root node here, so it's OK for binder creation
// to fail.
binder = CreateBinderCoreUncached(providerContext, token) ?? NoOpBinder.Instance;
if (!(binder is PlaceholderBinder))
{
AddToCache(providerContext.Metadata, token, binder);
}
return binder;
}
private IModelBinder? CreateBinderCoreUncached(DefaultModelBinderProviderContext providerContext, object? token)
{
if (!providerContext.Metadata.IsBindingAllowed)
{
return NoOpBinder.Instance;
}
// A non-null token will usually be passed in at the top level (ParameterDescriptor likely).
// This prevents us from treating a parameter the same as a collection-element - which could
// happen looking at just model metadata.
var key = new Key(providerContext.Metadata, token);
// The providerContext.Visited is used here to break cycles in recursion. We need a separate
// per-operation cache for cycle breaking because the global cache (_cache) needs to always stay
// in a valid state.
//
// We store null as a sentinel inside the providerContext.Visited to track the fact that we've visited
// a given node but haven't yet created a binder for it. We don't want to eagerly create a
// PlaceholderBinder because that would result in lots of unnecessary indirection and allocations.
var visited = providerContext.Visited;
if (visited.TryGetValue(key, out var binder))
{
if (binder != null)
{
return binder;
}
// If we're currently recursively building a binder for this type, just return
// a PlaceholderBinder. We'll fix it up later to point to the 'real' binder
// when the stack unwinds.
binder = new PlaceholderBinder();
visited[key] = binder;
return binder;
}
// OK this isn't a recursive case (yet) so add an entry and then ask the providers
// to create the binder.
visited.Add(key, null);
IModelBinder? result = null;
for (var i = 0; i < _providers.Length; i++)
{
var provider = _providers[i];
result = provider.GetBinder(providerContext);
if (result != null)
{
break;
}
}
// If the PlaceholderBinder was created, then it means we recursed. Hook it up to the 'real' binder.
if (visited[key] is PlaceholderBinder placeholderBinder)
{
// It's also possible that user code called into `CreateBinder` but then returned null, we don't
// want to create something that will null-ref later so just hook this up to the no-op binder.
placeholderBinder.Inner = result ?? NoOpBinder.Instance;
}
if (result != null)
{
visited[key] = result;
}
return result;
}
private void AddToCache(ModelMetadata metadata, object? cacheToken, IModelBinder binder)
{
Debug.Assert(metadata != null);
Debug.Assert(binder != null);
if (cacheToken == null)
{
return;
}
_cache.TryAdd(new Key(metadata, cacheToken), binder);
}
private bool TryGetCachedBinder(ModelMetadata metadata, object? cacheToken, [NotNullWhen(true)] out IModelBinder? binder)
{
Debug.Assert(metadata != null);
if (cacheToken == null)
{
binder = null;
return false;
}
return _cache.TryGetValue(new Key(metadata, cacheToken), out binder);
}
private sealed class DefaultModelBinderProviderContext : ModelBinderProviderContext
{
private readonly ModelBinderFactory _factory;
public DefaultModelBinderProviderContext(
ModelBinderFactory factory,
ModelBinderFactoryContext factoryContext)
{
_factory = factory;
Metadata = factoryContext.Metadata;
BindingInfo bindingInfo;
if (factoryContext.BindingInfo != null)
{
bindingInfo = new BindingInfo(factoryContext.BindingInfo);
}
else
{
bindingInfo = new BindingInfo();
}
bindingInfo.TryApplyBindingInfo(Metadata);
BindingInfo = bindingInfo;
MetadataProvider = _factory._metadataProvider;
Visited = new Dictionary<Key, IModelBinder?>();
}
private DefaultModelBinderProviderContext(
DefaultModelBinderProviderContext parent,
ModelMetadata metadata,
BindingInfo bindingInfo)
{
Metadata = metadata;
_factory = parent._factory;
MetadataProvider = parent.MetadataProvider;
Visited = parent.Visited;
BindingInfo = bindingInfo;
}
public override BindingInfo BindingInfo { get; }
public override ModelMetadata Metadata { get; }
public override IModelMetadataProvider MetadataProvider { get; }
public Dictionary<Key, IModelBinder?> Visited { get; }
public override IServiceProvider Services => _factory._serviceProvider;
public override IModelBinder CreateBinder(ModelMetadata metadata)
{
var bindingInfo = new BindingInfo();
bindingInfo.TryApplyBindingInfo(metadata);
return CreateBinder(metadata, bindingInfo);
}
public override IModelBinder CreateBinder(ModelMetadata metadata, BindingInfo bindingInfo)
{
ArgumentNullException.ThrowIfNull(metadata);
ArgumentNullException.ThrowIfNull(bindingInfo);
// For non-root nodes we use the ModelMetadata as the cache token. This ensures that all non-root
// nodes with the same metadata will have the same binder. This is OK because for an non-root
// node there's no opportunity to customize binding info like there is for a parameter.
var token = metadata;
var nestedContext = new DefaultModelBinderProviderContext(this, metadata, bindingInfo);
return _factory.CreateBinderCoreCached(nestedContext, token);
}
}
// This key allows you to specify a ModelMetadata which represents the type/property being bound
// and a 'token' which acts as an arbitrary discriminator.
//
// This is necessary because the same metadata might be bound as a top-level parameter (with BindingInfo on
// the ParameterDescriptor) or in a call to TryUpdateModel (no BindingInfo) or as a collection element.
//
// We need to be able to tell the difference between these things to avoid over-caching.
private readonly struct Key : IEquatable<Key>
{
private readonly ModelMetadata _metadata;
private readonly object? _token; // Explicitly using ReferenceEquality for tokens.
public Key(ModelMetadata metadata, object? token)
{
_metadata = metadata;
_token = token;
}
public bool Equals(Key other)
{
return _metadata.Equals(other._metadata) && object.ReferenceEquals(_token, other._token);
}
public override bool Equals(object? obj)
{
return obj is Key other && Equals(other);
}
public override int GetHashCode()
{
return HashCode.Combine(_metadata, RuntimeHelpers.GetHashCode(_token));
}
public override string ToString()
{
switch (_metadata.MetadataKind)
{
case ModelMetadataKind.Parameter:
return $"{_token} (Parameter: '{_metadata.ParameterName}' Type: '{_metadata.ModelType.Name}')";
case ModelMetadataKind.Property:
return $"{_token} (Property: '{_metadata.ContainerType!.Name}.{_metadata.PropertyName}' " +
$"Type: '{_metadata.ModelType.Name}')";
case ModelMetadataKind.Type:
return $"{_token} (Type: '{_metadata.ModelType.Name}')";
default:
return $"Unsupported MetadataKind '{_metadata.MetadataKind}'.";
}
}
}
private static partial class Log
{
[LoggerMessage(12, LogLevel.Debug, "Registered model binder providers, in the following order: {ModelBinderProviders}", EventName = "RegisteredModelBinderProviders")]
public static partial void RegisteredModelBinderProviders(ILogger logger, IModelBinderProvider[] modelBinderProviders);
}
}