ASP.NET Core 3中是否有一种内置的方法来使用snake case作为JSON命名策略?

39
我成功地使用以下代码使其正常工作:
.AddNewtonsoftJson(options => {
    options.SerializerSettings.ContractResolver = new DefaultContractResolver
    {
        NamingStrategy = new SnakeCaseNamingStrategy()
    };
});

然而这样做会使MVC使用Newtonsoft而不是更快、异步且内置的System.Text.JSON

查看System.Text.JSON中的命名策略选项,我只能找到camelCase。 是否有原生支持snake case的方法? 有什么更好的方法可以实现snake case JSON命名风格吗?


我认为asp.net core可以直接实现这个功能,不需要额外的配置。我错了吗? - Daniel Schmid
2
默认情况下它不使用蛇形命名法,而是使用驼峰命名法。 - numberjak
8个回答

43

更新

.Net 8已经发布,并且内置了对不同命名策略的支持。

var serializeOptions = new JsonSerializerOptions
{
    // This can be changed to other naming policies like SnakeCaseLower, KebabCaseLower
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
    WriteIndented = true
};
jsonString = JsonSerializer.Serialize(weatherForecast, serializeOptions);

旧答案

只需对 pfx 代码进行轻微修改,以消除对 Newtonsoft Json.Net 的依赖。

String 扩展方法将给定的字符串转换为 SnakeCase

public static class StringUtils
{
    public static string ToSnakeCase(this string str)
    {
        return string.Concat(str.Select((x, i) => i > 0 && char.IsUpper(x) ? "_" + x.ToString() : x.ToString())).ToLower();
    }
}

然后在我们的SnakeCaseNamingPolicy中,我们可以这样做
public class SnakeCaseNamingPolicy : JsonNamingPolicy
{
    public static SnakeCaseNamingPolicy Instance { get; } = new SnakeCaseNamingPolicy();

    public override string ConvertName(string name)
    {
        // Conversion to other naming convention goes here. Like SnakeCase, KebabCase etc.
        return name.ToSnakeCase();
    }
}

最后一步是在Startup.cs中注册我们的命名策略。
public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers()            
        .AddJsonOptions(
            options => { 
                options.JsonSerializerOptions.PropertyNamingPolicy = 
                    SnakeCaseNamingPolicy.Instance;
            });
}

使用这个模型:
public class WeatherForecast
{
    public DateTime Date { get; set; }

    public int TemperatureCelcius { get; set; }

    public int TemperatureFahrenheit { get; set; }

    public string Summary { get; set; }
}

Json 输出:

{
  "date": "2019-10-28T08:26:14.878444+05:00",
  "temperature_celcius": 4,
  "temperature_fahrenheit": 0,
  "summary": "Scorching"
}

7
实际上,它并没有错。C# 属性命名规范采用 PascalCase。因此,CPU 被认为是三个不同的单词。你可以使用两种解决方案:一种是使用 [JsonPropertyName("CPU_power")],另一种是将属性名称更改为 CpuPower - Muhammad Hannan
使用 PropertyNamingPolicy = SnakeCaseNamingPolicy.Instance 而不是 PropertyNamingPolicy = new SnakeCaseNamingPolicy() 的原因是什么?我尝试了 'new' 变体,就我所看到的,它完美地工作了。 - Niels Lucas

19

目前,没有内置的蛇形命名支持,但.NET Core 3.0允许通过继承JsonNamingPolicy来设置自定义命名策略。

您需要实现ConvertName方法以进行蛇形命名转换。(Newtonsoft Json.NET有一个内部的StringUtils类,展示了如何处理这个问题。)


下面的POC实现仅在蛇形命名转换中重用了Json.NET的SnakeCaseNamingStrategy, 而整个应用程序使用了System.Text.Json

最好避免仅为蛇形命名转换而依赖于Newtonsoft Json.Net,但是在这个相当懒惰的例子中,我不想重新思考或发明蛇形命名转换方法。
这个答案的主要重点是如何挂接自定义策略(而不是蛇形命名转换本身)。
(有许多库和代码示例展示了如何做到这一点。)

public class SnakeCaseNamingPolicy : JsonNamingPolicy
{
    private readonly SnakeCaseNamingStrategy _newtonsoftSnakeCaseNamingStrategy
        = new SnakeCaseNamingStrategy();

    public static SnakeCaseNamingPolicy Instance { get; } = new SnakeCaseNamingPolicy();

    public override string ConvertName(string name)
    { 
        /* A conversion to snake case implementation goes here. */

        return _newtonsoftSnakeCaseNamingStrategy.GetPropertyName(name, false);
    }
}

Startup.cs中应用自定义的SnakeCaseNamingPolicy

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers()            
        .AddJsonOptions(
            options => { 
                options.JsonSerializerOptions.PropertyNamingPolicy = 
                    SnakeCaseNamingPolicy.Instance;
            });
}
下面是该类的一个实例。
public class WeatherForecast
{
    public DateTime Date { get; set; }

    public int TemperatureCelcius { get; set; }

    public int TemperatureFahrenheit { get; set; }

    [JsonPropertyName("Description")]
    public string Summary { get; set; }
}

将具有Json表示形式,如下所示:

{ "date" : "2019-10-28T01:00:56.6498885+01:00",
  "temperature_celcius" : 48,
  "temperature_fahrenheit" : 118,
  "Description" : "Cool"
}
请注意,属性Summary的名称已更改为Description,以匹配其System.Text.Json.Serialization.JsonPropertyNameAttribute

2
请注意,已经有一个GitHub问题用于跟踪在System.Text.Json中直接实现JsonSnakeCaseNamingPolicy。截至2021年7月23日,该实现计划在.NET7中发布。 - Métoule

4

有一个GitHub仓库提供了对System.Text.JsonSnakeCaseKebabCase支持,同时还有一个nuget包可用。

nuget

PM> Install-Package JorgeSerrano.Json.JsonSnakeCaseNamingPolicy

SnakeCase 命名策略

var person = new Person { FirstName = "Jorge", Birthday = DateTime.UtcNow, MyJobCity = "Madrid" };

var options = new JsonSerializerOptions { PropertyNamingPolicy = new JsonSnakeCaseNamingPolicy() };
var json = JsonSerializer.Serialize(person, options);

烤肉串命名规则(KebabCase)

var person = new Person { FirstName = "Jorge", Birthday = DateTime.UtcNow, MyJobCity = "Madrid" };

var options = new JsonSerializerOptions { PropertyNamingPolicy = new JsonKebabCaseNamingPolicy() };
var json = JsonSerializer.Serialize(person, options);

3

我在这里分享@pfx的解决方案的全面实现。 定制命名策略(从NewtonSoft复制):

using System.Text;
using System.Text.Json;

namespace Utils
{
    public class SnakeCaseNamingPolicy : JsonNamingPolicy
    {
        public override string ConvertName(string name) => JsonUtils.ToSnakeCase(name);
    }

    public class JsonUtils
    {

        private enum SeparatedCaseState
        {
            Start,
            Lower,
            Upper,
            NewWord
        }

        public static string ToSnakeCase(string s) => ToSeparatedCase(s, '_');
    
        private static string ToSeparatedCase(string s, char separator)
        {
            if (string.IsNullOrEmpty(s))
            {
                return s;
            }

            StringBuilder sb = new StringBuilder();
            SeparatedCaseState state = SeparatedCaseState.Start;

            for (int i = 0; i < s.Length; i++)
            {
                if (s[i] == ' ')
                {
                    if (state != SeparatedCaseState.Start)
                    {
                        state = SeparatedCaseState.NewWord;
                    }
                }
                else if (char.IsUpper(s[i]))
                {
                    switch (state)
                    {
                        case SeparatedCaseState.Upper:
                            bool hasNext = (i + 1 < s.Length);
                            if (i > 0 && hasNext)
                            {
                                char nextChar = s[i + 1];
                                if (!char.IsUpper(nextChar) && nextChar != separator)
                                {
                                    sb.Append(separator);
                                }
                            }
                            break;
                        case SeparatedCaseState.Lower:
                        case SeparatedCaseState.NewWord:
                            sb.Append(separator);
                            break;
                    }

                    char c;
                    c = char.ToLowerInvariant(s[i]);
                    sb.Append(c);

                    state = SeparatedCaseState.Upper;
                }
                else if (s[i] == separator)
                {
                    sb.Append(separator);
                    state = SeparatedCaseState.Start;
                }
                else
                {
                    if (state == SeparatedCaseState.NewWord)
                    {
                        sb.Append(separator);
                    }

                    sb.Append(s[i]);
                    state = SeparatedCaseState.Lower;
                }
            }

            return sb.ToString();
        }
    }
}

以下模型被用于简单示例:
    public class TestSerializer
    {
        public DateTime TimeStamp { get; set; }
        public int CPUPower { get; set; } 
    }

示例用法:

var data = new TestSerializer();
data.TimeStamp = DateTime.Now;
data.CPUPower = 10;

var serializeOptions = new JsonSerializerOptions 
{
    PropertyNamingPolicy = new SnakeCaseNamingPolicy()
};
var json_string = JsonSerializer.Serialize(data, serializeOptions);
Console.WriteLine(json_string);

给出了{"time_stamp":"2020-08-06T00:30:35.3815583-04:00","cpu_power":10}



1
这个解决方案稍晚了一些,但也适用于ABCItem或MyCPU等情况。这只是一个概念,您可以完善它使其更加通用。
using System.Collections.Generic;

namespace Extensions
{
    public static class StringExtension
    {
        public static string ToSnakeCase(this string str)
        {
            // collect the final result
            var snakeCase = new List<char>();

            // check and add chars (using for loop for performance)
            for (int i = 0; i < str.Length; i++)
            {
                if (i > 0 && char.IsUpper(str[i]) && !char.IsUpper(str[i + 1]))
                {
                    snakeCase.Add('_');
                }
                snakeCase.Add(str[i]);
            }

            // build the new string
            return new string(snakeCase.ToArray()).ToLower();
        }
    }
}

var snakeCase = "CPUMeter".ToSnakeCase();
var snakeCase = "PascalCase".ToSnakeCase();
var snakeCase = "camelCase".ToSnakeCase();

1

没有必要使用单独的 kebab-case 和 snake-case 命名规则。 此外,如果您绝对想要最小化不必要的字符转换和查找次数,转换方法可以进行一定程度的优化。

using System.Collections.Generic;
using System.Linq;
using System.Text.Json;

public class SeperatorNamingPolicy : JsonNamingPolicy
{
    public SeperatorNamingPolicy(char seperator = '_')
    {
        Seperator = seperator;
    }
    public char Seperator { get; }

    public override string ConvertName(string name)
    {
        IEnumerable<char> ToSeperated()
        {
            var e = name.GetEnumerator();
            if (!e.MoveNext()) yield break;
            yield return char.ToLower(e.Current);
            while (e.MoveNext())
            {
                if (char.IsUpper(e.Current))
                {
                    yield return Seperator;
                    yield return char.ToLower(e.Current);
                }
                else
                {
                    yield return e.Current;
                }
            }
        }

        return new string(ToSeperated().ToArray());
    }
}

然而,如果你只想要一个蛇形命名策略而不向你的代码添加额外的依赖,那么一个专门的snakecasenamingpolicy就足够了:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;

public class SnakeCaseNamingPolicy : JsonNamingPolicy
{
    public override string ConvertName(string name)
    {
        static IEnumerable<char> ToSnakeCase(CharEnumerator e)
        {
            if (!e.MoveNext()) yield break;
            yield return char.ToLower(e.Current);
            while (e.MoveNext())
            {
                if (char.IsUpper(e.Current))
                {
                    yield return '_';
                    yield return char.ToLower(e.Current);
                }
                else
                {
                    yield return e.Current;
                }
            }
        }

        return new string(ToSnakeCase(name.GetEnumerator()).ToArray());
    }
}

当然,您可以通过在startup.cs中添加json选项来使用它:

public void ConfigureServices(IServiceCollection services)
{
    // . . .
    services
        .AddControllers()
        .AddJsonOptions(o => o.JsonSerializerOptions.PropertyNamingPolicy = new SnakeCaseNamingPolicy());
    // . . .
}

-1
        static IEnumerable<char> ToSnakeCase(CharEnumerator e)
        {
            if (!e.MoveNext()) yield break;
            var wasLower = char.IsLower(e.Current);
            yield return char.ToLower(e.Current);
            while (e.MoveNext())
            {
                var isLower = char.IsLower(e.Current);
                if (wasLower && !isLower)
                {
                    yield return '_';
                }

                wasLower = isLower;

                yield return char.ToLower(e.Current);
            }
        }

这个问题已经有七个现有答案,其中包括一个得到了四十多个赞同票的最佳答案。你确定你的解决方案还没有被提供过吗?如果不是,你为什么认为你的方法比已经得到社区验证的现有建议更好?在Stack Overflow上提供解释总是有用的,但在问题已经得到提问者和社区满意解决的情况下,这尤其重要。通过解释你的答案与众不同之处以及何时可能更受欢迎,帮助读者更好地理解。 - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接