Install
$ agentstack add skill-softspark-ai-toolkit-csharp-patterns ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ● Filesystem access Used
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
C# / .NET Patterns
Project Structure
Solution Layout
MyApp.sln
Directory.Build.props # Shared build properties
Directory.Packages.props # Central package management
src/
MyApp.Api/ # ASP.NET Core host (Controllers, Middleware, Program.cs)
MyApp.Application/ # Use cases, MediatR handlers, Behaviors
MyApp.Domain/ # Entities, value objects, domain events
MyApp.Infrastructure/ # EF Core, external services
tests/
MyApp.UnitTests/
MyApp.IntegrationTests/
Directory.Build.props
net9.0
enable
enable
true
true
latest-recommended
Central Package Management (Directory.Packages.props)
true
Idioms / Code Style
Nullable Reference Types
public class Order
{
public required string Id { get; init; }
public string? Notes { get; set; } // explicitly nullable
public required Customer Customer { get; init; } // never null
public string Summary => $"Order {Id}: {Notes ?? "no notes"}";
}
Records
public record Money(decimal Amount, string Currency)
{
public Money Add(Money other) => Currency != other.Currency
? throw new InvalidOperationException("Currency mismatch")
: this with { Amount = Amount + other.Amount };
}
public record CreateOrderRequest(string CustomerId, List Lines);
public record OrderLineDto(string ProductId, int Quantity);
Pattern Matching
public decimal CalculateDiscount(Customer c) => c switch
{
{ Tier: CustomerTier.Gold, TotalSpent: > 10_000m } => 0.20m,
{ Tier: CustomerTier.Gold } => 0.15m,
{ Tier: CustomerTier.Silver } => 0.10m,
{ IsNewCustomer: true } => 0.05m,
_ => 0m
};
// List patterns (.NET 8+)
public string Describe(int[] v) => v switch
{
[] => "empty", [var x] => $"one: {x}", [var f, .., var l] => $"{f}..{l}",
};
LINQ
var activeUsers = users
.Where(u => u.IsActive)
.OrderByDescending(u => u.LastLogin)
.Select(u => new UserDto(u.Id, u.Name))
.ToList();
Async/Await
// Always: Async suffix, CancellationToken parameter, never .Result/.Wait()
public async Task GetOrderAsync(string id, CancellationToken ct = default)
=> await _db.Orders.Include(o => o.Lines).FirstOrDefaultAsync(o => o.Id == id, ct);
// Parallel independent tasks
var ordersTask = _orderRepo.GetRecentAsync(ct);
var statsTask = _statsService.ComputeAsync(ct);
await Task.WhenAll(ordersTask, statsTask);
Primary Constructors (C# 12)
public class OrderService(IOrderRepository repo, ILogger logger, IPublisher pub)
{
public async Task CreateAsync(CreateOrderRequest req, CancellationToken ct)
{
logger.LogInformation("Creating order for {CustomerId}", req.CustomerId);
var order = Order.Create(req);
await repo.AddAsync(order, ct);
await pub.Publish(new OrderCreatedEvent(order.Id), ct);
return order;
}
}
Error Handling
Result Pattern
public sealed class Result
{
public T? Value { get; }
public Error? Error { get; }
public bool IsSuccess => Error is null;
private Result(T value) => Value = value;
private Result(Error error) => Error = error;
public static Result Success(T value) => new(value);
public static Result Failure(Error error) => new(error);
public TOut Match(Func ok, Func err) =>
IsSuccess ? ok(Value!) : err(Error!);
}
public record Error(string Code, string Message);
FluentValidation + MediatR Pipeline
public class CreateOrderValidator : AbstractValidator
{
public CreateOrderValidator()
{
RuleFor(x => x.CustomerId).NotEmpty().MaximumLength(36);
RuleForEach(x => x.Lines).ChildRules(line =>
{
line.RuleFor(l => l.ProductId).NotEmpty();
line.RuleFor(l => l.Quantity).GreaterThan(0).LessThanOrEqualTo(1000);
});
}
}
public class ValidationBehavior(IEnumerable> validators)
: IPipelineBehavior where TReq : IRequest
{
public async Task Handle(TReq req, RequestHandlerDelegate next, CancellationToken ct)
{
var failures = validators.Select(v => v.Validate(req)).SelectMany(r => r.Errors).ToList();
return failures.Count > 0 ? throw new ValidationException(failures) : await next();
}
}
IAsyncDisposable
public sealed class TempFileHandle(string path) : IAsyncDisposable
{
public async ValueTask DisposeAsync()
{
if (File.Exists(path)) await Task.Run(() => File.Delete(path));
}
}
// Usage: await using var handle = new TempFileHandle("/tmp/export.csv");
Testing Patterns
xUnit + NSubstitute + FluentAssertions
public class OrderServiceTests
{
private readonly IOrderRepository _repo = Substitute.For();
private readonly IPublisher _pub = Substitute.For();
private readonly OrderService _sut;
public OrderServiceTests() => _sut = new(_repo, Substitute.For>(), _pub);
[Fact]
public async Task CreateAsync_ValidRequest_ReturnsOrder()
{
var order = await _sut.CreateAsync(new("cust-1", [new("prod-1", 2)]), CancellationToken.None);
order.Should().NotBeNull();
order.Lines.Should().ContainSingle().Which.Quantity.Should().Be(2);
await _repo.Received(1).AddAsync(Arg.Any(), Arg.Any());
}
[Theory]
[InlineData(0)]
[InlineData(-1)]
public async Task CreateAsync_InvalidQuantity_Throws(int qty)
{
var act = () => _sut.CreateAsync(new("cust-1", [new("prod-1", qty)]), CancellationToken.None);
await act.Should().ThrowAsync();
}
}
WebApplicationFactory (Integration)
public class OrdersApiTests(WebApplicationFactory factory)
: IClassFixture>
{
[Fact]
public async Task PostOrder_Returns201()
{
var client = factory.WithWebHostBuilder(b => b.ConfigureServices(s =>
{
s.RemoveAll();
s.AddDbContext(o => o.UseInMemoryDatabase("test"));
})).CreateClient();
var response = await client.PostAsJsonAsync("/api/orders",
new { CustomerId = "cust-1", Lines = new[] { new { ProductId = "prod-1", Quantity = 2 } } });
response.StatusCode.Should().Be(HttpStatusCode.Created);
}
}
Testcontainers
public class PostgresFixture : IAsyncLifetime
{
private readonly PostgreSqlContainer _pg = new PostgreSqlBuilder().WithDatabase("testdb").Build();
public string ConnectionString => _pg.GetConnectionString();
public Task InitializeAsync() => _pg.StartAsync();
public Task DisposeAsync() => _pg.DisposeAsync().AsTask();
}
Common Frameworks
ASP.NET Core Minimal API
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(Program).Assembly));
var app = builder.Build();
app.MapGet("/api/orders/{id}", async (string id, ISender sender, CancellationToken ct) =>
await sender.Send(new GetOrderQuery(id), ct) is { } order ? Results.Ok(order) : Results.NotFound());
app.MapPost("/api/orders", async (CreateOrderRequest req, ISender sender, CancellationToken ct) =>
(await sender.Send(new CreateOrderCommand(req), ct)).Match(
o => Results.Created($"/api/orders/{o.Id}", o),
e => Results.Problem(e.Message, statusCode: 400)));
app.Run();
Entity Framework Core
public class AppDbContext(DbContextOptions options) : DbContext(options)
{
public DbSet Orders => Set();
protected override void OnModelCreating(ModelBuilder mb)
{
mb.Entity(b =>
{
b.HasKey(o => o.Id);
b.Property(o => o.Id).HasMaxLength(36);
b.HasMany(o => o.Lines).WithOne().HasForeignKey(l => l.OrderId);
b.HasIndex(o => o.CustomerId);
b.Property(o => o.Total).HasPrecision(18, 2);
});
mb.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly);
}
}
MediatR (CQRS)
public record CreateOrderCommand(CreateOrderRequest Request) : IRequest>;
public class CreateOrderHandler(IOrderRepository repo, IPublisher pub)
: IRequestHandler>
{
public async Task> Handle(CreateOrderCommand cmd, CancellationToken ct)
{
var order = Order.Create(cmd.Request);
await repo.AddAsync(order, ct);
await pub.Publish(new OrderCreatedEvent(order.Id), ct);
return Result.Success(order);
}
}
Polly (Resilience)
builder.Services.AddHttpClient("Payments", c => c.BaseAddress = new Uri("https://api.payments.com"))
.AddResilienceHandler("default", p =>
{
p.AddRetry(new() { MaxRetryAttempts = 3, Delay = TimeSpan.FromMilliseconds(500),
BackoffType = DelayBackoffType.Exponential,
ShouldHandle = new PredicateBuilder()
.HandleResult(r => r.StatusCode >= HttpStatusCode.InternalServerError) });
p.AddCircuitBreaker(new() { FailureRatio = 0.5, SamplingDuration = TimeSpan.FromSeconds(30),
BreakDuration = TimeSpan.FromSeconds(15) });
p.AddTimeout(TimeSpan.FromSeconds(5));
});
MassTransit (Messaging)
builder.Services.AddMassTransit(x =>
{
x.AddConsumer();
x.UsingRabbitMq((ctx, cfg) => { cfg.Host("rabbitmq://localhost"); cfg.ConfigureEndpoints(ctx); });
});
Performance Tips
Span -- Zero-Allocation Parsing
public static ReadOnlySpan ExtractDomain(ReadOnlySpan email)
{
int at = email.IndexOf('@');
return at >= 0 ? email[(at + 1)..] : ReadOnlySpan.Empty;
}
// Stack-allocated buffer: Span buf = stackalloc byte[256];
ValueTask -- Cache-Hit Fast Path
public ValueTask GetProductAsync(string id, CancellationToken ct)
{
if (_cache.TryGetValue(id, out var cached)) return ValueTask.FromResult(cached);
return new ValueTask(LoadFromDbAsync(id, ct));
}
Source Generators
// JSON -- avoids runtime reflection
[JsonSerializable(typeof(Order))]
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
public partial class AppJsonContext : JsonSerializerContext;
// Logging -- avoids boxing
public static partial class Log
{
[LoggerMessage(Level = LogLevel.Information, Message = "Order {OrderId} created")]
public static partial void OrderCreated(ILogger logger, string orderId);
}
BenchmarkDotNet
[MemoryDiagnoser, SimpleJob(RuntimeMoniker.Net90)]
public class ParsingBenchmarks
{
private readonly string _email = "user@example.com";
[Benchmark(Baseline = true)] public string Sub() => _email[(_email.IndexOf('@') + 1)..];
[Benchmark] public ReadOnlySpan Span() => ExtractDomain(_email.AsSpan());
}
// dotnet run -c Release --project Benchmarks
Build / Package Management
dotnet CLI
dotnet new sln -n MyApp && dotnet new webapi -n MyApp.Api -o src/MyApp.Api
dotnet build -c Release
dotnet test -c Release --collect:"XPlat Code Coverage"
dotnet publish src/MyApp.Api -c Release -o ./publish
dotnet ef migrations add Init --project src/MyApp.Infrastructure --startup-project src/MyApp.Api
.editorconfig
[*.cs]
dotnet_sort_system_directives_first = true
csharp_style_namespace_declarations = file_scoped:error
csharp_prefer_primary_constructors = true:suggestion
dotnet_diagnostic.CA1848.severity = warning
CI (GitHub Actions)
steps:
- uses: actions/checkout@v6
- uses: actions/setup-dotnet@v4
with: { dotnet-version: '9.0.x' }
- run: dotnet restore
- run: dotnet build --no-restore -c Release -warnaserror
- run: dotnet test --no-build -c Release --collect:"XPlat Code Coverage"
- run: dotnet publish src/MyApp.Api -c Release -o publish --no-build
Dockerfile
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
WORKDIR /src
COPY . .
RUN dotnet publish src/MyApp.Api -c Release -o /app
FROM mcr.microsoft.com/dotnet/aspnet:9.0
COPY --from=build /app /app
WORKDIR /app
ENTRYPOINT ["dotnet", "MyApp.Api.dll"]
Anti-Patterns
async voidoutside event handlers -- always returnTask- Blocking with
.Result/.Wait()-- deadlock risk - Missing
CancellationTokenpropagation through the call chain - Catching bare
Exceptionwithout re-throw or structured logging - Service locator via
IServiceProvider.GetServicein business logic - Exposing
IQueryablefrom repositories -- leaks persistence details - Public setters on domain entities -- use methods + private setters
- String concatenation in hot paths -- use
StringBuilderor string handlers
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: softspark
- Source: softspark/ai-toolkit
- License: MIT
- Homepage: https://softspark.eu
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.