| name | lang-csharp-library-dev |
| description | C#-specific library development patterns. Use when creating .NET class libraries, designing NuGet packages, configuring project files, implementing strong naming and versioning, writing XML documentation, unit testing with xUnit/NUnit, source generators, and multi-targeting. Extends meta-library-dev with .NET tooling and ecosystem patterns. |
C# Library Development
C#-specific patterns for .NET library development. This skill extends meta-library-dev with .NET tooling, API design guidelines, and NuGet ecosystem practices.
This Skill Extends
meta-library-dev - Foundational library patterns (API design, versioning, testing strategies)
For general concepts like semantic versioning, module organization principles, and testing pyramids, see the meta-skill first.
This Skill Adds
- .NET tooling: .csproj configuration, dotnet CLI, NuGet packaging
- .NET idioms: API design guidelines, strong naming, XML documentation
- .NET ecosystem: NuGet publishing, multi-targeting, source generators
This Skill Does NOT Cover
- General library patterns - see
meta-library-dev
- ASP.NET Core - see
lang-csharp-aspnet-dev
- Entity Framework - see
lang-csharp-ef-dev
- Desktop application development
Overview
Publishing a .NET library requires understanding the modern .NET SDK project system and NuGet ecosystem:
┌─────────────────────────────────────────────────────────────────┐
│ .NET Library Stack │
├─────────────────────────────────────────────────────────────────┤
│ Source Code (*.cs) │
│ │ │
│ ▼ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ .csproj │───▶│ C# Compiler│───▶│ Assembly │ │
│ │ Config │ │ (Roslyn) │ │ (.dll) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────┐ │ │
│ │ │ XML Docs │ │ │
│ │ │ (.xml) │ │ │
│ │ └─────────────┘ │ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ NuGet Package │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │ .dll │ │ .xml │ │ .nupkg │ │ │
│ │ │ Assembly│ │ Docs │ │Metadata │ │ │
│ │ └─────────┘ └─────────┘ └─────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────┐ │
│ │ NuGet │ │
│ │ Publish │ │
│ └───────────┘ │
└─────────────────────────────────────────────────────────────────┘
Key Decision Points:
| Decision | Options | Recommendation |
|---|
| Target framework | .NET 6+, .NET Standard 2.0, .NET Framework 4.x | .NET 8+ for new libraries; multi-target if broad compatibility needed |
| Testing framework | xUnit, NUnit, MSTest | xUnit for new projects; NUnit for compatibility |
| Documentation | XML comments, DocFX, Sandcastle | XML comments required; DocFX for rich docs |
| Strong naming | Signed, Unsigned | Sign only if required by consumers |
Quick Reference
| Task | Command |
|---|
| New class library | dotnet new classlib -n MyLibrary |
| Build | dotnet build |
| Test | dotnet test |
| Pack | dotnet pack |
| Publish to NuGet | dotnet nuget push *.nupkg -s nuget.org -k <key> |
| Multi-target build | dotnet build -f net8.0 |
| Generate docs | dotnet build -p:GenerateDocumentationFile=true |
Project File Structure (.csproj)
Required Fields for NuGet Publishing
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<PackageId>MyCompany.MyLibrary</PackageId>
<Version>1.0.0</Version>
<Authors>Author Name</Authors>
<Company>Company Name</Company>
<Description>Brief description of the library</Description>
<Copyright>Copyright (c) 2025 Company Name</Copyright>
<PackageLicenseExpression>MIT</>
https://github.com/username/repo
https://github.com/username/repo
git
README.md
tag1;tag2;tag3
icon.png
Release notes for this version
true
true
true
true
true
true
snupkg
Multi-Targeting Configuration
<PropertyGroup>
<TargetFrameworks>net8.0;net6.0;netstandard2.0</TargetFrameworks>
</PropertyGroup>
<PropertyGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
<LangVersion>9.0</LangVersion>
</PropertyGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
<PackageReference Include="System.Memory" Version="4.5.5" />
</ItemGroup>
Strong Naming
<PropertyGroup>
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>MyLibrary.snk</AssemblyOriginatorKeyFile>
<PublicSign Condition="'$(OS)' != 'Windows_NT'">true</PublicSign>
</PropertyGroup>
API Design Guidelines
Namespace Organization
namespace MyCompany.MyLibrary
{
}
namespace MyCompany.MyLibrary.Extensions
{
}
namespace MyCompany.MyLibrary.Abstractions
{
}
namespace MyCompany.MyLibrary.Internal
{
}
Class Design Patterns
Immutable Value Types
public readonly record struct UserId
{
public UserId(Guid value)
{
if (value == Guid.Empty)
throw new ArgumentException("User ID cannot be empty.", nameof(value));
Value = value;
}
public Guid Value { get; }
public override string ToString() => Value.ToString();
}
Builder Pattern
public sealed class ConfigurationBuilder
{
private TimeSpan _timeout = TimeSpan.FromSeconds(30);
private int _retryCount = 3;
private bool _strictMode;
public ConfigurationBuilder WithTimeout(TimeSpan timeout)
{
ArgumentOutOfRangeException.ThrowIfLessThan(timeout, TimeSpan.Zero);
_timeout = timeout;
return this;
}
public ConfigurationBuilder WithRetryCount(int count)
{
ArgumentOutOfRangeException.ThrowIfNegative(count);
_retryCount = count;
return this;
}
public ConfigurationBuilder EnableStrictMode()
{
_strictMode = true;
return this;
}
public Configuration Build()
{
return new Configuration(_timeout, _retryCount, _strictMode);
}
}
{
{
Timeout = timeout;
RetryCount = retryCount;
StrictMode = strictMode;
}
TimeSpan Timeout { ; }
RetryCount { ; }
StrictMode { ; }
}
Factory Pattern
public static class ParserFactory
{
public static IParser Create(ParserOptions options)
{
return options.Mode switch
{
ParserMode.Strict => new StrictParser(options),
ParserMode.Lenient => new LenientParser(options),
_ => throw new ArgumentOutOfRangeException(nameof(options.Mode))
};
}
}
Interface Design
public interface IParser<in TInput, out TOutput>
{
Task<TOutput> ParseAsync(TInput input, CancellationToken cancellationToken = default);
}
Extension Methods
public static class StringExtensions
{
public static string ToSnakeCase(this string value)
{
ArgumentNullException.ThrowIfNull(value);
return value;
}
public static bool IsNullOrWhiteSpace([NotNullWhen(false)] ? )
{
.IsNullOrWhiteSpace();
}
}
XML Documentation
Required Documentation Elements
{
ArgumentNullException.ThrowIfNull(input);
}
Documentation for Properties
public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(30);
Documentation Comments Best Practices
- Always document public APIs
- Use
<see cref=""/> for cross-references
- Include
<example> sections for complex APIs
- Document exceptions with
<exception>
- Use
<remarks> for additional context
- Include
<value> for properties
Testing Patterns
xUnit Test Structure
using Xunit;
using FluentAssertions;
namespace MyLibrary.Tests;
public class ParserTests
{
[Fact]
public async Task ParseAsync_ValidInput_ReturnsExpectedOutput()
{
var parser = new Parser();
var input = "valid input";
var result = await parser.ParseAsync(input);
result.Should().NotBeNull();
result.Value.Should().Be("expected");
}
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
public async Task ParseAsync_InvalidInput_ThrowsArgumentException(string input)
{
var parser = new Parser();
await Assert.ThrowsAsync<ArgumentException>(
async () => await parser.ParseAsync(input));
}
[Fact]
public void Constructor_NullOptions_ThrowsArgumentNullException()
{
var exception = Assert.Throws<ArgumentNullException>(
() => Parser(options: !));
exception.ParamName.Should().Be();
}
}
NUnit Test Structure
using NUnit.Framework;
namespace MyLibrary.Tests;
[TestFixture]
public class ParserTests
{
private Parser _parser = null!;
[SetUp]
public void SetUp()
{
_parser = new Parser();
}
[Test]
public async Task ParseAsync_ValidInput_ReturnsExpectedOutput()
{
var input = "valid input";
var result = await _parser.ParseAsync(input);
Assert.That(result, Is.Not.Null);
Assert.That(result.Value, Is.EqualTo("expected"));
}
[TestCase(null)]
[TestCase("")]
[TestCase(" ")]
public void ParseAsync_InvalidInput_ThrowsArgumentException(string input)
{
Assert.ThrowsAsync<ArgumentException>(
async () => await _parser.ParseAsync(input));
}
[TearDown]
public void TearDown()
{
_parser?.Dispose();
}
}
Test Project Configuration
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<IsPackable>false</IsPackable>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="xunit" Version="2.6.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.4" />
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="Moq" Version="4.20.70" />
< = = />
Source Generators
Creating a Source Generator
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using System.Text;
[Generator]
public class MySourceGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var classDeclarations = context.SyntaxProvider
.CreateSyntaxProvider(
predicate: static (s, _) => IsSyntaxTargetForGeneration(s),
transform: static (ctx, _) => GetSemanticTargetForGeneration(ctx))
.Where(static m => m is not null);
context.RegisterSourceOutput(classDeclarations, Execute);
}
private static bool IsSyntaxTargetForGeneration(SyntaxNode node)
{
return node is ClassDeclarationSyntax { AttributeLists.Count: > 0 };
}
private static ClassDeclarationSyntax? GetSemanticTargetForGeneration(
GeneratorSyntaxContext context)
{
var classDeclaration = (ClassDeclarationSyntax)context.Node;
foreach (var attributeList in classDeclaration.AttributeLists)
{
foreach (var attribute attributeList.Attributes)
{
symbol = context.SemanticModel.GetSymbolInfo(attribute).Symbol;
(symbol?.ContainingType.Name == )
{
classDeclaration;
}
}
}
;
}
{
(classDeclaration )
;
source = GenerateSource(classDeclaration);
context.AddSource(,
SourceText.From(source, Encoding.UTF8));
}
{
;
}
{
.Empty;
}
}
Source Generator Project Configuration
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
</ItemGroup>
</Project>
Using the Generator
<ItemGroup>
<ProjectReference Include="..\MyGenerator\MyGenerator.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
</ItemGroup>
Versioning and Compatibility
Semantic Versioning
<PropertyGroup>
<Version>2.1.3</Version>
<AssemblyVersion>2.0.0.0</AssemblyVersion>
<FileVersion>2.1.3.42</FileVersion>
</PropertyGroup>
Package Version from Git
<PropertyGroup>
<MinVerTagPrefix>v</MinVerTagPrefix>
<MinVerDefaultPreReleaseIdentifiers>preview.0</MinVerDefaultPreReleaseIdentifiers>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MinVer" Version="5.0.0" PrivateAssets="all" />
</ItemGroup>
API Compatibility Analysis
<PropertyGroup>
<GenerateCompatibilitySuppressionFile>true</GenerateCompatibilitySuppressionFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.DotNet.ApiCompat.Task" Version="8.0.100" PrivateAssets="all" />
</ItemGroup>
Publishing to NuGet
Pre-Publish Checklist
Publishing Commands
dotnet pack -c Release
dotnet nuget push bin/Release/*.nupkg \
--api-key YOUR_API_KEY \
--source https://api.nuget.org/v3/index.json
dotnet nuget push bin/Release/*.nupkg \
--api-key YOUR_GITHUB_TOKEN \
--source https://nuget.pkg.github.com/USERNAME/index.json
Package Validation
<PropertyGroup>
<EnablePackageValidation>true</EnablePackageValidation>
<PackageValidationBaselineVersion>1.0.0</PackageValidationBaselineVersion>
</PropertyGroup>
Common Dependencies
Serialization
<ItemGroup>
<PackageReference Include="System.Text.Json" Version="8.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
HTTP Client
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
</ItemGroup>
Dependency Injection
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0" />
</ItemGroup>
Logging
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
</ItemGroup>
Options Pattern
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Options" Version="8.0.0" />
</ItemGroup>
Anti-Patterns
1. Exposing Internal Types
public Dictionary<string, List<InternalType>> GetData();
public DataCollection GetData();
2. Breaking Binary Compatibility
public void Process(string input) { }
public void Process(string input, int count) { }
public void Process(string input) { }
public void Process(string input, int count) { Process(input); }
3. Missing Nullability Annotations
public string? GetName() => null;
[return: NotNullIfNotNull(nameof(defaultName))]
public string? GetName(string? defaultName = null)
{
return _name ?? defaultName;
}
4. Synchronous API Over Async
public Result Process()
{
return ProcessAsync().GetAwaiter().GetResult();
}
public Result Process() { }
public Task<Result> ProcessAsync(CancellationToken ct = default) { }
Best Practices
Use Nullable Reference Types
<PropertyGroup>
<Nullable>enable</Nullable>
</PropertyGroup>
Enable All Warnings
<PropertyGroup>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<WarningLevel>9999</WarningLevel>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
</PropertyGroup>
Use EditorConfig
root = true
[*.cs]
dotnet_naming_rule.interfaces_should_be_prefixed_with_i.severity = warning
dotnet_naming_rule.interfaces_should_be_prefixed_with_i.symbols = interface
dotnet_naming_rule.interfaces_should_be_prefixed_with_i.style = begins_with_i
csharp_prefer_braces = true:warning
csharp_using_directive_placement = outside_namespace:warning
Analyzer Packages
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="8.0.0" PrivateAssets="all" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556" PrivateAssets="all" />
<PackageReference Include="Roslynator.Analyzers" Version="4.7.0" PrivateAssets="all" />
</ItemGroup>
References