.Net Core Api:在来自不同来源的请求中获取数据

.Net Core具有内置的模型绑定机制,该机制不仅允许接受控制器中的输入参数,而且还可以立即接收具有填充字段的对象。这使您可以使用模型验证将所有必要的检查嵌入到这样的对象中。

这些只是API运作所需的数据,而不仅仅是从Query或Body获得的。需要从Headers接收一些数据(在我的情况下,base64中存在json),如果您使用REST,则某些数据应该来自外部服务或ActionRoute。您可以使用绑定从那里获取数据。没错,这里有一个问题:如果您决定不破坏封装并通过构造函数初始化模型,那么您将不得不进行萨满。

对于我自己和子孙后代,我决定写一些类似的说明,以结合使用Binding和萨满教。

问题


典型的控制器如下所示:

[HttpGet]
public async Task<IActionResult> GetSomeData([FromQuery[IncomeData someData)
{
    var moreData = GetFromHeaderAndDecode("X-Property");
    if (moreData.Id == 0)
    {
        return StatusCode(400, "Nginx doesnt know your id");
    }
    var externalData = GetFromExternalService("http://myservice.com/MoreData");
    if (externalData == null)
    {
        return StatusCode(500, "Cant connect to external service");
    }
    var finalData = new FinalData(someData, moreData, externalData);
    return _myService.Handle(finalData);
}

结果,我们遇到以下问题:

  1. 验证逻辑由请求对象,标头中的请求方法,服务中的请求方法和控制器方法涂抹。为确保绝对必要的检查,您需要进行整个调查!
  2. 相邻的控制器方法将具有完全相同的代码。攻击中的复制粘贴编程。
  3. 通常,比示例中的检查要多得多,因此,唯一重要的一行(对业务逻辑处理方法的调用)隐藏在代码堆中。要见到他并了解这里发生的情况,通常需要付出一些努力。

自定义绑定(简易模式)


通过在请求处理管道中实现您的处理程序,可以解决部分问题。为此,首先通过将最终对象立即传递给方法来更正我们的控制器。看起来好多了吧?

[HttpGet]
public async Task<IActionResult> GetSomeData([FromQuery]FinalData finalData)
{
    return _myService.Handle(finalData);
}

接下来,为类型MoreData创建活页夹。

public class MoreDataBinder : IModelBinder
{
    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        var moreData = GetFromHeaderAndDecode(bindingContext.HttpContext.Request.Headers);
        if (moreData != null)
        {
            bindingContext.Result = ModelBindingResult.Success(moreData);
        }
        return Task.CompletedTask;
    }
    private MoreData GetFromHeaderAndDecode(IHeaderDictionary headers) { ... }
}

最后,我们通过将活页夹绑定添加到该属性来修复FinalData模型:

public class FinalData
{
    public int SomeDataNumber { get; set; }

    public string SomeDataText { get; set; }

    [ModelBinder(BinderType = typeof(MoreDataBinder))]
    public MoreData MoreData { get; set; }
}

它已经好了,但是痔疮增加了:现在您需要知道我们有一个特殊的处理器,并在所有型号中进行注明。但这是可以解决的。

创建您的BinderProvider:

public class MoreDataBinderProvider : IModelBinderProvider
{
    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        var modelType = context.Metadata.UnderlyingOrModelType;
        if (modelType == typeof(MoreData))
        {
            return new BinderTypeModelBinder(typeof(MoreDataBinder));
        }
        return null;
    }
}

并在启动中注册:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();
    services.AddMvc(options =>
    {
        options.ModelBinderProviders.Insert(0, new MoreDataBinderProvider());
     });
}

将按队列顺序为每个模型对象调用提供程序。如果我们的提供者符合所需的类型,它将返回所需的活页夹。如果没有,则默认活页夹将起作用。因此,现在无论何时指定MoreData的类型,都将从Header中获取并解码它,并且无需在模型中指定特殊属性。

自定义绑定(硬模式)


所有这一切都很棒,但是还有一件事:为了使魔术发挥作用,我们的模型必须具有设置的公共属性。但是封装呢?如果我想将请求数据传输到云中的各个地方并且知道在那里将不会更改该怎么办?

问题是默认绑定程序不适用于没有默认构造函数的模型。但是是什么阻止我们编写自己的呢?
我为其编写此代码的服务不使用REST,仅通过Query和Body传输参数,并且仅使用两种类型的请求-Get
和Post。因此,在REST API的情况下,处理逻辑将略有不同。
通常,代码将保持不变,只有我们的活页夹需要改进,以便它创建一个对象并填充其私有字段。此外,我将给出一些带有注释的代码,这些代码不感兴趣-在文章的最后,该类的整个清单。

首先,让我们确定MoreData是否是该类的唯一属性。如果是,那么您需要自己创建对象(hello,Activator),否则,JsonConvert可以完美地完成工作,而我们只需将必要的数据放入属性中即可。

private static bool NeedActivator(IReflect modelType)
{
    var propFlags = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance;
    var properties = modelType.GetProperties(propFlags);

    return properties.Select(p => p.Name).Distinct().Count() == 1;
}

通过JsonConvert创建对象很简单,对于使用Body进行查询:

private static object? GetModelFromBody(ModelBindingContext bindingContext, Type modelType)
{
    using var reader = new StreamReader(bindingContext.HttpContext.Request.Body);
    var jsonString = reader.ReadToEnd();
    var data = JsonConvert.DeserializeObject(jsonString, modelType);
    return data;
}

但是使用查询我不得不放下。如果有人可以提出更美丽的解决方案,我将感到非常高兴。

传递数组时,将获得多个具有相同名称的参数。强制转换为平面类型会有所帮助,但是序列化会在数组[]上加上多余的引号,必须手动将其删除。

private static object? GetModelFromQuery(ModelBindingContext bindingContext, Type modelType)
{
    var valuesDictionary = QueryHelpers.ParseQuery(bindingContext.HttpContext.Request.QueryString.Value);
    var jsonDictionary = valuesDictionary.ToDictionary(pair => pair.Key, pair => pair.Value.Count < 2 ? pair.Value.ToString() : $"[{pair.Value}]");

    var jsonStr = JsonConvert.SerializeObject(jsonDictionary).Replace("\"[", "[").Replace("]\"", "]");
    var data = JsonConvert.DeserializeObject(jsonStr, modelType);
    return data;
}

最后,创建对象时,有必要将我们的数据写入其私有属性。我在本文开头谈到的正是这种萨满教。我在这里找到了这个解决方案,对此非常感谢作者。

private void ForceSetValue(PropertyInfo propertyInfo, object obj, object value)
{
    var propName = $"<{propertyInfo.Name}>k__BackingField";
    var propFlags = BindingFlags.Instance | BindingFlags.NonPublic;
            
    obj.GetType().GetField(propName, propFlags)?.SetValue(obj, value);
}

好吧,我们将这些方法合并在一个调用中:

public Task BindModelAsync(ModelBindingContext bindingContext)
{
    var moreData = GetFromHeaderAndDecode(bindingContext.HttpContext.Request.Headers);
    if (moreData == null)
    {
        return Task.CompletedTask;
    }

    var modelType = bindingContext.ModelMetadata.UnderlyingOrModelType;
    if (NeedActivator(modelType))
    {
        var data = Activator.CreateInstance(modelType, moreData);
        bindingContext.Result = ModelBindingResult.Success(data);

        return Task.CompletedTask;
    }

    var model = bindingContext.HttpContext.Request.Method == "GET"
                            ? GetModelFromQuery(bindingContext, modelType)
                            : GetModelFromBody(bindingContext, modelType);

    if (model is null)
    {
        throw new Exception("  ");
    }

    var ignoreCase = StringComparison.InvariantCultureIgnoreCase;
    var dataProperty = modelType.GetProperties()
                            .FirstOrDefault(p => p.Name.Equals(typeof(T).Name, ignoreCase));

    if (dataProperty != null)
    {
        ForceSetValue(dataProperty, model, moreData);
    }

    bindingContext.Result = ModelBindingResult.Success(model);
    return Task.CompletedTask;
}

仍然需要修复BinderProvider,以便它以所需的属性响应任何类:

public class MoreDataBinderProvider : IModelBinderProvider
{
    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
       var modelType = context.Metadata.UnderlyingOrModelType;
       if (HasDataProperty(modelType))
       {
           return new BinderTypeModelBinder(typeof(PrivateDataBinder<MoreData>));
       }
       return null;
    }
    private bool HasDataProperty(IReflect modelType)
    {
        var propFlags = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance;
        var properties = modelType.GetProperties(propFlags);

        return properties.Select(p => p.Name) .Contains(nameof(MoreData));
    }
}


就这样。事实证明,活页夹比简单模式下要复杂一些,但是现在我们可以在所有控制器的所有方法中绑定“外部”属性,而无需付出额外的努力。缺点:

  1. 具有私有字段的对象的构造函数必须指定[JsonConstrustor]属性。但这完全符合模型的逻辑,并且不会干扰模型的感知。
  2. 在某个地方,您可能需要从头文件中获取MoreData。但这通过创建一个单独的类来处理。
  3. 团队的其他成员必须意识到魔术的存在。但是文档将挽救人性。

生成的活页夹的完整列表在这里:

PrivateMoreDataBinder.cs
public class PrivateDataBinder<T> : IModelBinder
    {
        /// <summary></summary>
        /// <param name="bindingContext"></param>
        public Task BindModelAsync(ModelBindingContext bindingContext)
        {
            var moreData = GetFromHeaderAndDecode(bindingContext.HttpContext.Request.Headers);
            if (moreData == null)
            {
                return Task.CompletedTask;
            }

            var modelType = bindingContext.ModelMetadata.UnderlyingOrModelType;
            if (NeedActivator(modelType))
            {
                var data = Activator.CreateInstance(modelType, moreData);
                bindingContext.Result = ModelBindingResult.Success(data);

                return Task.CompletedTask;
            }

            var model = bindingContext.HttpContext.Request.Method == "GET"
                            ? GetModelFromQuery(bindingContext, modelType)
                            : GetModelFromBody(bindingContext, modelType);

            if (model is null)
            {
                throw new Exception("  ");
            }

            var ignoreCase = StringComparison.InvariantCultureIgnoreCase;
            var dataProperty = modelType.GetProperties()
                                        .FirstOrDefault(p => p.Name.Equals(typeof(T).Name, ignoreCase));

            if (dataProperty != null)
            {
                ForceSetValue(dataProperty, model, moreData);
            }

            bindingContext.Result = ModelBindingResult.Success(model);

            return Task.CompletedTask;
        }

        private static object? GetModelFromQuery(ModelBindingContext bindingContext,
                                                 Type modelType)
        {
            var valuesDictionary = QueryHelpers.ParseQuery(bindingContext.HttpContext.Request.QueryString.Value);

            var jsonDictionary = valuesDictionary.ToDictionary(pair => pair.Key, pair => pair.Value.Count < 2 ? pair.Value.ToString() : $"[{pair.Value}]");

            var jsonStr = JsonConvert.SerializeObject(jsonDictionary)
                                     .Replace("\"[", "[")
                                     .Replace("]\"", "]");

            var data = JsonConvert.DeserializeObject(jsonStr, modelType);

            return data;
        }

        private static object? GetModelFromBody(ModelBindingContext bindingContext,
                                                Type modelType)
        {
            using var reader = new StreamReader(bindingContext.HttpContext.Request.Body);
            var jsonString = reader.ReadToEnd();
            var data = JsonConvert.DeserializeObject(jsonString, modelType);

            return data;
        }

        private static bool NeedActivator(IReflect modelType)
        {
            var propFlags = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance;
            var properties = modelType.GetProperties(propFlags);

            return properties.Select(p => p.Name).Distinct().Count() == 1;
        }

        private void ForceSetValue(PropertyInfo propertyInfo, object obj, object value)
        {
            var propName = $"<{propertyInfo.Name}>k__BackingField";
            var propFlags = BindingFlags.Instance | BindingFlags.NonPublic;
            
            obj.GetType().GetField(propName, propFlags)?.SetValue(obj, value);
        }

        private T GetFromHeaderAndDecode(IHeaderDictionary headers) { return (T)Activator.CreateInstance(typeof(T), new object[] { "ok" }); }
    }


All Articles