{
  "schemaVersion": "1.0",
  "item": {
    "slug": "dotnet-expert",
    "name": "Dotnet Expert",
    "source": "tencent",
    "type": "skill",
    "category": "开发工具",
    "sourceUrl": "https://clawhub.ai/jgarrison929/dotnet-expert",
    "canonicalUrl": "https://clawhub.ai/jgarrison929/dotnet-expert",
    "targetPlatform": "OpenClaw"
  },
  "install": {
    "downloadMode": "redirect",
    "downloadUrl": "/downloads/dotnet-expert",
    "sourceDownloadUrl": "https://wry-manatee-359.convex.site/api/v1/download?slug=dotnet-expert",
    "sourcePlatform": "tencent",
    "targetPlatform": "OpenClaw",
    "installMethod": "Manual import",
    "extraction": "Extract archive",
    "prerequisites": [
      "OpenClaw"
    ],
    "packageFormat": "ZIP package",
    "includedAssets": [
      "SKILL.md"
    ],
    "primaryDoc": "SKILL.md",
    "quickSetup": [
      "Download the package from Yavira.",
      "Extract the archive and review SKILL.md first.",
      "Import or place the package into your OpenClaw setup."
    ],
    "agentAssist": {
      "summary": "Hand the extracted package to your coding agent with a concrete install brief instead of figuring it out manually.",
      "steps": [
        "Download the package from Yavira.",
        "Extract it into a folder your agent can access.",
        "Paste one of the prompts below and point your agent at the extracted folder."
      ],
      "prompts": [
        {
          "label": "New install",
          "body": "I downloaded a skill package from Yavira. Read SKILL.md from the extracted folder and install it by following the included instructions. Tell me what you changed and call out any manual steps you could not complete."
        },
        {
          "label": "Upgrade existing",
          "body": "I downloaded an updated skill package from Yavira. Read SKILL.md from the extracted folder, compare it with my current installation, and upgrade it while preserving any custom configuration unless the package docs explicitly say otherwise. Summarize what changed and any follow-up checks I should run."
        }
      ]
    },
    "sourceHealth": {
      "source": "tencent",
      "status": "healthy",
      "reason": "direct_download_ok",
      "recommendedAction": "download",
      "checkedAt": "2026-04-30T16:55:25.780Z",
      "expiresAt": "2026-05-07T16:55:25.780Z",
      "httpStatus": 200,
      "finalUrl": "https://wry-manatee-359.convex.site/api/v1/download?slug=network",
      "contentType": "application/zip",
      "probeMethod": "head",
      "details": {
        "probeUrl": "https://wry-manatee-359.convex.site/api/v1/download?slug=network",
        "contentDisposition": "attachment; filename=\"network-1.0.0.zip\"",
        "redirectLocation": null,
        "bodySnippet": null
      },
      "scope": "source",
      "summary": "Source download looks usable.",
      "detail": "Yavira can redirect you to the upstream package for this source.",
      "primaryActionLabel": "Download for OpenClaw",
      "primaryActionHref": "/downloads/dotnet-expert"
    },
    "validation": {
      "installChecklist": [
        "Use the Yavira download entry.",
        "Review SKILL.md after the package is downloaded.",
        "Confirm the extracted package contains the expected setup assets."
      ],
      "postInstallChecks": [
        "Confirm the extracted package includes the expected docs or setup files.",
        "Validate the skill or prompts are available in your target agent workspace.",
        "Capture any manual follow-up steps the agent could not complete."
      ]
    },
    "downloadPageUrl": "https://openagent3.xyz/downloads/dotnet-expert",
    "agentPageUrl": "https://openagent3.xyz/skills/dotnet-expert/agent",
    "manifestUrl": "https://openagent3.xyz/skills/dotnet-expert/agent.json",
    "briefUrl": "https://openagent3.xyz/skills/dotnet-expert/agent.md"
  },
  "agentAssist": {
    "summary": "Hand the extracted package to your coding agent with a concrete install brief instead of figuring it out manually.",
    "steps": [
      "Download the package from Yavira.",
      "Extract it into a folder your agent can access.",
      "Paste one of the prompts below and point your agent at the extracted folder."
    ],
    "prompts": [
      {
        "label": "New install",
        "body": "I downloaded a skill package from Yavira. Read SKILL.md from the extracted folder and install it by following the included instructions. Tell me what you changed and call out any manual steps you could not complete."
      },
      {
        "label": "Upgrade existing",
        "body": "I downloaded an updated skill package from Yavira. Read SKILL.md from the extracted folder, compare it with my current installation, and upgrade it while preserving any custom configuration unless the package docs explicitly say otherwise. Summarize what changed and any follow-up checks I should run."
      }
    ]
  },
  "documentation": {
    "source": "clawhub",
    "primaryDoc": "SKILL.md",
    "sections": [
      {
        "title": ".NET Expert",
        "body": "Senior .NET 9 / ASP.NET Core specialist with expertise in clean architecture, CQRS, and modular monolith patterns."
      },
      {
        "title": "Role Definition",
        "body": "You are a senior .NET engineer building production-grade APIs with ASP.NET Core, Entity Framework Core 9, MediatR, and FluentValidation. You follow clean architecture principles with a pragmatic approach."
      },
      {
        "title": "Core Principles",
        "body": "Result pattern over exceptions for business logic — exceptions for infrastructure only\nCQRS with MediatR — separate commands (writes) from queries (reads)\nFluentValidation for all input validation in the pipeline\nModular monolith — organized by feature/domain, not by technical layer\nStrongly-typed IDs to prevent primitive obsession\nAsync all the way — never .Result or .Wait()"
      },
      {
        "title": "Project Structure (Modular Monolith)",
        "body": "src/\n├── Api/                          # ASP.NET Core host\n│   ├── Program.cs\n│   ├── appsettings.json\n│   └── Endpoints/                # Minimal API endpoint definitions\n├── Modules/\n│   ├── Users/\n│   │   ├── Users.Core/           # Domain entities, interfaces\n│   │   ├── Users.Application/    # Commands, queries, handlers\n│   │   └── Users.Infrastructure/ # EF Core, external services\n│   ├── Orders/\n│   │   ├── Orders.Core/\n│   │   ├── Orders.Application/\n│   │   └── Orders.Infrastructure/\n│   └── Shared/\n│       ├── Shared.Core/          # Common abstractions\n│       └── Shared.Infrastructure/# Cross-cutting concerns\n└── Tests/\n    ├── Users.Tests/\n    └── Orders.Tests/"
      },
      {
        "title": "Basic Endpoint Group",
        "body": "// Api/Endpoints/UserEndpoints.cs\npublic static class UserEndpoints\n{\n    public static void MapUserEndpoints(this IEndpointRouteBuilder app)\n    {\n        var group = app.MapGroup(\"/api/users\")\n            .WithTags(\"Users\")\n            .RequireAuthorization();\n\n        group.MapGet(\"/\", GetUsers);\n        group.MapGet(\"/{id:guid}\", GetUserById);\n        group.MapPost(\"/\", CreateUser);\n        group.MapPut(\"/{id:guid}\", UpdateUser);\n        group.MapDelete(\"/{id:guid}\", DeleteUser);\n    }\n\n    private static async Task<IResult> GetUsers(\n        [AsParameters] GetUsersQuery query,\n        ISender mediator,\n        CancellationToken ct)\n    {\n        var result = await mediator.Send(query, ct);\n        return result.Match(\n            success => Results.Ok(success),\n            error => Results.Problem(error.ToProblemDetails()));\n    }\n\n    private static async Task<IResult> GetUserById(\n        Guid id,\n        ISender mediator,\n        CancellationToken ct)\n    {\n        var result = await mediator.Send(new GetUserByIdQuery(id), ct);\n        return result.Match(\n            success => Results.Ok(success),\n            error => error.Type == ErrorType.NotFound\n                ? Results.NotFound()\n                : Results.Problem(error.ToProblemDetails()));\n    }\n\n    private static async Task<IResult> CreateUser(\n        CreateUserCommand command,\n        ISender mediator,\n        CancellationToken ct)\n    {\n        var result = await mediator.Send(command, ct);\n        return result.Match(\n            success => Results.Created($\"/api/users/{success.Id}\", success),\n            error => Results.Problem(error.ToProblemDetails()));\n    }\n}"
      },
      {
        "title": "Program.cs Setup",
        "body": "var builder = WebApplication.CreateBuilder(args);\n\n// Add modules\nbuilder.Services.AddUsersModule(builder.Configuration);\nbuilder.Services.AddOrdersModule(builder.Configuration);\n\n// Add shared infrastructure\nbuilder.Services.AddMediatR(cfg =>\n    cfg.RegisterServicesFromAssemblies(\n        typeof(UsersModule).Assembly,\n        typeof(OrdersModule).Assembly));\n\nbuilder.Services.AddValidatorsFromAssemblies(new[]\n{\n    typeof(UsersModule).Assembly,\n    typeof(OrdersModule).Assembly,\n});\n\n// Add validation pipeline behavior\nbuilder.Services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));\n\nbuilder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)\n    .AddJwtBearer(options =>\n    {\n        options.TokenValidationParameters = new TokenValidationParameters\n        {\n            ValidateIssuer = true,\n            ValidateAudience = true,\n            ValidateLifetime = true,\n            ValidateIssuerSigningKey = true,\n            ValidIssuer = builder.Configuration[\"Jwt:Issuer\"],\n            ValidAudience = builder.Configuration[\"Jwt:Audience\"],\n            IssuerSigningKey = new SymmetricSecurityKey(\n                Encoding.UTF8.GetBytes(builder.Configuration[\"Jwt:Key\"]!)),\n        };\n    });\n\nbuilder.Services.AddAuthorization();\n\nvar app = builder.Build();\n\napp.UseAuthentication();\napp.UseAuthorization();\n\napp.MapUserEndpoints();\napp.MapOrderEndpoints();\n\napp.Run();"
      },
      {
        "title": "Result Type",
        "body": "// Shared.Core/Result.cs\npublic sealed class Result<T>\n{\n    public T? Value { get; }\n    public Error? Error { get; }\n    public bool IsSuccess { get; }\n\n    private Result(T value) { Value = value; IsSuccess = true; }\n    private Result(Error error) { Error = error; IsSuccess = false; }\n\n    public static Result<T> Success(T value) => new(value);\n    public static Result<T> Failure(Error error) => new(error);\n\n    public TResult Match<TResult>(\n        Func<T, TResult> onSuccess,\n        Func<Error, TResult> onFailure) =>\n        IsSuccess ? onSuccess(Value!) : onFailure(Error!);\n}\n\npublic sealed record Error(string Code, string Message, ErrorType Type = ErrorType.Failure)\n{\n    public static Error NotFound(string code, string message) => new(code, message, ErrorType.NotFound);\n    public static Error Validation(string code, string message) => new(code, message, ErrorType.Validation);\n    public static Error Conflict(string code, string message) => new(code, message, ErrorType.Conflict);\n    public static Error Forbidden(string code, string message) => new(code, message, ErrorType.Forbidden);\n\n    public ProblemDetails ToProblemDetails() => new()\n    {\n        Title = Code,\n        Detail = Message,\n        Status = Type switch\n        {\n            ErrorType.NotFound => StatusCodes.Status404NotFound,\n            ErrorType.Validation => StatusCodes.Status400BadRequest,\n            ErrorType.Conflict => StatusCodes.Status409Conflict,\n            ErrorType.Forbidden => StatusCodes.Status403Forbidden,\n            _ => StatusCodes.Status500InternalServerError,\n        },\n    };\n}\n\npublic enum ErrorType { Failure, NotFound, Validation, Conflict, Forbidden }"
      },
      {
        "title": "Usage in Handlers",
        "body": "// No exceptions for business logic!\npublic sealed class CreateUserHandler : IRequestHandler<CreateUserCommand, Result<UserResponse>>\n{\n    private readonly AppDbContext _db;\n\n    public CreateUserHandler(AppDbContext db) => _db = db;\n\n    public async Task<Result<UserResponse>> Handle(\n        CreateUserCommand command, CancellationToken ct)\n    {\n        // Business rule validation returns errors, not exceptions\n        var existingUser = await _db.Users\n            .AnyAsync(u => u.Email == command.Email, ct);\n\n        if (existingUser)\n            return Result<UserResponse>.Failure(\n                Error.Conflict(\"User.DuplicateEmail\", \"A user with this email already exists\"));\n\n        var user = new User\n        {\n            Id = Guid.NewGuid(),\n            Email = command.Email,\n            Name = command.Name,\n            CreatedAt = DateTime.UtcNow,\n        };\n\n        _db.Users.Add(user);\n        await _db.SaveChangesAsync(ct);\n\n        return Result<UserResponse>.Success(user.ToResponse());\n    }\n}"
      },
      {
        "title": "Commands (Write Operations)",
        "body": "// Users.Application/Commands/CreateUserCommand.cs\npublic sealed record CreateUserCommand(\n    string Email,\n    string Name,\n    string Password) : IRequest<Result<UserResponse>>;"
      },
      {
        "title": "Queries (Read Operations)",
        "body": "// Users.Application/Queries/GetUsersQuery.cs\npublic sealed record GetUsersQuery(\n    int Page = 1,\n    int PageSize = 20,\n    string? Search = null) : IRequest<Result<PagedResult<UserResponse>>>;\n\npublic sealed class GetUsersHandler : IRequestHandler<GetUsersQuery, Result<PagedResult<UserResponse>>>\n{\n    private readonly AppDbContext _db;\n\n    public GetUsersHandler(AppDbContext db) => _db = db;\n\n    public async Task<Result<PagedResult<UserResponse>>> Handle(\n        GetUsersQuery query, CancellationToken ct)\n    {\n        var dbQuery = _db.Users.AsNoTracking();\n\n        if (!string.IsNullOrWhiteSpace(query.Search))\n            dbQuery = dbQuery.Where(u =>\n                u.Name.Contains(query.Search) || u.Email.Contains(query.Search));\n\n        var total = await dbQuery.CountAsync(ct);\n\n        var users = await dbQuery\n            .OrderBy(u => u.Name)\n            .Skip((query.Page - 1) * query.PageSize)\n            .Take(query.PageSize)\n            .Select(u => u.ToResponse())\n            .ToListAsync(ct);\n\n        return Result<PagedResult<UserResponse>>.Success(\n            new PagedResult<UserResponse>(users, total, query.Page, query.PageSize));\n    }\n}"
      },
      {
        "title": "Validation Pipeline Behavior",
        "body": "public sealed class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>\n    where TRequest : IRequest<TResponse>\n{\n    private readonly IEnumerable<IValidator<TRequest>> _validators;\n\n    public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators)\n        => _validators = validators;\n\n    public async Task<TResponse> Handle(\n        TRequest request,\n        RequestHandlerDelegate<TResponse> next,\n        CancellationToken ct)\n    {\n        if (!_validators.Any()) return await next();\n\n        var context = new ValidationContext<TRequest>(request);\n        var results = await Task.WhenAll(\n            _validators.Select(v => v.ValidateAsync(context, ct)));\n\n        var failures = results\n            .SelectMany(r => r.Errors)\n            .Where(f => f != null)\n            .ToList();\n\n        if (failures.Count > 0)\n            throw new ValidationException(failures);\n\n        return await next();\n    }\n}"
      },
      {
        "title": "FluentValidation",
        "body": "public sealed class CreateUserValidator : AbstractValidator<CreateUserCommand>\n{\n    public CreateUserValidator()\n    {\n        RuleFor(x => x.Email)\n            .NotEmpty().WithMessage(\"Email is required\")\n            .EmailAddress().WithMessage(\"Invalid email format\")\n            .MaximumLength(255);\n\n        RuleFor(x => x.Name)\n            .NotEmpty().WithMessage(\"Name is required\")\n            .MinimumLength(2)\n            .MaximumLength(100)\n            .Matches(@\"^[a-zA-Z\\s'-]+$\").WithMessage(\"Name contains invalid characters\");\n\n        RuleFor(x => x.Password)\n            .NotEmpty()\n            .MinimumLength(8)\n            .Matches(\"[A-Z]\").WithMessage(\"Password must contain uppercase letter\")\n            .Matches(\"[a-z]\").WithMessage(\"Password must contain lowercase letter\")\n            .Matches(\"[0-9]\").WithMessage(\"Password must contain a number\")\n            .Matches(\"[^a-zA-Z0-9]\").WithMessage(\"Password must contain a special character\");\n    }\n}"
      },
      {
        "title": "DbContext",
        "body": "public sealed class AppDbContext : DbContext\n{\n    public DbSet<User> Users => Set<User>();\n    public DbSet<Order> Orders => Set<Order>();\n    public DbSet<OrderItem> OrderItems => Set<OrderItem>();\n\n    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }\n\n    protected override void OnModelCreating(ModelBuilder modelBuilder)\n    {\n        modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly);\n    }\n\n    public override async Task<int> SaveChangesAsync(CancellationToken ct = default)\n    {\n        // Auto-set audit fields\n        foreach (var entry in ChangeTracker.Entries<IAuditable>())\n        {\n            if (entry.State == EntityState.Added)\n                entry.Entity.CreatedAt = DateTime.UtcNow;\n\n            if (entry.State == EntityState.Modified)\n                entry.Entity.UpdatedAt = DateTime.UtcNow;\n        }\n\n        return await base.SaveChangesAsync(ct);\n    }\n}"
      },
      {
        "title": "Entity Configuration",
        "body": "public sealed class UserConfiguration : IEntityTypeConfiguration<User>\n{\n    public void Configure(EntityTypeBuilder<User> builder)\n    {\n        builder.ToTable(\"users\");\n\n        builder.HasKey(u => u.Id);\n\n        builder.Property(u => u.Email)\n            .HasMaxLength(255)\n            .IsRequired();\n\n        builder.HasIndex(u => u.Email).IsUnique();\n\n        builder.Property(u => u.Name)\n            .HasMaxLength(100)\n            .IsRequired();\n\n        builder.Property(u => u.PasswordHash)\n            .HasMaxLength(255)\n            .IsRequired();\n\n        builder.HasMany(u => u.Orders)\n            .WithOne(o => o.User)\n            .HasForeignKey(o => o.UserId)\n            .OnDelete(DeleteBehavior.Cascade);\n\n        // Query filter for soft delete\n        builder.HasQueryFilter(u => u.DeletedAt == null);\n    }\n}"
      },
      {
        "title": "Migrations",
        "body": "# Create migration\ndotnet ef migrations add AddUserTable -p src/Users.Infrastructure -s src/Api\n\n# Apply migration\ndotnet ef database update -p src/Users.Infrastructure -s src/Api\n\n# Generate SQL script (for production)\ndotnet ef migrations script -p src/Users.Infrastructure -s src/Api -o migrations.sql --idempotent"
      },
      {
        "title": "Query Optimization",
        "body": "// ❌ BAD: N+1 queries\nvar users = await _db.Users.ToListAsync(ct);\nforeach (var user in users)\n{\n    var orders = await _db.Orders.Where(o => o.UserId == user.Id).ToListAsync(ct);\n}\n\n// ✅ GOOD: Eager loading\nvar users = await _db.Users\n    .Include(u => u.Orders)\n    .ToListAsync(ct);\n\n// ✅ BEST: Projection (only load what you need)\nvar users = await _db.Users\n    .AsNoTracking()\n    .Select(u => new UserResponse\n    {\n        Id = u.Id,\n        Name = u.Name,\n        Email = u.Email,\n        OrderCount = u.Orders.Count,\n    })\n    .ToListAsync(ct);"
      },
      {
        "title": "Identity Setup",
        "body": "builder.Services.AddIdentity<ApplicationUser, IdentityRole<Guid>>(options =>\n{\n    options.Password.RequireDigit = true;\n    options.Password.RequiredLength = 8;\n    options.Password.RequireUppercase = true;\n    options.Password.RequireNonAlphanumeric = true;\n    options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(15);\n    options.Lockout.MaxFailedAccessAttempts = 5;\n})\n.AddEntityFrameworkStores<AppDbContext>()\n.AddDefaultTokenProviders();"
      },
      {
        "title": "JWT Token Generation",
        "body": "public sealed class TokenService : ITokenService\n{\n    private readonly IConfiguration _config;\n\n    public TokenService(IConfiguration config) => _config = config;\n\n    public string GenerateAccessToken(ApplicationUser user, IList<string> roles)\n    {\n        var claims = new List<Claim>\n        {\n            new(ClaimTypes.NameIdentifier, user.Id.ToString()),\n            new(ClaimTypes.Email, user.Email!),\n            new(ClaimTypes.Name, user.UserName!),\n        };\n\n        claims.AddRange(roles.Select(role => new Claim(ClaimTypes.Role, role)));\n\n        var key = new SymmetricSecurityKey(\n            Encoding.UTF8.GetBytes(_config[\"Jwt:Key\"]!));\n\n        var token = new JwtSecurityToken(\n            issuer: _config[\"Jwt:Issuer\"],\n            audience: _config[\"Jwt:Audience\"],\n            claims: claims,\n            expires: DateTime.UtcNow.AddMinutes(15),\n            signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256));\n\n        return new JwtSecurityTokenHandler().WriteToken(token);\n    }\n\n    public string GenerateRefreshToken()\n    {\n        var randomBytes = new byte[64];\n        using var rng = RandomNumberGenerator.Create();\n        rng.GetBytes(randomBytes);\n        return Convert.ToBase64String(randomBytes);\n    }\n}"
      },
      {
        "title": "Domain Entity Pattern",
        "body": "public sealed class Order : IAuditable\n{\n    public Guid Id { get; private set; }\n    public Guid UserId { get; private set; }\n    public OrderStatus Status { get; private set; }\n    public decimal Total { get; private set; }\n    public DateTime CreatedAt { get; set; }\n    public DateTime? UpdatedAt { get; set; }\n\n    private readonly List<OrderItem> _items = [];\n    public IReadOnlyCollection<OrderItem> Items => _items.AsReadOnly();\n\n    private Order() { } // EF Core\n\n    public static Order Create(Guid userId)\n    {\n        return new Order\n        {\n            Id = Guid.NewGuid(),\n            UserId = userId,\n            Status = OrderStatus.Pending,\n            Total = 0,\n        };\n    }\n\n    public Result<OrderItem> AddItem(Guid productId, int quantity, decimal unitPrice)\n    {\n        if (Status != OrderStatus.Pending)\n            return Result<OrderItem>.Failure(\n                Error.Validation(\"Order.NotPending\", \"Cannot add items to a non-pending order\"));\n\n        if (quantity <= 0)\n            return Result<OrderItem>.Failure(\n                Error.Validation(\"Order.InvalidQuantity\", \"Quantity must be positive\"));\n\n        var item = new OrderItem(Guid.NewGuid(), Id, productId, quantity, unitPrice);\n        _items.Add(item);\n        RecalculateTotal();\n\n        return Result<OrderItem>.Success(item);\n    }\n\n    public Result<bool> Submit()\n    {\n        if (_items.Count == 0)\n            return Result<bool>.Failure(\n                Error.Validation(\"Order.Empty\", \"Cannot submit an empty order\"));\n\n        Status = OrderStatus.Submitted;\n        return Result<bool>.Success(true);\n    }\n\n    private void RecalculateTotal()\n    {\n        Total = _items.Sum(i => i.Quantity * i.UnitPrice);\n    }\n}\n\npublic enum OrderStatus { Pending, Submitted, Processing, Shipped, Delivered, Cancelled }"
      },
      {
        "title": "Anti-Patterns to Avoid",
        "body": "❌ Throwing exceptions for validation/business logic — use Result pattern\n❌ Anemic domain models (entities with only properties) — put behavior in entities\n❌ Fat controllers/endpoints — delegate to MediatR handlers\n❌ .Result or .Wait() on async calls — async all the way\n❌ Returning IQueryable from repositories — materialize queries in the handler\n❌ Using AutoMapper for simple mappings — manual mapping or extension methods\n❌ Catching Exception broadly — catch specific exceptions at infrastructure boundaries\n❌ Hard-coding connection strings — use IConfiguration and environment variables\n❌ Missing CancellationToken — pass it through the entire call chain\n❌ Using DbContext without AsNoTracking() for read queries"
      }
    ],
    "body": ".NET Expert\n\nSenior .NET 9 / ASP.NET Core specialist with expertise in clean architecture, CQRS, and modular monolith patterns.\n\nRole Definition\n\nYou are a senior .NET engineer building production-grade APIs with ASP.NET Core, Entity Framework Core 9, MediatR, and FluentValidation. You follow clean architecture principles with a pragmatic approach.\n\nCore Principles\nResult pattern over exceptions for business logic — exceptions for infrastructure only\nCQRS with MediatR — separate commands (writes) from queries (reads)\nFluentValidation for all input validation in the pipeline\nModular monolith — organized by feature/domain, not by technical layer\nStrongly-typed IDs to prevent primitive obsession\nAsync all the way — never .Result or .Wait()\nProject Structure (Modular Monolith)\nsrc/\n├── Api/                          # ASP.NET Core host\n│   ├── Program.cs\n│   ├── appsettings.json\n│   └── Endpoints/                # Minimal API endpoint definitions\n├── Modules/\n│   ├── Users/\n│   │   ├── Users.Core/           # Domain entities, interfaces\n│   │   ├── Users.Application/    # Commands, queries, handlers\n│   │   └── Users.Infrastructure/ # EF Core, external services\n│   ├── Orders/\n│   │   ├── Orders.Core/\n│   │   ├── Orders.Application/\n│   │   └── Orders.Infrastructure/\n│   └── Shared/\n│       ├── Shared.Core/          # Common abstractions\n│       └── Shared.Infrastructure/# Cross-cutting concerns\n└── Tests/\n    ├── Users.Tests/\n    └── Orders.Tests/\n\nMinimal API Patterns\nBasic Endpoint Group\n// Api/Endpoints/UserEndpoints.cs\npublic static class UserEndpoints\n{\n    public static void MapUserEndpoints(this IEndpointRouteBuilder app)\n    {\n        var group = app.MapGroup(\"/api/users\")\n            .WithTags(\"Users\")\n            .RequireAuthorization();\n\n        group.MapGet(\"/\", GetUsers);\n        group.MapGet(\"/{id:guid}\", GetUserById);\n        group.MapPost(\"/\", CreateUser);\n        group.MapPut(\"/{id:guid}\", UpdateUser);\n        group.MapDelete(\"/{id:guid}\", DeleteUser);\n    }\n\n    private static async Task<IResult> GetUsers(\n        [AsParameters] GetUsersQuery query,\n        ISender mediator,\n        CancellationToken ct)\n    {\n        var result = await mediator.Send(query, ct);\n        return result.Match(\n            success => Results.Ok(success),\n            error => Results.Problem(error.ToProblemDetails()));\n    }\n\n    private static async Task<IResult> GetUserById(\n        Guid id,\n        ISender mediator,\n        CancellationToken ct)\n    {\n        var result = await mediator.Send(new GetUserByIdQuery(id), ct);\n        return result.Match(\n            success => Results.Ok(success),\n            error => error.Type == ErrorType.NotFound\n                ? Results.NotFound()\n                : Results.Problem(error.ToProblemDetails()));\n    }\n\n    private static async Task<IResult> CreateUser(\n        CreateUserCommand command,\n        ISender mediator,\n        CancellationToken ct)\n    {\n        var result = await mediator.Send(command, ct);\n        return result.Match(\n            success => Results.Created($\"/api/users/{success.Id}\", success),\n            error => Results.Problem(error.ToProblemDetails()));\n    }\n}\n\nProgram.cs Setup\nvar builder = WebApplication.CreateBuilder(args);\n\n// Add modules\nbuilder.Services.AddUsersModule(builder.Configuration);\nbuilder.Services.AddOrdersModule(builder.Configuration);\n\n// Add shared infrastructure\nbuilder.Services.AddMediatR(cfg =>\n    cfg.RegisterServicesFromAssemblies(\n        typeof(UsersModule).Assembly,\n        typeof(OrdersModule).Assembly));\n\nbuilder.Services.AddValidatorsFromAssemblies(new[]\n{\n    typeof(UsersModule).Assembly,\n    typeof(OrdersModule).Assembly,\n});\n\n// Add validation pipeline behavior\nbuilder.Services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));\n\nbuilder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)\n    .AddJwtBearer(options =>\n    {\n        options.TokenValidationParameters = new TokenValidationParameters\n        {\n            ValidateIssuer = true,\n            ValidateAudience = true,\n            ValidateLifetime = true,\n            ValidateIssuerSigningKey = true,\n            ValidIssuer = builder.Configuration[\"Jwt:Issuer\"],\n            ValidAudience = builder.Configuration[\"Jwt:Audience\"],\n            IssuerSigningKey = new SymmetricSecurityKey(\n                Encoding.UTF8.GetBytes(builder.Configuration[\"Jwt:Key\"]!)),\n        };\n    });\n\nbuilder.Services.AddAuthorization();\n\nvar app = builder.Build();\n\napp.UseAuthentication();\napp.UseAuthorization();\n\napp.MapUserEndpoints();\napp.MapOrderEndpoints();\n\napp.Run();\n\nResult Pattern\nResult Type\n// Shared.Core/Result.cs\npublic sealed class Result<T>\n{\n    public T? Value { get; }\n    public Error? Error { get; }\n    public bool IsSuccess { get; }\n\n    private Result(T value) { Value = value; IsSuccess = true; }\n    private Result(Error error) { Error = error; IsSuccess = false; }\n\n    public static Result<T> Success(T value) => new(value);\n    public static Result<T> Failure(Error error) => new(error);\n\n    public TResult Match<TResult>(\n        Func<T, TResult> onSuccess,\n        Func<Error, TResult> onFailure) =>\n        IsSuccess ? onSuccess(Value!) : onFailure(Error!);\n}\n\npublic sealed record Error(string Code, string Message, ErrorType Type = ErrorType.Failure)\n{\n    public static Error NotFound(string code, string message) => new(code, message, ErrorType.NotFound);\n    public static Error Validation(string code, string message) => new(code, message, ErrorType.Validation);\n    public static Error Conflict(string code, string message) => new(code, message, ErrorType.Conflict);\n    public static Error Forbidden(string code, string message) => new(code, message, ErrorType.Forbidden);\n\n    public ProblemDetails ToProblemDetails() => new()\n    {\n        Title = Code,\n        Detail = Message,\n        Status = Type switch\n        {\n            ErrorType.NotFound => StatusCodes.Status404NotFound,\n            ErrorType.Validation => StatusCodes.Status400BadRequest,\n            ErrorType.Conflict => StatusCodes.Status409Conflict,\n            ErrorType.Forbidden => StatusCodes.Status403Forbidden,\n            _ => StatusCodes.Status500InternalServerError,\n        },\n    };\n}\n\npublic enum ErrorType { Failure, NotFound, Validation, Conflict, Forbidden }\n\nUsage in Handlers\n// No exceptions for business logic!\npublic sealed class CreateUserHandler : IRequestHandler<CreateUserCommand, Result<UserResponse>>\n{\n    private readonly AppDbContext _db;\n\n    public CreateUserHandler(AppDbContext db) => _db = db;\n\n    public async Task<Result<UserResponse>> Handle(\n        CreateUserCommand command, CancellationToken ct)\n    {\n        // Business rule validation returns errors, not exceptions\n        var existingUser = await _db.Users\n            .AnyAsync(u => u.Email == command.Email, ct);\n\n        if (existingUser)\n            return Result<UserResponse>.Failure(\n                Error.Conflict(\"User.DuplicateEmail\", \"A user with this email already exists\"));\n\n        var user = new User\n        {\n            Id = Guid.NewGuid(),\n            Email = command.Email,\n            Name = command.Name,\n            CreatedAt = DateTime.UtcNow,\n        };\n\n        _db.Users.Add(user);\n        await _db.SaveChangesAsync(ct);\n\n        return Result<UserResponse>.Success(user.ToResponse());\n    }\n}\n\nMediatR CQRS\nCommands (Write Operations)\n// Users.Application/Commands/CreateUserCommand.cs\npublic sealed record CreateUserCommand(\n    string Email,\n    string Name,\n    string Password) : IRequest<Result<UserResponse>>;\n\nQueries (Read Operations)\n// Users.Application/Queries/GetUsersQuery.cs\npublic sealed record GetUsersQuery(\n    int Page = 1,\n    int PageSize = 20,\n    string? Search = null) : IRequest<Result<PagedResult<UserResponse>>>;\n\npublic sealed class GetUsersHandler : IRequestHandler<GetUsersQuery, Result<PagedResult<UserResponse>>>\n{\n    private readonly AppDbContext _db;\n\n    public GetUsersHandler(AppDbContext db) => _db = db;\n\n    public async Task<Result<PagedResult<UserResponse>>> Handle(\n        GetUsersQuery query, CancellationToken ct)\n    {\n        var dbQuery = _db.Users.AsNoTracking();\n\n        if (!string.IsNullOrWhiteSpace(query.Search))\n            dbQuery = dbQuery.Where(u =>\n                u.Name.Contains(query.Search) || u.Email.Contains(query.Search));\n\n        var total = await dbQuery.CountAsync(ct);\n\n        var users = await dbQuery\n            .OrderBy(u => u.Name)\n            .Skip((query.Page - 1) * query.PageSize)\n            .Take(query.PageSize)\n            .Select(u => u.ToResponse())\n            .ToListAsync(ct);\n\n        return Result<PagedResult<UserResponse>>.Success(\n            new PagedResult<UserResponse>(users, total, query.Page, query.PageSize));\n    }\n}\n\nValidation Pipeline Behavior\npublic sealed class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>\n    where TRequest : IRequest<TResponse>\n{\n    private readonly IEnumerable<IValidator<TRequest>> _validators;\n\n    public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators)\n        => _validators = validators;\n\n    public async Task<TResponse> Handle(\n        TRequest request,\n        RequestHandlerDelegate<TResponse> next,\n        CancellationToken ct)\n    {\n        if (!_validators.Any()) return await next();\n\n        var context = new ValidationContext<TRequest>(request);\n        var results = await Task.WhenAll(\n            _validators.Select(v => v.ValidateAsync(context, ct)));\n\n        var failures = results\n            .SelectMany(r => r.Errors)\n            .Where(f => f != null)\n            .ToList();\n\n        if (failures.Count > 0)\n            throw new ValidationException(failures);\n\n        return await next();\n    }\n}\n\nFluentValidation\npublic sealed class CreateUserValidator : AbstractValidator<CreateUserCommand>\n{\n    public CreateUserValidator()\n    {\n        RuleFor(x => x.Email)\n            .NotEmpty().WithMessage(\"Email is required\")\n            .EmailAddress().WithMessage(\"Invalid email format\")\n            .MaximumLength(255);\n\n        RuleFor(x => x.Name)\n            .NotEmpty().WithMessage(\"Name is required\")\n            .MinimumLength(2)\n            .MaximumLength(100)\n            .Matches(@\"^[a-zA-Z\\s'-]+$\").WithMessage(\"Name contains invalid characters\");\n\n        RuleFor(x => x.Password)\n            .NotEmpty()\n            .MinimumLength(8)\n            .Matches(\"[A-Z]\").WithMessage(\"Password must contain uppercase letter\")\n            .Matches(\"[a-z]\").WithMessage(\"Password must contain lowercase letter\")\n            .Matches(\"[0-9]\").WithMessage(\"Password must contain a number\")\n            .Matches(\"[^a-zA-Z0-9]\").WithMessage(\"Password must contain a special character\");\n    }\n}\n\nEntity Framework Core 9\nDbContext\npublic sealed class AppDbContext : DbContext\n{\n    public DbSet<User> Users => Set<User>();\n    public DbSet<Order> Orders => Set<Order>();\n    public DbSet<OrderItem> OrderItems => Set<OrderItem>();\n\n    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }\n\n    protected override void OnModelCreating(ModelBuilder modelBuilder)\n    {\n        modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly);\n    }\n\n    public override async Task<int> SaveChangesAsync(CancellationToken ct = default)\n    {\n        // Auto-set audit fields\n        foreach (var entry in ChangeTracker.Entries<IAuditable>())\n        {\n            if (entry.State == EntityState.Added)\n                entry.Entity.CreatedAt = DateTime.UtcNow;\n\n            if (entry.State == EntityState.Modified)\n                entry.Entity.UpdatedAt = DateTime.UtcNow;\n        }\n\n        return await base.SaveChangesAsync(ct);\n    }\n}\n\nEntity Configuration\npublic sealed class UserConfiguration : IEntityTypeConfiguration<User>\n{\n    public void Configure(EntityTypeBuilder<User> builder)\n    {\n        builder.ToTable(\"users\");\n\n        builder.HasKey(u => u.Id);\n\n        builder.Property(u => u.Email)\n            .HasMaxLength(255)\n            .IsRequired();\n\n        builder.HasIndex(u => u.Email).IsUnique();\n\n        builder.Property(u => u.Name)\n            .HasMaxLength(100)\n            .IsRequired();\n\n        builder.Property(u => u.PasswordHash)\n            .HasMaxLength(255)\n            .IsRequired();\n\n        builder.HasMany(u => u.Orders)\n            .WithOne(o => o.User)\n            .HasForeignKey(o => o.UserId)\n            .OnDelete(DeleteBehavior.Cascade);\n\n        // Query filter for soft delete\n        builder.HasQueryFilter(u => u.DeletedAt == null);\n    }\n}\n\nMigrations\n# Create migration\ndotnet ef migrations add AddUserTable -p src/Users.Infrastructure -s src/Api\n\n# Apply migration\ndotnet ef database update -p src/Users.Infrastructure -s src/Api\n\n# Generate SQL script (for production)\ndotnet ef migrations script -p src/Users.Infrastructure -s src/Api -o migrations.sql --idempotent\n\nQuery Optimization\n// ❌ BAD: N+1 queries\nvar users = await _db.Users.ToListAsync(ct);\nforeach (var user in users)\n{\n    var orders = await _db.Orders.Where(o => o.UserId == user.Id).ToListAsync(ct);\n}\n\n// ✅ GOOD: Eager loading\nvar users = await _db.Users\n    .Include(u => u.Orders)\n    .ToListAsync(ct);\n\n// ✅ BEST: Projection (only load what you need)\nvar users = await _db.Users\n    .AsNoTracking()\n    .Select(u => new UserResponse\n    {\n        Id = u.Id,\n        Name = u.Name,\n        Email = u.Email,\n        OrderCount = u.Orders.Count,\n    })\n    .ToListAsync(ct);\n\nASP.NET Identity + JWT Auth\nIdentity Setup\nbuilder.Services.AddIdentity<ApplicationUser, IdentityRole<Guid>>(options =>\n{\n    options.Password.RequireDigit = true;\n    options.Password.RequiredLength = 8;\n    options.Password.RequireUppercase = true;\n    options.Password.RequireNonAlphanumeric = true;\n    options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(15);\n    options.Lockout.MaxFailedAccessAttempts = 5;\n})\n.AddEntityFrameworkStores<AppDbContext>()\n.AddDefaultTokenProviders();\n\nJWT Token Generation\npublic sealed class TokenService : ITokenService\n{\n    private readonly IConfiguration _config;\n\n    public TokenService(IConfiguration config) => _config = config;\n\n    public string GenerateAccessToken(ApplicationUser user, IList<string> roles)\n    {\n        var claims = new List<Claim>\n        {\n            new(ClaimTypes.NameIdentifier, user.Id.ToString()),\n            new(ClaimTypes.Email, user.Email!),\n            new(ClaimTypes.Name, user.UserName!),\n        };\n\n        claims.AddRange(roles.Select(role => new Claim(ClaimTypes.Role, role)));\n\n        var key = new SymmetricSecurityKey(\n            Encoding.UTF8.GetBytes(_config[\"Jwt:Key\"]!));\n\n        var token = new JwtSecurityToken(\n            issuer: _config[\"Jwt:Issuer\"],\n            audience: _config[\"Jwt:Audience\"],\n            claims: claims,\n            expires: DateTime.UtcNow.AddMinutes(15),\n            signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256));\n\n        return new JwtSecurityTokenHandler().WriteToken(token);\n    }\n\n    public string GenerateRefreshToken()\n    {\n        var randomBytes = new byte[64];\n        using var rng = RandomNumberGenerator.Create();\n        rng.GetBytes(randomBytes);\n        return Convert.ToBase64String(randomBytes);\n    }\n}\n\nDomain Entity Pattern\npublic sealed class Order : IAuditable\n{\n    public Guid Id { get; private set; }\n    public Guid UserId { get; private set; }\n    public OrderStatus Status { get; private set; }\n    public decimal Total { get; private set; }\n    public DateTime CreatedAt { get; set; }\n    public DateTime? UpdatedAt { get; set; }\n\n    private readonly List<OrderItem> _items = [];\n    public IReadOnlyCollection<OrderItem> Items => _items.AsReadOnly();\n\n    private Order() { } // EF Core\n\n    public static Order Create(Guid userId)\n    {\n        return new Order\n        {\n            Id = Guid.NewGuid(),\n            UserId = userId,\n            Status = OrderStatus.Pending,\n            Total = 0,\n        };\n    }\n\n    public Result<OrderItem> AddItem(Guid productId, int quantity, decimal unitPrice)\n    {\n        if (Status != OrderStatus.Pending)\n            return Result<OrderItem>.Failure(\n                Error.Validation(\"Order.NotPending\", \"Cannot add items to a non-pending order\"));\n\n        if (quantity <= 0)\n            return Result<OrderItem>.Failure(\n                Error.Validation(\"Order.InvalidQuantity\", \"Quantity must be positive\"));\n\n        var item = new OrderItem(Guid.NewGuid(), Id, productId, quantity, unitPrice);\n        _items.Add(item);\n        RecalculateTotal();\n\n        return Result<OrderItem>.Success(item);\n    }\n\n    public Result<bool> Submit()\n    {\n        if (_items.Count == 0)\n            return Result<bool>.Failure(\n                Error.Validation(\"Order.Empty\", \"Cannot submit an empty order\"));\n\n        Status = OrderStatus.Submitted;\n        return Result<bool>.Success(true);\n    }\n\n    private void RecalculateTotal()\n    {\n        Total = _items.Sum(i => i.Quantity * i.UnitPrice);\n    }\n}\n\npublic enum OrderStatus { Pending, Submitted, Processing, Shipped, Delivered, Cancelled }\n\nAnti-Patterns to Avoid\n❌ Throwing exceptions for validation/business logic — use Result pattern\n❌ Anemic domain models (entities with only properties) — put behavior in entities\n❌ Fat controllers/endpoints — delegate to MediatR handlers\n❌ .Result or .Wait() on async calls — async all the way\n❌ Returning IQueryable from repositories — materialize queries in the handler\n❌ Using AutoMapper for simple mappings — manual mapping or extension methods\n❌ Catching Exception broadly — catch specific exceptions at infrastructure boundaries\n❌ Hard-coding connection strings — use IConfiguration and environment variables\n❌ Missing CancellationToken — pass it through the entire call chain\n❌ Using DbContext without AsNoTracking() for read queries"
  },
  "trust": {
    "sourceLabel": "tencent",
    "provenanceUrl": "https://clawhub.ai/jgarrison929/dotnet-expert",
    "publisherUrl": "https://clawhub.ai/jgarrison929/dotnet-expert",
    "owner": "jgarrison929",
    "version": "1.0.0",
    "license": null,
    "verificationStatus": "Indexed source record"
  },
  "links": {
    "detailUrl": "https://openagent3.xyz/skills/dotnet-expert",
    "downloadUrl": "https://openagent3.xyz/downloads/dotnet-expert",
    "agentUrl": "https://openagent3.xyz/skills/dotnet-expert/agent",
    "manifestUrl": "https://openagent3.xyz/skills/dotnet-expert/agent.json",
    "briefUrl": "https://openagent3.xyz/skills/dotnet-expert/agent.md"
  }
}