주말 아침 - 주간 닷넷 #50


한 주 동안 .NET 생태계에서 있었던 주요 이슈와 아티클, 기술 트렌드를 정리해 소개합니다.


:pushpin: .NET 11 Release Candidate 2 발표

  • 저자: .NET Team
  • 태그: #dotnet11 #rc2 #release

주요 내용

  • go-live 지원 라이선스가 포함된 두 번째 RC. Visual Studio 2026 Insiders와 VS Code C# Dev Kit에서 지원. 본문은 영역별 제목 목록이고 상세는 GitHub 릴리스 노트(dotnet/core)에 있음
  • 라이브러리: CborReader 증분 읽기(isFinalBlock: false, SlideData, Lax 모드 필요), 실험적 CompositeMLKem·HPKE API, ValidationAttribute.FormatMessage, SocketsHttpHandler.InitialHttp2MaxConcurrentStreams 추가. RC1 대비 MemoryCache 메트릭 속성명이 dotnet.cache.request.type에서 dotnet.cache.request.result로 변경되고, Decimal32/64/128 초월함수 정확도가 개선됨
  • 런타임·SDK: iOS·tvOS에서 파일 잠금이 기본 비활성이 되어 FileShare가 강제되지 않음(복원은 DOTNET_SYSTEM_IO_DISABLEFILELOCKING=0). RHEL 8 Arm64 SDK 크래시, cgroup v2, AssemblyLoadContext 언로드 후 제네릭 가상 디스패치 중단, ConcurrentQueue.TryDequeue 오보고 수정. 파일 기반 프로그램에서 #:property 값에 따옴표 허용, launch profile 인자에서 MSBuild 속성 확장
  • ASP.NET Core: QuickGrid의 AnchorMode·ItemComparer가 Experimental 해제. 남은 AddRazorRuntimeCompilation 오버로드가 ASPDEPR003 경고 대상. IValidationMessageFormatter 제거, Blazor BasePath 네임스페이스 이동, 실험적 DeviceBoundSessions 제거
  • MSBuild 와일드카드 매칭 최적화로 프로젝트 평가 10~13% 단축·할당 16~34% 감소, NuGet --min-publish-age-hours 옵션, F#(runtime async, reraise 등)·.NET MAUI·Windows Forms(KioskModeManager 등) 변경 포함. What’s new in C# 15, EF Core, Breaking Changes 문서는 링크로만 안내

:pushpin: Aspire 대시보드에 Native AOT 도입하기

주요 내용

  • Aspire 13.6부터 대시보드를 Native AOT로 게시. 페이지 표시 기준 시작 시간 중앙값 66% 감소, .NET 런타임 설치 불필요, .NET 11 타깃으로 브라우저 자산과 서버 코드 불일치(기존 .NET 8 빌드 + roll-forward의 JS 자산 버그) 해소
  • Blazor Web App은 지원되는 Native AOT 게시가 없었으나 .NET 11 RC1에 실험적 지원이 반영됨. Blazor 회로용 JSON 메타데이터 훅과 폼 필드 식 평가 대체 경로, Fluent UI의 런타임 제네릭 리플렉션 제거, Dapper.AOT의 DynamicParameters 지원
  • 트리밍 대응: Type 속성에 DynamicallyAccessedMembers(All)를 지정하고 DynamicDependency로 숨은 의존성을 보존. 경고 억제만으로는 멤버가 보존되지 않음
  • System.Text.Json 소스 생성: [JsonSerializable] 컨텍스트를 TypeInfoResolverChain.Insert로 등록해야 실제로 사용됨
  • 런타임 제네릭 생성 대체: RuntimeFeature.IsDynamicCodeSupported 가 false면 ValueTask.AsTask를 리플렉션으로 호출(DotNetDispatcher)

:pushpin: Durable Orchestration이 Activity를 await할 때 실제로 일어나는 일

  • 저자: Tamir Dresher
  • 태그: #durabletask #replay #orchestration dotnet

주요 내용

  • Durable Task SDK v1.26.0(독립 gRPC 워커)과 DurableTask.Core 3.9.0 소스 기준. CallActivityAsync는 action과 로컬 Task를 만들고, TaskOrchestrationExecutor가 OrchestratorExecutionResult.Actions로 남은 action을 반환
  • 저장되는 것은 history이고 일시 중단된 메서드가 아님. TaskScheduled 이벤트는 재구성된 후보 action을 제거하고, TaskCompleted 이벤트는 TaskScheduledId로 결과를 로컬 Task에 공급. L3의 total은 저장된 값이 아니라 재계산됨
  • 3번의 활성화(A/B/C)를 코드 줄 단위로 추적하고 HandleTaskCompletedEvent 등 실제 소스를 발췌. 대기 중인 오케스트레이션은 원래 워커의 메모리를 요구하지 않음
  • 결정성 제약의 근거: CurrentUtcDateTime, NewGuid(), CreateTimer, CreateReplaySafeLogger 사용 이유와 IsReplaying 안에서 durable action을 억제하면 안 되는 이유
  • replay, 설정된 retry, 미확인 activity의 재전달(at-least-once) 구분. ChargeOrder처럼 외부 효과가 있는 activity는 멱등 키가 필요

:pushpin: C# 15 유니온 타입: 직렬화 가능한 워크플로 만들기

  • 저자: Khalid Abuhakmeh
  • 태그: #csharp15 #dotnet11 #unions #systemtextjson

주요 내용

  • C# 15 union OrderState(Drafted, Submitted, InReview, Approved, Rejected, Fulfilled) 선언 방식. 케이스는 별도로 선언한 record이고, 컴파일러가 활성 케이스를 object? Value 하나에 담는 struct를 생성
  • OrderState, StepInput, InputRequest, StepResult 4개 유니온과 Next 메서드 하나로 구성한 주문 승인 워크플로. switch 식이 모든 케이스를 다루는지 컴파일러가 검사
  • System.Text.Json 기본 동작은 활성 케이스의 JSON만 쓰며 태그가 없음. 객체 모양 케이스가 둘 이상이면 JsonException: JSON value type 'Object' is ambiguous for union type 발생. {"$case":"InReview","value":{...}} 형태의 컨버터로 해결
  • 주의점: switch 문은 exhaustive 검사 대상이 아니어서 CS0161 발생, default(OrderState).Value는 null이라 CS8655 경고와 런타임 SwitchExpressionException, ?.는 CS0023, nullable 반환은 CS0508
  • 11.0.100-rc.1에서 실행했고, 직렬화 후 재구성한 실행 결과가 중단 없는 실행과 같은지 어서션으로 검증하는 전체 프로그램 수록

:pushpin: Aspire 대시보드에 영속성 추가하기

  • 저자: James Newton-King
  • 태그: aspire #sqlite #dapper #opentelemetry

주요 내용

  • Aspire 13.6 대시보드가 AppHost 실행마다 SQLite 데이터베이스를 새로 만들어 이전 실행의 리소스 상태·로그·트레이스·메트릭을 실행 선택기로 조회
  • 콘솔 로그·구조화 로그·트레이스 기본 한도가 각각 10,000에서 100,000으로 상향
  • 동일 대용량 부하에서 비공개 메모리 사용량이 13.5의 1,007 MB에서 13.6의 241 MB로 76% 감소
  • EF Core 대신 Dapper로 필터·집계·정렬·페이징 SQL을 직접 제어하고, Native AOT 대시보드에서는 Dapper.AOT로 빌드 시 코드 생성
  • 영속성 모드 None/Run/Resume(aspire dashboard run --persistence Resume), 기본 저장 위치는 사용자 .aspire 디렉터리(ASPIRE_HOME으로 변경)

:pushpin: UWP 및 WinUI 3 앱: MSTest로 UI 테스트하기

  • 저자: Amaury Levé
  • 태그: #mstest #winui uwp #testing

주요 내용

  • MSTest 4.5.1 + Microsoft.Testing.Platform 2.5.1에서 [UITestMethod]로 UWP·WinUI 3 앱의 UI 스레드 테스트 지원
  • 배포 모델 5종(클래식 UWP, 모던 UWP, 비패키지 WinUI 3, 패키지 WinUI 3, AppContainer WinUI 3)별 신원·신뢰 수준·MTP 시작 경로 표
  • global.json의 MSTest.Sdk 4.5.1과 test runner 지정, UseWinUI/UseUwp/PublishAot 설정, OnLaunched에서 DispatcherQueue 게시 후 MicrosoftTestingPlatformApplication.RunAsync 호출
  • sidecar 컨트롤러가 인자·취소·리포트·재시도·종료 코드를 관리하며, 패키지 앱은 AUMID로 활성화하고 AppContainer는 패키지 SID에만 파이프 접근 허용
  • Environment.ExitCode를 지정하지 않으면 실패한 테스트 실행이 성공으로 보일 수 있고, 패키지 활성화에는 Developer Mode 또는 사이드로딩 정책이 필요

:pushpin: DotThrow 2: .NET의 가드 절과 유효성 검증을 하나의 검사 체계로

https://shanto462.medium.com/dotthrow-2-one-set-of-checks-for-guard-clauses-and-validation-in-net-6d95c7724a6e

  • 저자: Shanto462 (실명 확인 못 함)
  • 태그: dotnet #guardclauses #validation #roslyn

주요 내용

  • 같은 규칙을 도메인 가드 절과 FluentValidation으로 이중 작성하는 문제를 하나의 체인(Throw().IfNullOrWhiteSpace()…)으로 통합, 3개 NuGet 패키지(DotThrow, DependencyInjection, AspNetCore)로 배포
  • Guard를 readonly ref struct로 만들어 스택에 두고 JIT 인라인. BenchmarkDotNet(.NET 10)에서 int 범위 검사 0.27ns 대 직접 작성 if 0.25ns, 통과 체인은 할당 없음
  • ValidationReport 수집 모드, Validator(When/Unless, 비동기 규칙, 자식·다형 검증기), JSON·IConfiguration 기반 동적 규칙. FluentValidation 12.1.1 대비 2.7~7배 빠르고 메모리 6~16%
  • Roslyn 분석기 11개(코드 수정 9개). WhenPresent()가 int?를 default(int)로 바꾸는 함정을 DT0011 경고로 탐지
  • DI의 ValidateWithDotThrow().ValidateOnStart()로 옵션 검증, AspNetCore는 .WithValidation()로 400 ProblemDetails 반환(실패 값은 응답에 미포함), Native AOT·.NET Standard 2.0~.NET 10 지원

:pushpin: .NET Framework에서 Autofac 8을 9로 올린 방법, 그리고 System.ValueTuple을 그대로 둔 이유

https://rogerkeizer.medium.com/how-i-upgraded-autofac-8-to-9-on-net-framework-and-why-i-left-system-valuetuple-alone-2551c078f2d1

  • 저자: Roger (성은 확인 못 함)
  • 태그: #autofac #dotnetframework #bindingredirect #legacy

주요 내용

  • 장애 원인 규명: System.ValueTuple 4.6.x의 lib/net471에는 빈 . 파일만 있고 bin에 DLL이 복사되지 않는데, Web.config의 bindingRedirect(→4.0.3.0)가 남아 FileLoadException 발생. 팀이 지목한 Autofac은 원인이 아니었음
  • 패키지 버전과 어셈블리 버전은 다름(4.6.2 → 4.0.5.0 등). AssemblyName.GetAssemblyName으로 실제 버전 확인, 웹·테스트 프로젝트의 DiagnosticSource redirect 불일치 발견
  • 업그레이드 단계: Autofac 8.4.0→9.3.4, Autofac.Mvc5·WebApi2 →7.0.0, DiagnosticSource·Bcl.AsyncInterfaces →10.0.12, CommonServiceLocator 2개 패키지 제거
  • 정적 ServiceLocator를 request.GetDependencyScope()로 교체, 전역 필터의 public 필드 공유로 인한 경쟁 조건 제거
  • 안전망 테스트: ContainerConfig 추출 후 생성자 의존성 구조 검사, Web.config·App.config redirect 일치 검사, 수동 생성한 HS256 토큰으로 JWT 필터 13개 케이스 검증. 각 테스트의 한계도 명시

:pushpin: 병렬 코드가 업데이트를 잃는 이유: lock, Interlocked, 스레드 로컬 상태 측정

  • 저자: Dhana gani (페이지 표기)
  • 태그: csharp #threading #parallel #interlocked

주요 내용

  • Parallel.For로 카운터를 100,000번 증가시키는 실험. 안전하지 않은 버전 13회 결과가 1,020~32,690(67~99% 손실), 예외 없음
  • counter++의 읽기·더하기·쓰기 3단계와, 람다에서 캡처된 변수가 클로저 객체 필드가 되어 모든 스레드가 공유하는 구조
  • 측정(.NET 8, Release): 순차 0.033ms, 스레드 로컬+병합 0.28ms, Interlocked 2.2~2.7ms, lock 9~22ms. 작은 작업에서는 순차 루프가 모든 병렬 버전을 앞섬
  • Interlocked.CompareExchange로 AtomicMax 구현, volatile은 ++를 원자적으로 만들지 않음, ConcurrentDictionary는 AddOrUpdate로 한 번에 갱신
  • await가 필요한 보호 구간은 SemaphoreSlim(finally에서 Release), 선택 표와 BenchmarkDotNet 버전 제시

:pushpin: 기다리지 않고 시간 의존 코드 테스트하기

  • 저자: Milan Jovanović
  • 태그: dotnet #testing #timeprovider

주요 내용

  • 5분 뒤 만료되는 초대 링크를 예로, InvitationPolicy가 TimeProvider를 생성자로 받고 clock.GetUtcNow() < expiresAt 으로 만료를 판정. 운영에서는 AddSingleton(TimeProvider.System) 등록
  • Microsoft.Extensions.TimeProvider.Testing 패키지의 FakeTimeProvider와 Advance로 만료 1 tick 전, 정각, 1 tick 후를 검증. < 를 <= 로 바꾸면 정각 단언만 실패
  • Task.Delay(TimeSpan, TimeProvider)에 provider를 넘기지 않으면 가짜 시계를 진행해도 지연이 끝나지 않음. WaitAsync(5초)로 실수 시 5분 대기 대신 5초 만에 실패시킴
  • CancellationTokenSource(TimeSpan, TimeProvider) 오버로드로 1분 타임아웃이 대기 중인 작업을 취소하는지 검증
  • DateTime.UtcNow 직접 호출은 FakeTimeProvider 영향을 받지 않으며, DB 타임스탬프와 캐시 만료는 통합 테스트로 남겨야 함

:pushpin: Minimal API에서 System.Text.Json을 REDox로 교체하기

  • 저자: Khalid Abuhakmeh
  • 태그: #redox #json #minimalapi #benchmark

주요 내용

  • Capcom이 공개한 Apache-2.0 토큰 기반 구조화 데이터 엔진 REDox(NuGet CAPCOM.REDox, 네임스페이스 REDox.Json, .NET 10 필요). 토큰 DOM으로 JSON, JSON5, CBOR, MessagePack, INI 등을 지원
  • Minimal API의 TypedResults.Ok, Results.Json 은 System.Text.Json에 고정돼 있어 IResult 구현(RedoxJsonResult)과 Results.Extensions 확장으로 Response.BodyWriter에 직접 직렬화. 기본 camelCase가 적용되지 않아 “Id”, “Sku” 로 출력됨
  • BenchmarkDotNet(500개 Product, 약 468 KB, .NET 10, Apple M4 Pro): 직렬화 357.6us 대 419.9us(약 15% 빠름, 할당 약 9% 많음), 역직렬화 872.9us 대 1,009.4us(약 13% 빠름, 할당 약 17% 적음)
  • HTTP 계층은 순차 curl 루프에서 129.76 대 128.44 req/s로 사실상 동률. 직렬화 바이트 할당은 약 468,800 대 513,100
  • 결론: README의 1.6~2.8배 수치는 canada.json 등 다른 데이터 형태 기준이며, 페이로드 형태와 전체 요청 비용에 따라 달라짐

:pushpin: 실전 Durable Task: 실제로 어떤 문제를 해결하는가

  • 저자: Tamir Dresher
  • 태그: #durabletask #orchestration #workflow

주요 내용

  • Durable Task .NET SDK v1.26.0과 Durable Task Scheduler로 다섯 시나리오(송장 승인, Azure 테넌트 프로비저닝, 구독 결제, 순서가 뒤바뀐 웹훅, AI 에이전트 인시던트 조사)를 다룸. 각 시나리오마다 일반 C# 구현과 오케스트레이션 코드를 비교
  • 송장 승인: 송장별 하위 오케스트레이션, WaitForExternalEvent와 지속 타이머. 이벤트는 힌트일 뿐이고 확정된 결정은 애플리케이션 저장소가 보유. 결과 재생은 CLR 스택 복원이 아니라 기록된 히스토리에 대한 오케스트레이터 코드 재실행
  • 프로비저닝: CreateTimer 기반 폴링, 활동 결과 기록 전후 워커 장애의 두 시나리오 구분. ARM/Bicep이 이미 해결하는 범위를 먼저 검토. 보상은 되돌리기가 아니며 진행 중인 Azure 작업을 확인한 뒤에만 정리
  • 결제: 타임아웃은 거절이 아님. ContinueAsNew로 청구 주기를 넘기고 결제 원장이 권위. 웹훅: 엔티티가 최신 리비전만 기억하고 대상(검색 인덱스)이 오래된 리비전을 원자적으로 거부해야 함
  • 정확히 한 번 외부 효과는 히스토리로 보장되지 않으며 안정적 키와 조정 절차는 애플리케이션 책임. 코드는 컴파일된 샘플이 아니라 발췌

:bookmark_tabs: 가벼운 읽을거리

후보 항목 중 이슈로 선정되지 않은 가벼운 읽을거리들


테스트 374개가 들어가고 352개가 나왔는데 빌드는 초록이었다

  • Windows 데스크톱 UI 테스트 엔진 winwright(WPF, WinForms, 트레이, 메뉴, 다이얼로그 대상, Windows 전용·.NET 10 전용). 실행 전제가 빠져 검사가 실행되지 않은 경우를 "hole"로 정의하고 통과·실패와 분리
  • 종료 코드 0(Passed), 1(Failed), 2(Degraded), 3(Broken). 요약 줄이 "Passed: 1 of 9 cases, 8 not run"처럼 실행되지 않은 수를 앞에 표시. 0건을 선택하는 필터는 거부
  • 기본 액션(invoke, toggle, set value, select, expand)은 UI Automation 패턴을 사용해 포그라운드·마우스·키보드가 필요 없음. type·click 같은 실제 입력 액션은 별도 구분하며, click은 NoAutomationPeer 등 사유 지정 필요

ASP.NET Core의 BOLA: 인증된 사용자가 다른 사람의 데이터를 읽을 수 있는 이유

  • OWASP API Security Top 10 1위인 BOLA(IDOR): [Authorize]와 정책 인가를 통과해도 invoiceId만 바꾸면 다른 테넌트의 객체를 읽는 취약 컨트롤러 사례
  • 1차 방어: AuthorizationHandler<SameTenantRequirement, Invoice>와 IAuthorizationService.AuthorizeAsync(User, invoice, “SameTenant”)로 객체 단위 검사를 정책으로 분리, HTTP 없이 단위 테스트 가능
  • 2차 방어: EF Core HasQueryFilter(i => i.TenantId == tenantProvider.TenantId). 필터가 켜져 있으면 타 테넌트 조회는 null이 되어 404가 되고 핸들러는 실행되지 않음

Directory.Build.props: .NET 프로젝트 간 설정 공유

  • Sdk.props가 프로젝트 디렉터리에서 위로 올라가며 처음 발견한 Directory.Build.props 하나만 import하고 여러 파일을 병합하지 않음. 프로젝트 본문 이전에 import되므로 .csproj에서 같은 속성을 다시 지정하면 덮어씀
  • 하위 폴더에 파일이 있으면 루트 파일이 통째로 무시됨. Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))"로 부모 파일을 명시적으로 가져와 확장
  • Directory.Build.targets는 프로젝트 본문 이후에 import되어 AssemblyName, TargetFramework 같은 최종 값을 참조 가능. Directory.Packages.props에서 ManagePackageVersionsCentrally를 켜면 Version 속성이 남은 PackageReference에서 NU1008 발생

Serilog로 오래된 로그 파일 정리하기

  • 디스크가 가득 찬 실제 장애에서 출발. 기본 설정은 롤링과 삭제가 없고, fileSizeLimitBytes 기본 1GB에 도달하면 이벤트를 조용히 버림
  • rollingInterval(Infinite 기본, Year~Minute), retainedFileCountLimit(기본 31), retainedFileTimeLimit, rollOnFileSizeLimit 옵션과 log-20261010_001.txt 형태의 시퀀스 번호
  • 최악 디스크 사용량 = fileSizeLimitBytes x retainedFileCountLimit(100MB x 14 = 1.4GB). 개수 제한은 일 수가 아니라 파일 수라서 바쁜 날 오래된 날짜가 먼저 밀려남

코드가 Vault에서 시크릿을 읽는다면 무엇으로 테스트하는가

  • .NET용 Testcontainers Vault 모듈 Drydock.Testcontainers.Vault(v0.1, MIT). VaultBuilder(“hashicorp/vault:2.1”).WithInitCommand(…)로 시작 시 Vault CLI 명령을 순서대로 실행, openbao 이미지로도 동작
  • Java·Go 모듈이 요청하는 IPC_LOCK 권한이 dev 모드에서는 불필요(Mlock enabled:false, 1.13~2.1)하다고 확인해 요청하지 않음
  • 초기화 명령이 실패하면 한 번의 셸 호출이 첫 실패에서 중단되고 StartAsync가 Vault 오류 메시지와 함께 예외를 던짐

JCOReflector: .NET API를 JVM으로, 이제 실험적 제네릭 지원

  • JCOReflector는 JCOBridge 위에서 .NET 어셈블리의 public 타입마다 Java 클래스를 생성. 속성은 getX()/setX(), 이벤트는 addX/removeX, [Flags] enum은 add/remove 메서드로 변환하며 모든 클래스가 IJCOBridgeReflected 를 구현
  • 리파이드 제네릭(.NET)과 타입 소거(Java) 차이를 super type token 기법으로 해결. 생성자 뒤에 {} 를 붙인 익명 하위 클래스(new List_1() {})에서 타입 인자를 복원하고, 누락 시 IllegalArgumentException 발생
  • 생성 규칙: 제네릭 타입은 아리티 접미사(_1, _2), 제네릭 델리게이트는 NetGenericHelper 사용, 소거로 시그니처가 충돌하는 멤버는 EqualsGeneric, RemoveByKey 로 이름 변경

dotnetup 소개: .NET 툴체인 관리자

  • .NET SDK와 런타임을 사용자 계정 폴더에 설치·관리하는 툴체인 관리자의 공개 프리뷰. 관리자 권한이 필요 없고 macOS·Linux·PowerShell용 부트스트랩 스크립트 제공
  • 명령: dotnetup init(셸 프로필 설정, 최신 안정 SDK 설치), list, update, runtime install aspnetcore@10.0, sdk install preview, sdk install daily, self update
  • 채널(10.0, latest)은 update 때 다시 해석되고 정확한 버전(10.0.3)은 고정. update 후 더 이상 필요 없는 설치는 자동 제거. global.json 고정과 함께 동작

더 빠르고 가벼워진 C# Dev Kit

  • C# Dev Kit을 전면 재설계하고 버전을 .NET 릴리스에 맞춰 3.3에서 11.0으로 변경. 현재 프리릴리스 채널에서 제공
  • 로드 시간: Orleans(155개 프로젝트) 전체 50.3초에서 2.3초(22배), Aspire(407개) 84.1초에서 3.0초(28배). 활성 파일 기준은 각각 0.53초, 0.47초
  • 증분 빌드(Aspire 407개): 변경 없음 34.4초에서 0.88초, 단일 C# 파일 변경 36.1초에서 2.1초. 빌드 가속과 fast up-to-date check 도입. 서버 메모리는 Orleans 1,307MB에서 208MB, Roslyn 2,000MB에서 379MB, Aspire 2,072MB에서 316MB(약 81~85% 감소)
4개의 좋아요