.Net Core Api: obtendo dados em uma solicitação de diferentes fontes

O .Net Core possui um mecanismo interno de Model Binding, que permite não apenas aceitar parâmetros de entrada nos controladores, mas também receber objetos imediatamente com campos preenchidos. Isso permite incorporar todas as verificações necessárias a esse objeto usando a Validação de modelo.

Aqui estão apenas os dados necessários para o funcionamento da API, não apenas para consulta ou para corpo. Alguns dados precisam ser recebidos dos cabeçalhos (no meu caso, havia json na base64), alguns dados devem ser de serviços externos ou ActionRoute se você usar REST. Você pode usar sua ligação para obter dados a partir daí. É verdade que há um problema: se você decidir não quebrar o encapsulamento e inicializar o modelo através do construtor, terá que usar xamã.

Para mim e para as gerações futuras, decidi escrever algo como instruções para usar o Binding e o xamanismo.

Problema


Um controlador típico se parece com isso:

[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);
}

Como resultado, temos os seguintes problemas:

  1. A lógica de validação é manchada pelo objeto de solicitação, pelo método de solicitação do cabeçalho, pelo método de solicitação do serviço e pelo método do controlador. Para garantir que a verificação necessária esteja definitivamente lá, é necessário realizar uma investigação completa!
  2. O método do controlador adjacente terá exatamente o mesmo código. Programação de copiar e colar em ataque.
  3. Geralmente, há muito mais verificações do que no exemplo e, como resultado, a única linha significativa - a chamada para o método de processamento da lógica de negócios - fica oculta em um monte de código. Vê-lo e entender o que está acontecendo aqui em geral exige algum esforço.

Encadernação personalizada (modo fácil)


Parte do problema pode ser resolvida implementando seu manipulador no pipeline de processamento de solicitações. Para fazer isso, primeiro corrija nosso controlador passando o objeto final para o método imediatamente. Parece muito melhor, certo?

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

Em seguida, crie seu fichário para o tipo 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) { ... }
}

Por fim, corrigiremos o modelo FinalData adicionando a ligação do fichário à propriedade:

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

    public string SomeDataText { get; set; }

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

Já é melhor, mas as hemorróidas aumentaram: agora você precisa saber que temos um manipulador especial e indicá-lo em todos os modelos. Mas é solucionável.

Crie seu 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;
    }
}

E registre-o na Inicialização:

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

O provedor é chamado para cada objeto de modelo na ordem da fila. Se o nosso provedor atender ao tipo desejado, ele retornará o fichário desejado. Caso contrário, o fichário padrão funcionará. Portanto, agora, sempre que especificarmos o tipo de MoreData, ele será obtido e decodificado no Header e os atributos especiais nos modelos não precisarão ser especificados.

Encadernação personalizada (modo rígido)


Tudo isso é ótimo, mas há uma coisa: para que a mágica funcione, nosso modelo deve ter propriedades públicas com set. Mas e o encapsulamento? E se eu quiser transferir os dados da solicitação para vários lugares na nuvem e saber que eles não serão alterados lá?

O problema é que o fichário padrão não funciona para modelos que não possuem um construtor padrão. Mas o que nos impede de escrever os nossos?
O serviço para o qual escrevi esse código não usa REST, os parâmetros são transmitidos apenas por meio de Consulta e Corpo e apenas dois tipos de consultas são usados ​​- Get
e Post. Por conseguinte, no caso da API REST, a lógica de processamento será ligeiramente diferente.
Em geral, o código permanecerá inalterado, apenas nosso fichário precisa de aprimoramento para criar um objeto e preencher seus campos particulares. Além disso, darei trechos de código com comentários, que não estão interessados ​​- no final do artigo, abaixo do gato, está a lista inteira da classe.

Primeiro, vamos determinar se MoreData é a única propriedade da classe. Se sim, você precisa criar o objeto você mesmo (olá, Activator); caso contrário, o JsonConvert fará o trabalho perfeitamente, e apenas inseriremos os dados necessários na propriedade.

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;
}

Criar um objeto através do JsonConvert é simples, para consultas com 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;
}

Mas com o Query eu tive que me deitar. Ficaria feliz se alguém pudesse sugerir uma solução mais bonita.

Ao passar uma matriz, vários parâmetros com o mesmo nome são obtidos. A conversão para um tipo simples ajuda, mas a serialização coloca aspas extras na matriz [], que deve ser removida manualmente.

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;
}

Por fim, criando o objeto, é necessário gravar nossos dados em sua propriedade privada. Foi sobre esse xamanismo que falei no começo do artigo. Encontrei esta solução aqui , pela qual muito obrigado ao autor.

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);
}

Bem, combinamos esses métodos em uma única chamada:

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;
}

Resta corrigir o BinderProvider para que ele responda a qualquer classe com a propriedade desejada:

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));
    }
}


Isso é tudo. O Binder se mostrou um pouco mais complicado do que no Modo Fácil, mas agora podemos vincular propriedades “externas” em todos os métodos de todos os controladores sem esforços adicionais. Dos menos:

  1. O construtor de objetos com campos particulares deve especificar o atributo [JsonConstrustor]. Mas isso se encaixa completamente na lógica do modelo e não interfere com sua percepção.
  2. Em algum lugar, pode ser necessário obter MoreData não do cabeçalho. Mas isso é tratado através da criação de uma classe separada.
  3. O resto da equipe deve estar ciente da presença de mágica. Mas a documentação salvará a humanidade.

Uma lista completa do fichário resultante está aqui:

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