GN2.Common.Library 3.0.0

dotnet add package GN2.Common.Library --version 3.0.0
                    
NuGet\Install-Package GN2.Common.Library -Version 3.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="GN2.Common.Library" Version="3.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="GN2.Common.Library" Version="3.0.0" />
                    
Directory.Packages.props
<PackageReference Include="GN2.Common.Library" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add GN2.Common.Library --version 3.0.0
                    
#r "nuget: GN2.Common.Library, 3.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package GN2.Common.Library@3.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=GN2.Common.Library&version=3.0.0
                    
Install as a Cake Addin
#tool nuget:?package=GN2.Common.Library&version=3.0.0
                    
Install as a Cake Tool

GN2.Common.Library

GN2.Common.LibraryGN2.Core 를 사용하는 프로젝트가 반복해서 구현하는 일들을 모아둔 범용 유틸리티 패키지입니다. 웹 프레임워크에 의존하지 않으므로 API 서버, 워커, 콘솔 앱에서 똑같이 사용할 수 있습니다.

패키지 정보

  • Target Framework: net10.0
  • Version: 3.0.0
  • NuGet: GN2.Common.Library
  • 의존성: GN2.Core, Microsoft.Extensions.DependencyInjection.Abstractions, Microsoft.Extensions.Logging.Abstractions, Microsoft.Extensions.Options

구성

Abstracts/  IActionBridge · IBridgeRequest · IBridgeHandler · IBridgeBehavior
            IObjectLinker · MappingExpression
            IEmailSender · EmailMessage
            ISecretProtector · IPasswordHasher
Bridge/     ActionBridge, AddActionBridge
Mapping/    ObjectLinker, AddObjectLinker
Security/   AesGcmSecretProtector, Pbkdf2PasswordHasher, HashText, Base64UrlText
Mail/       SmtpEmailSender, AddSmtpEmailSender
Text/       HtmlText

ActionBridge — 요청 디스패처

요청 하나에 핸들러 하나를 대응시키고, 필요하면 파이프라인으로 감쌉니다.

using GN2.Common.Library.Abstracts;
using GN2.Common.Library.Bridge;

services.AddActionBridge(options =>
{
    options.RegisterServicesFromAssemblyContaining<Program>();
    options.AddOpenBehavior(typeof(LoggingBehavior<,>));
});

public sealed record GetArticle(long Id) : IBridgeRequest<ReturnValues<ArticleModel>>;

public sealed class GetArticleHandler : IBridgeHandler<GetArticle, ReturnValues<ArticleModel>>
{
    public async Task<ReturnValues<ArticleModel>> HandleAsync(GetArticle request, CancellationToken cancellationToken = default)
    {
        // ...
    }
}

var result = await bridge.SendAsync(new GetArticle(10), cancellationToken);

파이프라인 동작은 등록한 순서대로 바깥에서 안쪽으로 실행됩니다.

public sealed class LoggingBehavior<TRequest, TResponse> : IBridgeBehavior<TRequest, TResponse>
    where TRequest : IBridgeRequest<TResponse>
{
    private readonly ILogger<LoggingBehavior<TRequest, TResponse>> _logger;

    public LoggingBehavior(ILogger<LoggingBehavior<TRequest, TResponse>> logger) => _logger = logger;

    public async Task<TResponse> HandleAsync(TRequest request, BridgeHandlerDelegate<TResponse> next, CancellationToken cancellationToken = default)
    {
        _logger.LogDebug("{Request} 처리 시작", typeof(TRequest).Name);
        var response = await next(cancellationToken);
        _logger.LogDebug("{Request} 처리 완료", typeof(TRequest).Name);
        return response;
    }
}
  • 요청 타입 하나에 핸들러는 하나만 등록할 수 있습니다. 둘 이상 발견되면 등록 시점에 예외가 발생합니다.
  • AddActionBridge 는 여러 번 호출해도 중복 등록되지 않습니다.
  • 핸들러 수명은 기본 Transient, IActionBridge 는 기본 Scoped 이며 옵션으로 바꿀 수 있습니다.

ObjectLinker — 경량 객체 매퍼

이름이 같고 타입이 호환되는 속성은 규약으로 복사하고, 나머지는 명시적으로 지정합니다.

using GN2.Common.Library.Mapping;

services.AddObjectLinker(options =>
{
    options.CreateMap<Article, ArticleModel>()
           .ForMember(d => d.UserName, s => s.User.FullName)
           .ForMember(d => d.LocalizedUrls, s => s.Translations.Where(t => t.IsConfirmed).ToList())
           .Ignore(d => d.InternalMemo);
});

var model = linker.Map<Article, ArticleModel>(article);
var models = linker.MapMany<Article, ArticleModel>(articles);
  • 매핑 계획은 타입 쌍마다 한 번만 계산되고 인스턴스 단위로 캐시됩니다.
  • 규약 매핑은 같은 이름 + 대입 가능한 타입(값 타입 TT? 포함)일 때 적용됩니다. 숫자 암시 변환(intlong)은 매핑하지 않습니다.
  • 대상 속성이 없거나 쓰기 불가능하면 ForMember 구성 시점에 예외가 발생합니다.
  • 대상 타입은 참조 타입이어야 하며 매개변수 없는 생성자가 필요합니다.

Security — 암호화 · 해시

문자열 보호 (AES-256-GCM)

using GN2.Common.Library.Abstracts;
using GN2.Common.Library.Security;

services.AddSecretProtector(options =>
{
    options.Passphrase = configuration["Security:Passphrase"]; // 보안 저장소에서 주입
    options.Salt = "gn2-article-v1";                           // 애플리케이션 고정값
});

var token = protector.Protect("010-0000-0000");
if (protector.TryUnprotect(token, out var phone))
{
    // ...
}
  • 결과는 URL·쿼리스트링에 그대로 넣을 수 있는 Base64Url 문자열입니다.
  • 메시지마다 임의 nonce 를 사용하므로 같은 평문도 매번 다른 값이 됩니다. 보호된 값을 조회 키로 쓸 수 없습니다. 검색이 필요하면 HashText.Sha256Hex 로 만든 별도 컬럼을 사용하세요.
  • 인증 태그로 변조를 감지합니다. 손상되거나 위조된 값은 TryUnprotectfalse 를 반환합니다.
  • SaltPassphrase 를 바꾸면 기존 값을 복호화할 수 없습니다.

비밀번호 해시 (PBKDF2-HMAC-SHA512)

services.AddPasswordHasher();

var stored = hasher.Hash(password);        // pbkdf2-sha512$210000$...$...
var isValid = hasher.Verify(password, stored);
if (isValid && hasher.NeedsRehash(stored))
{
    // 반복 횟수 정책이 올라갔다면 로그인 성공 시점에 다시 저장
}

반복 횟수가 해시 문자열에 포함되므로, 정책을 올려도 기존 해시를 그대로 검증할 수 있습니다.

해시 · 인코딩

HashText.Sha256Hex("text");                       // 체크섬 · 캐시 키 · 조회용 해시
HashText.HmacSha256Hex(payload, secretKey);       // 웹훅 서명
HashText.FixedTimeEquals(signature, expected);    // 타이밍 공격 방지 비교

Base64UrlText.EncodeText("한글도 안전합니다");
Base64UrlText.TryDecode(token, out var bytes);

HashText 는 비밀번호 저장에 사용하지 않습니다. 비밀번호는 반드시 IPasswordHasher 를 사용하세요.

Mail — SMTP 발송

using GN2.Common.Library.Mail;

services.AddSmtpEmailSender(options => configuration.GetSection("Smtp").Bind(options));

await sender.SendHtmlAsync("user@example.com", "가입을 환영합니다", html, cancellationToken);

await sender.SendAsync(new EmailMessage
{
    Subject = "월간 리포트",
    To = new[] { "user@example.com" },
    Bcc = new[] { "archive@example.com" },
    HtmlBody = html,
    TextBody = text,
}, cancellationToken);

SmtpConfigurationGN2.Core.Configurations 에 있습니다. 로그에는 수신자 수와 결과만 남고 주소·본문은 남지 않습니다.

Text — HTML 헬퍼

using GN2.Common.Library.Text;

var summary = HtmlText.Summarize(article.Content, 120);
var thumbnail = HtmlText.ExtractFirstImageSource(article.Content);
var plain = HtmlText.StripTags(article.Content);
var body = HtmlText.ToHtmlLineBreaks(comment.Content);

정규식 기반이므로 편집기 산출물 같은 단순한 마크업을 대상으로 합니다. StripTags 는 요약·색인용이며 XSS 방어용 새니타이저가 아닙니다.

2.x 에서 올라올 때

2.x 3.0.0
CryptoHelper.AES256.Encrypt/Decrypt ISecretProtector.Protect/Unprotect (저장 형식 변경, 이전 값 복호화 불가)
CryptoHelper.SHA512.Encrypt/ValidateCheck IPasswordHasher.Hash/Verify (저장 형식 변경, 이전 해시 검증 불가)
CryptoHelper.SHA256.Encrypt HashText.Sha256Hex (비밀번호 용도로는 사용 금지)
CryptoHelper.SaltAdd/SaltRemove, Base64Handler Base64UrlText
StartupHelper.GetConfiguration(args) builder.Configuration
StartupHelper.RegisterAllowedCors(hosts) ASP.NET Core 기본 services.AddCors(...)
ServiceLocator.Resolve<T>() / AddLocatorProvider() 생성자 주입
HtmlTagHelper HtmlText (뷰 비교 헬퍼와 YouTube 추출은 제거)
ActionBridgeEngine, ObjectLinkerEngine ActionBridge, ObjectLinker (네임스페이스 Bridge, Mapping)

CORS 는 프레임워크 기본 API 로 대체합니다.

services.AddCors(options => options.AddDefaultPolicy(policy =>
    policy.WithOrigins(allowedHosts).AllowAnyHeader().AllowAnyMethod()));

사용 시 주의

  • ActionBridgeObjectLinker 는 런타임 리플렉션을 사용하므로 트리밍 · Native AOT 경로에서는 사용하지 않습니다. Security · Mail · Text 는 리플렉션을 사용하지 않습니다.
  • AddSecretProtector · AddPasswordHasher 는 구성 오류를 등록 시점에 예외로 알립니다.
  • Passphrase 같은 비밀 값은 appsettings.json 이 아니라 환경 변수나 보안 저장소에서 주입하세요.
Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
3.0.0 102 8/22/2026
2.1.0 142 3/11/2026
2.0.6 123 3/2/2026
2.0.5 120 3/2/2026
2.0.4 118 2/23/2026
2.0.3 166 2/1/2026
2.0.2 137 1/11/2026
2.0.1 134 1/11/2026
2.0.0 127 1/11/2026
1.2.1 277 10/20/2025
1.2.0 231 10/20/2025
1.1.9 327 10/19/2025
1.1.8 250 10/10/2025
1.1.7 351 10/2/2025
1.1.6 224 9/6/2025
1.1.5 283 9/5/2025
1.1.4 321 8/30/2025
1.1.3 239 8/21/2025
1.1.2 215 8/21/2025
1.1.1 176 8/16/2025
Loading failed

3.0.0 (호환성 없는 변경 포함)

이번 버전은 패키지를 "범용 유틸리티" 로 다시 정의하면서 저장 형식과 공개 API 가 모두 바뀌었습니다. 2.x 에서 올릴 때는 아래 내용을 반드시 확인하세요.

보안 (중요)
- 암호화를 AES-256-GCM 인증 암호화로 교체했습니다. 메시지마다 임의 nonce 를 사용하고 인증 태그로 변조를 감지합니다. 기존 AESHandler / AES128Handler / AES256Handler 는 CBC 고정 IV(비밀키 길이를 솔트로 사용)에 무결성 검증이 없어 제거했습니다.
- 비밀번호 해시를 PBKDF2-HMAC-SHA512 210,000회(OWASP 권장)로 교체했습니다. 무염 SHA-256 해시로 비밀번호를 검증하던 SHA256Handler.ValidateCheck 와 1,000회 반복이던 SHA512Handler 를 제거했습니다.
- 문자로 치환하던 SaltAdd / SaltRemove 를 표준 Base64Url 인코딩으로 대체했습니다.
- 2.x 형식으로 저장된 암호문과 해시는 3.0.0 에서 복호화 · 검증되지 않습니다. 비밀번호는 다음 로그인 시 재해시하고, 암호문은 이전 버전으로 복호화한 뒤 다시 저장해야 합니다.
- 전역 가변 상태였던 CryptoHelper.ConfigureSecret 을 제거했습니다. 이제 ISecretProtector / IPasswordHasher 를 DI 로 주입받습니다.

제거
- ServiceLocator 와 AddLocatorProvider — 서비스 로케이터는 안티패턴이며, AddLocatorProvider 는 BuildServiceProvider 로 컨테이너를 하나 더 만들어 싱글턴이 두 번 생성되는 버그가 있었습니다.
- StartupHelper.GetConfiguration — 호스트 빌더가 이미 수행하는 일을 중복 구현하면서 인자로 받은 builder 를 무시했습니다. builder.Configuration 을 사용하세요.
- RegisterAllowedCors — 레거시 Microsoft.AspNetCore.Cors 2.x 를 참조하게 만들고, 허용 목록이 비면 AllowAnyOrigin 으로 열리는 문제가 있었습니다. ASP.NET Core 기본 AddCors 를 직접 사용하세요.
- HtmlTagHelper 의 ValueCompare / ValueContain — 뷰 전용 헬퍼이며 ValueContain(string) 은 비교 방향이 뒤바뀐 버그가 있었습니다.
- GetYoutubeURLFromTags — 정규식이 잘못돼 그룹당 값이 중복 수집됐습니다.

정확성
- ObjectLinker 의 매핑 캐시가 static 이어서, 서로 다른 컨테이너가 같은 타입 쌍을 다르게 구성하면 먼저 만들어진 규칙이 계속 사용되던 문제를 수정했습니다. 캐시는 이제 인스턴스 단위입니다.
- ObjectLinker 가 매핑할 때마다 속성 목록을 선형 탐색하던 것을, 타입 쌍마다 한 번 계산하는 매핑 계획으로 바꿨습니다.
- ForMember 대상이 존재하지 않거나 쓰기 불가능하면 첫 매핑 시점이 아니라 구성 시점에 예외를 던집니다. (bool)(object) 변환이 섞인 식도 인식합니다.
- ActionBridge 가 호출마다 MethodInfo.Invoke 로 핸들러를 호출하던 것을, 요청 타입별 강타입 디스패처 캐시로 바꿨습니다.
- 한 요청에 핸들러가 둘 이상 발견되면 조용히 마지막 등록이 이기는 대신 등록 시점에 예외를 던집니다. AddActionBridge 를 여러 번 호출해도 중복 등록되지 않습니다.
- SmtpEmailSender 가 수신자 주소와 본문 전체를 Information 수준으로 기록하던 문제를 수정했습니다(개인정보 유출). 이제 수신자 수만 남깁니다.
- SmtpEmailSender 가 SmtpClient 하나를 재사용하고 동기 Send 를 async 메서드에서 호출하던 문제를 수정했습니다. 발송마다 클라이언트를 만들고 SendMailAsync 와 CancellationToken 을 사용합니다.
- 정규식에 타임아웃과 소스 생성기를 적용했습니다.

기능
- IBridgeBehavior 파이프라인을 추가했습니다. AddOpenBehavior(typeof(LoggingBehavior<,>)) 로 모든 요청에 횡단 관심사를 적용할 수 있습니다.
- ObjectLinker 에 MapMany, Ignore, 값 타입 T 에서 T? 로의 규약 매핑을 추가했습니다.
- EmailMessage 로 다중 수신자 · 참조 · 숨은참조 · 회신 주소 · HTML/평문 본문을 지원합니다.
- HtmlText.Summarize 로 목록 미리보기 문구를 만들 수 있습니다.

의존성
- Microsoft.AspNetCore.Cors, Microsoft.Extensions.Configuration(및 CommandLine / EnvironmentVariables / Json), Microsoft.Extensions.Hosting.Abstractions, Microsoft.Extensions.DependencyInjection 참조를 제거했습니다.
- 남은 의존성은 GN2.Core 와 Microsoft.Extensions 추상화(DependencyInjection.Abstractions, Logging.Abstractions, Options) 뿐입니다.

전체 변경 내역: https://github.com/gn2studio/GN2/releases