Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 1개 있습니다.)

C# - JSON 역/직렬화 시 리플렉션 손실을 없애는 JsonSrcGen

Source Generator를 이용해,

C# - Source Generator 소개
; https://www.sysnet.pe.kr/2/0/12223

Reflection 단계를 없애고 JSON을 직렬화/역직렬화하는 nuget 패키지가 나왔습니다. ^^

Is the era of reflection-heavy C# libraries at an end?
; https://blog.marcgravell.com/2021/05/is-era-of-reflection-heavy-c-libraries.html

JsonSrcGen
; https://github.com/trampster/JsonSrcGen

따라서 단순히 JsonSrcGen 패키지만 참조 추가하고, 다음과 같이 적절하게 특성을 적용해 주면 JSON 직렬화/역직렬화를 고속으로 수행할 수 있습니다.

using System;
using JsonSrcGen;

namespace ConsoleApp2
{
    // Install-Package JsonSrcGen
    class Program
    {
        static void Main(string[] args)
        {
            var converter = new JsonConverter();

            ReadOnlySpan<char> json = converter.ToJson(new MyType() { MyProperty = "Some value" });
            Console.WriteLine(json.ToString());

            var myType = new MyType();
            converter.FromJson(myType, "{ \"my_name\" : \"Some value\" }");
            // 또는, converter.FromJson(myType, json);

            Console.WriteLine(myType);
        }
    }


    [Json]
    public class MyType
    {
        [JsonName("my_name")]
        public string MyProperty { get; set; }

        [JsonIgnore]
        public string IgnoredProperty { get; set; }

        public override string ToString()
        {
            return $"MyProperty = {MyProperty}";
        }
    }
}

/* 출력 결과
{"my_name":"Some value"}
MyProperty = Some value
*/

이때 Source Generator가 자동 생성하는 코드는 JsonConverter이고, 위와 같은 경우 MyType에 대해 다음과 같은 식의 소스 코드를 생성해 냅니다.

// %LOCALAPPDATA%\Temp\VSGeneratedDocuments\6aae0b5e-b25f-5c50-066b-6883b1eb7a7f\JsonConverter.cs

using System;
using System.Text;
using System.Collections.Generic;

namespace JsonSrcGen
{
#nullable enable
    public class JsonConverter
    {
        [ThreadStatic]
        JsonStringBuilder? Builder;
        public ReadOnlySpan<char> ToJson(ConsoleApp2.MyType value)
        {

            var builder = Builder;
            if(builder == null)
            {
                builder = new JsonStringBuilder();
                Builder = builder;
            }
            builder.Clear();
            ToJson(value, builder);
            return builder.AsSpan();
        }
        public void ToJson(ConsoleApp2.MyType value, JsonStringBuilder builder)
        {
            if(value.MyProperty == null)
            {
                builder.Append("{\"my_name\":null");
            }
            else
            {
                builder.Append("{\"my_name\":\"");
                builder.AppendEscaped(value.MyProperty);
                builder.Append("\"");
            }
            builder.Append("}");
        }
        public ReadOnlySpan<char> FromJson(ConsoleApp2.MyType value, ReadOnlySpan<char> json)
        {
            json = json.SkipWhitespaceTo('{');
            while(true)
            {
                json = json.SkipWhitespace();
                char value452 = json[0];
                if(value452 == '\"')
                {
                    json = json.Slice(1);
                }
                else if(value452 == '}')
                {
                    return json.Slice(1);
                }
                else
                {
                    throw new InvalidJsonException($"Unexpected character! expected '}}' or '\"' but got '{value452}'", json);
                }
                var propertyName = json.ReadTo('\"');
                json = json.Slice(propertyName.Length + 1);
                json = json.SkipWhitespaceTo(':');
                switch(propertyName.Length % 1)
                {
                    case 0:
                        if(!propertyName.EqualsString("my_name"))
                        {
                            json = json.SkipProperty();
                            break;
                        }
                        json = json.Read(out string? property453Value);
                        value.MyProperty = property453Value;
                        break;
                }
                json = json.SkipWhitespace();
                if(json[0] == ',')
                {
                    json = json.Slice(1);
                }
            }
        }
    }
}
#nullable restore

재미있군요. ^^ 현재 .NET Core 2.1 이상의 프로젝트에서 사용할 수 있습니다.

(첨부 파일은 이 글의 예제 코드를 포함합니다.)




(아래의 버그는 1.1 버전에서 해결될 거라고 합니다. ^^ 대응이 무척 빠르군요.)

참고로, JsonSrcGen 대상이 되는 타입을 namespace 없이 정의하면,

using System;
using JsonSrcGen;

/*
namespace ConsoleApp2
{
*/
    [Json]
    public class MyType
    {
        [JsonName("my_name")]
        public string MyProperty { get; set; }

        [JsonIgnore]
        public string IgnoredProperty { get; set; }
    }
// }

이런 식의 무수한 컴파일 오류 메시지를 보게 될 것입니다. ^^;

Error CS0501 '<invalid-global-code>.<invalid-global-code>(value, builder)' must declare a body because it is not marked abstract, extern, or partial

Error CS0501 'JsonConverter.ToJson(global)' must declare a body because it is not marked abstract, extern, or partial

Error CS0116 A namespace cannot directly contain members such as fields or methods

Error CS1520 Method must have a return type

아직 1.0.3의 초기 버전이라 현업에 적용하실 때는 적절한 사전 검증 작업이 필요할 듯합니다.




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]






[최초 등록일: ]
[최종 수정일: 6/25/2021]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 



2021-07-30 09시43분
정성태
2023-01-20 08시30분
riok/mapperly
; https://github.com/riok/mapperly

A .NET source generator for generating object mappings. No runtime reflection. Inspired by MapStruct.
정성태

1  2  [3]  4  5  6  7  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
13557정성태2/18/20241901Windows: 258. Task Scheduler의 Author 속성 값을 변경하는 방법
13556정성태2/17/20241946Windows: 257. Windows - Symbolic (hard/soft) Link 및 Junction 차이점
13555정성태2/15/20242100닷넷: 2216. C# - SemaphoreSlim 사용 시 주의점
13554정성태2/15/20241845VS.NET IDE: 189. Visual Studio - 닷넷 소스코드 디컴파일 찾기가 안 될 때
13553정성태2/14/20241931닷넷: 2215. windbg - thin/fat lock 없이 동작하는 Monitor.Wait + Pulse
13552정성태2/13/20241879닷넷: 2214. windbg - Monitor.Enter의 thin lock과 fat lock
13551정성태2/12/20242076닷넷: 2213. ASP.NET/Core 웹 응용 프로그램 - 2차 스레드의 예외로 인한 비정상 종료
13550정성태2/11/20242161Windows: 256. C# - Server socket이 닫히면 Accept 시켰던 자식 소켓이 닫힐까요?
13549정성태2/3/20242486개발 환경 구성: 706. C# - 컨테이너에서 실행하기 위한 (소켓) 콘솔 프로젝트 구성
13548정성태2/1/20242318개발 환경 구성: 705. "Docker Desktop for Windows" - ASP.NET Core 응용 프로그램의 소켓 주소 바인딩(IPv4/IPv6 loopback, Any)
13547정성태1/31/20242064개발 환경 구성: 704. Visual Studio - .NET 8 프로젝트부터 dockerfile에 추가된 "USER app" 설정
13546정성태1/30/20241921Windows: 255. (디버거의 영향 등으로) 대상 프로세스가 멈추면 Socket KeepAlive로 연결이 끊길까요?
13545정성태1/30/20241839닷넷: 2212. ASP.NET Core - 우선순위에 따른 HTTP/HTTPS 호스트:포트 바인딩 방법
13544정성태1/30/20241853오류 유형: 894. Microsoft.Data.SqlClient - Could not load file or assembly 'System.Security.Permissions, ...'
13543정성태1/30/20241849Windows: 254. Windows - 기본 사용 중인 5357 포트 비활성화는 방법
13542정성태1/30/20241881오류 유형: 893. Visual Studio - Web Application을 실행하지 못하는 IISExpress - 두 번째 이야기
13541정성태1/29/20241928VS.NET IDE: 188. launchSettings.json의 useSSL 옵션
13540정성태1/29/20242057Linux: 69. 리눅스 - "Docker Desktop for Windows" Container 환경에서 IPv6 Loopback Address 바인딩 오류
13539정성태1/26/20242153개발 환경 구성: 703. Visual Studio - launchSettings.json을 이용한 HTTP/HTTPS 포트 바인딩
13538정성태1/25/20242235닷넷: 2211. C# - NonGC(FOH) 영역에 .NET 개체를 생성파일 다운로드1
13537정성태1/24/20242292닷넷: 2210. C# - Native 메모리에 .NET 개체를 생성파일 다운로드1
13536정성태1/23/20242457닷넷: 2209. .NET 8 - NonGC Heap / FOH (Frozen Object Heap) [1]
13535정성태1/22/20242254닷넷: 2208. C# - GCHandle 구조체의 메모리 분석
13534정성태1/21/20242087닷넷: 2207. C# - SQL Server DB를 bacpac으로 Export/Import파일 다운로드1
13533정성태1/18/20242287닷넷: 2206. C# - TCP KeepAlive의 서버 측 구현파일 다운로드1
13532정성태1/17/20242179닷넷: 2205. C# - SuperSimpleTcp 사용 시 주의할 점파일 다운로드1
1  2  [3]  4  5  6  7  8  9  10  11  12  13  14  15  ...