Нормализация строковых входных данных с помощью Binding ASP.Net Core 5

Мне нужно нормализовать строковые данные (заменить некоторые символы друг другом, например: «ی» на «ي» или обрезать их). Для этого я создал следующую привязку модели, как показано ниже:

public class StringModelBinder : IModelBinder
{
    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);

        if (valueProviderResult == ValueProviderResult.None)
            return Task.CompletedTask;

        var value = Normalize(valueProviderResult.FirstValue);

        bindingContext.Result = ModelBindingResult.Success(value);

        return Task.CompletedTask;
    }
}

Этот биндер работает как для Query, так и для Route, но не работает, если я использую атрибут FromBody. Это не удается, потому что BindModelAsync никогда не вызывается. Я нашел еще один вопрос, поднятый по этой проблеме здесь и к сожалению нет ответа.

Я попытался расширить ComplexObjectModelBinder, но это класс sealed (а также не предоставляет никакого конструктора). Поэтому я попытался расширить ComplexTypeModelBinder, который помечен как устаревший.

Я скопировал логику из ComplexTypeModelBinderProvider из исходный код и, к моему удивлению, BindModelAsync из моих StringModelBinder теперь принимает звонки. Но все равно не получается, потому что bindingContext.ValueProvider содержит только провайдера для маршрута и результат остается нулевым.

Мой поставщик связывателя на данном этапе:

public class MyModelBinderProvider : IModelBinderProvider
{
    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        if (context == null)
        {
            throw new ArgumentNullException(nameof(context));
        }

        if (context.Metadata.IsComplexType && !context.Metadata.IsCollectionType)
        {
            var propertyBinders = new Dictionary<ModelMetadata, IModelBinder>();
            for (var i = 0; i < context.Metadata.Properties.Count; i++)
            {
                var property = context.Metadata.Properties[i];
                propertyBinders.Add(property, context.CreateBinder(property));
            }
                
            var loggerFactory = context.Services.GetRequiredService<ILoggerFactory>();
            return new ComplexTypeModelBinder(
                propertyBinders,
                loggerFactory,
                allowValidatingTopLevelNodes: true);
        }

        if (context.Metadata.ModelType == typeof(string))
        {
            return new StringModelBinder();
        }

        return null;
    }
}

Я также попытался создать провайдера из тела и изменил свой StringModelBinder на:

public class StringModelBinder : IModelBinder
{
    public async Task BindModelAsync(ModelBindingContext bindingContext)
    {
        var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);

        if (valueProviderResult == ValueProviderResult.None)
        {
            var context = new ValueProviderFactoryContext(bindingContext.ActionContext);
            await new FormValueProviderFactory().CreateValueProviderAsync(context);

            valueProviderResult = context.ValueProviders
                .Select(x => x.GetValue(bindingContext.ModelName))
                .FirstOrDefault(x => x != ValueProviderResult.None);

            if (valueProviderResult == ValueProviderResult.None) return;
        }

        var value = valueProviderResult.FirstValue.Replace("A", "B");

        bindingContext.Result = ModelBindingResult.Success(value);
    }
}

Теперь вопрос в том, как лучше всего выполнить эту нормализацию в .Net 5?

Кому может быть интересно: этот вопрос может показаться дублированием, но я не смог найти ничего, связанного с .Net 5, и если есть вопрос, отвечающий на вопрос ComplexTypeModelBinder, он не подходит для .Net 5, поскольку он устарел.


person Hamid Mayeli    schedule 02.01.2021    source источник
comment
Связыватель модели создается для настройки способа связывания модели, т. е. способа чтения и преобразования строкового/двоичного тела в объекты .NET. Мне кажется, вы очень ошибаетесь в этой идее и должны просто создать статический класс с помощью служебного метода и вызвать его, где и когда это необходимо.   -  person Camilo Terevinto    schedule 02.01.2021
comment
Это означает, что мне нужно вызывать служебный метод почти для каждого действия и для всех строковых параметров/свойств. Я думаю, что должен быть более простой способ сделать это.   -  person Hamid Mayeli    schedule 02.01.2021
comment
У вас есть промежуточное программное обеспечение, фильтры действий и т. д. и т. д. и т. д. Есть много мест и способов сделать это, связыватель, конечно, не тот (вы ничего не привязываете, вы меняете материал, который уже был десериализован)   -  person Camilo Terevinto    schedule 02.01.2021
comment
Выполнение этого в Middleware или ActionFilter означает добавление отражения в жизненный цикл запроса, не повлияет ли это на производительность. Я знаю, что это было бы очень крошечным, но я все еще беспокоюсь об этом. Кроме того, спасибо за вашу поддержку.   -  person Hamid Mayeli    schedule 02.01.2021


Ответы (2)


FromBody отличается от FromQuery и других глаголов HTTP.

В сложной привязке модели (FromBody) их можно получить в bindingContext.HttpContext.Request.Body.

public class StringModelBinder : IModelBinder
{
    public async Task BindModelAsync(ModelBindingContext bindingContext)
    {
        
        using (var reader = new StreamReader(bindingContext.HttpContext.Request.Body))
        {
            var body = reader.ReadToEndAsync();
            var mydata = body.Result;

            //...
            bindingContext.Result = ModelBindingResult.Success(mydata);
        }
        //...
    }
}

действие

    [HttpPost]
    public IActionResult test1([ModelBinder(binderType: typeof(StringModelBinder))]string model)
    {

        return Ok(model);
    }

Затем передайте строку в действие.

введите здесь описание изображения

Получить его в StringModelBinder.

введите здесь описание изображения

person Karney.    schedule 04.01.2021
comment
Спасибо за ваше решение. Но это означает, что мне также нужно создать поставщика значений. Должен быть какой-то способ использовать встроенный поставщик значений, я ошибаюсь? С другой стороны, если я прочитаю тело для первого свойства, смогу ли я сделать то же самое для второго? - person Hamid Mayeli; 04.01.2021
comment
Да, вы правы, в ядре asp.net есть встроенный поставщик. Вы можете передать тело json, затем отформатировать его и получить второе. - person Karney.; 04.01.2021
comment
На самом деле я не понял, как я могу передать тело JSON встроенному поставщику значений и использовать его в своем поставщике строк. Кроме того, главный вопрос заключался в том, как лучше всего выполнить задачу, а не заставить мою реализацию работать любой ценой. - person Hamid Mayeli; 11.01.2021
comment
Как вы передаете данные от клиента, потому что вы используете [FromBody]? - person Karney.; 12.01.2021
comment
@HamidMayeli, вам не нужно использовать поставщика значений, потому что [FromBody] отличается от [FromQuery]. Кроме того, я отредактировал ответ. - person Karney.; 12.01.2021

Вы можете реализовать собственный преобразователь строк:

    public class CustomStringConverter : System.Text.Json.Serialization.JsonConverter<string>
{
    public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        var value = reader.GetString();

        // Do your stuffs with value

        return value;
    }

    public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options)
    {
        writer.WriteStringValue(value);
    }
}

а затем зарегистрируйте его:

services.AddControllers()
            .AddJsonOptions(options =>
            {
                options.JsonSerializerOptions.Converters.Add(new CustomStringConverter());
            });
person Alireza Yavari    schedule 19.04.2021