| File: ModelBinding\Binders\HeaderModelBinder.cs | Web Access |
| Project: src\aspnetcore\src\Mvc\Mvc.Core\src\Microsoft.AspNetCore.Mvc.Core.csproj (Microsoft.AspNetCore.Mvc.Core) |
// 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.Diagnostics; using System.Globalization; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; 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> /// Initializes a new instance of <see cref="HeaderModelBinder"/>. /// </summary> /// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param> public HeaderModelBinder(ILoggerFactory loggerFactory) { _logger = loggerFactory.CreateLogger(typeof(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) { ArgumentNullException.ThrowIfNull(loggerFactory); ArgumentNullException.ThrowIfNull(innerModelBinder); _logger = loggerFactory.CreateLogger(typeof(HeaderModelBinder)); InnerModelBinder = innerModelBinder; } // to enable unit testing internal IModelBinder? InnerModelBinder { get; } /// <inheritdoc /> public async Task BindModelAsync(ModelBindingContext bindingContext) { ArgumentNullException.ThrowIfNull(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 static 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.TryGetValue(headerName, out var header)) { if (bindingContext.ModelMetadata.IsEnumerableType) { values = request.Headers.GetCommaSeparatedValues(headerName); } else { values = new[] { header.ToString() }; } } 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 = value.ToString(); } 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 sealed 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); } } } }