Token导航 LogoToken导航TokenDH.com
效率需要联网clawhub未标认证来源可访问clear审计通过

dotnet-expert点网专家

Agent Skill

dotnet-expert 用于辅助前端页面、组件、样式和交互逻辑开发,适合在 OpenClaw 中需要维护前端项目、生成组件或检查界面实现时使用。可结合来源仓库、安装命令和原始 README 继续核验具体用法。安装前建议确认权限范围、维护状态,以及是否会触发联网、命令执行或文件读写。

总安装

61,182

周安装

2,575

GitHub Stars

1

下载量

21,424
OpenClaw

安装说明

本站只整理中文说明和来源信息,不托管安装包,也不代用户安装。

GitHub

来源数

2

许可证

MIT-0

最后核验

2026-05-01

来源状态

来源可访问

安装方式

通过对话安装

复制提示词发给支持本地命令或 Skills 的 AI 助手,先确认命令和权限,再让它执行。

请帮我安装这个 Agent Skill:dotnet-expert(点网专家)
来源仓库:https://github.com/jgarrison929/dotnet-expert
安装命令:
openclaw skills install dotnet-expert
安装前请先检查当前环境是否支持对应 CLI,并向我确认将要执行的命令、安装目录、联网范围和文件读写权限;确认后再执行。

命令行安装

复制命令到本机终端执行。该命令会通过 OpenClaw 从第三方来源获取 Skill;本站只展示命令,不托管安装包,也不自动执行。

ClawHubOpenClaw
openclaw skills install dotnet-expert

简介

在构建 .NET 8/9 应用程序、ASP.NET Core API、Entity Framework Core、MediatR CQRS、模块化整体架构、FluentValidation、结果模式、JWT 身份验证或任何 C# 后端开发问题时使用。

SKILL.md

name
dotnet-expert
version
1.0.0
description
Use when building .NET 8/9 applications, ASP.NET Core APIs, Entity Framework Core, MediatR CQRS, modular monolith architecture, FluentValidation, Result pattern, JWT authentication, or any C# backend development question.
triggers
role
specialist
scope
implementation
output-format
code

.NET Expert

Senior .NET 9 / ASP.NET Core specialist with expertise in clean architecture, CQRS, and modular monolith patterns.

Role Definition

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.

Core Principles

  1. Result pattern over exceptions for business logic — exceptions for infrastructure only
  2. CQRS with MediatR — separate commands (writes) from queries (reads)
  3. FluentValidation for all input validation in the pipeline
  4. Modular monolith — organized by feature/domain, not by technical layer
  5. Strongly-typed IDs to prevent primitive obsession
  6. Async all the way — never .Result or .Wait()

Project Structure (Modular Monolith)

src/
├── Api/                          # ASP.NET Core host
│   ├── Program.cs
│   ├── appsettings.json
│   └── Endpoints/                # Minimal API endpoint definitions
├── Modules/
│   ├── Users/
│   │   ├── Users.Core/           # Domain entities, interfaces
│   │   ├── Users.Application/    # Commands, queries, handlers
│   │   └── Users.Infrastructure/ # EF Core, external services
│   ├── Orders/
│   │   ├── Orders.Core/
│   │   ├── Orders.Application/
│   │   └── Orders.Infrastructure/
│   └── Shared/
│       ├── Shared.Core/          # Common abstractions
│       └── Shared.Infrastructure/# Cross-cutting concerns
└── Tests/
    ├── Users.Tests/
    └── Orders.Tests/

Minimal API Patterns

Basic Endpoint Group

// Api/Endpoints/UserEndpoints.cs
public static class UserEndpoints
{
    public static void MapUserEndpoints(this IEndpointRouteBuilder app)
    {
        var group = app.MapGroup("/api/users")
            .WithTags("Users")
            .RequireAuthorization();

        group.MapGet("/", GetUsers);
        group.MapGet("/{id:guid}", GetUserById);
        group.MapPost("/", CreateUser);
        group.MapPut("/{id:guid}", UpdateUser);
        group.MapDelete("/{id:guid}", DeleteUser);
    }

    private static async Task<IResult> GetUsers(
        [AsParameters] GetUsersQuery query,
        ISender mediator,
        CancellationToken ct)
    {
        var result = await mediator.Send(query, ct);
        return result.Match(
            success => Results.Ok(success),
            error => Results.Problem(error.ToProblemDetails()));
    }

    private static async Task<IResult> GetUserById(
        Guid id,
        ISender mediator,
        CancellationToken ct)
    {
        var result = await mediator.Send(new GetUserByIdQuery(id), ct);
        return result.Match(
            success => Results.Ok(success),
            error => error.Type == ErrorType.NotFound
                ? Results.NotFound()
                : Results.Problem(error.ToProblemDetails()));
    }

    private static async Task<IResult> CreateUser(
        CreateUserCommand command,
        ISender mediator,
        CancellationToken ct)
    {
        var result = await mediator.Send(command, ct);
        return result.Match(
            success => Results.Created($"/api/users/{success.Id}", success),
            error => Results.Problem(error.ToProblemDetails()));
    }
}

Program.cs Setup

var builder = WebApplication.CreateBuilder(args);

// Add modules
builder.Services.AddUsersModule(builder.Configuration);
builder.Services.AddOrdersModule(builder.Configuration);

// Add shared infrastructure
builder.Services.AddMediatR(cfg =>
    cfg.RegisterServicesFromAssemblies(
        typeof(UsersModule).Assembly,
        typeof(OrdersModule).Assembly));

builder.Services.AddValidatorsFromAssemblies(new[]
{
    typeof(UsersModule).Assembly,
    typeof(OrdersModule).Assembly,
});

// Add validation pipeline behavior
builder.Services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ValidIssuer = builder.Configuration["Jwt:Issuer"],
            ValidAudience = builder.Configuration["Jwt:Audience"],
            IssuerSigningKey = new SymmetricSecurityKey(
                Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]!)),
        };
    });

builder.Services.AddAuthorization();

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();

app.MapUserEndpoints();
app.MapOrderEndpoints();

app.Run();

Result Pattern

Result Type

// Shared.Core/Result.cs
public sealed class Result<T>
{
    public T? Value { get; }
    public Error? Error { get; }
    public bool IsSuccess { get; }

    private Result(T value) { Value = value; IsSuccess = true; }
    private Result(Error error) { Error = error; IsSuccess = false; }

    public static Result<T> Success(T value) => new(value);
    public static Result<T> Failure(Error error) => new(error);

    public TResult Match<TResult>(
        Func<T, TResult> onSuccess,
        Func<Error, TResult> onFailure) =>
        IsSuccess ? onSuccess(Value!) : onFailure(Error!);
}

public sealed record Error(string Code, string Message, ErrorType Type = ErrorType.Failure)
{
    public static Error NotFound(string code, string message) => new(code, message, ErrorType.NotFound);
    public static Error Validation(string code, string message) => new(code, message, ErrorType.Validation);
    public static Error Conflict(string code, string message) => new(code, message, ErrorType.Conflict);
    public static Error Forbidden(string code, string message) => new(code, message, ErrorType.Forbidden);

    public ProblemDetails ToProblemDetails() => new()
    {
        Title = Code,
        Detail = Message,
        Status = Type switch
        {
            ErrorType.NotFound => StatusCodes.Status404NotFound,
            ErrorType.Validation => StatusCodes.Status400BadRequest,
            ErrorType.Conflict => StatusCodes.Status409Conflict,
            ErrorType.Forbidden => StatusCodes.Status403Forbidden,
            _ => StatusCodes.Status500InternalServerError,
        },
    };
}

public enum ErrorType { Failure, NotFound, Validation, Conflict, Forbidden }

Usage in Handlers

// No exceptions for business logic!
public sealed class CreateUserHandler : IRequestHandler<CreateUserCommand, Result<UserResponse>>
{
    private readonly AppDbContext _db;

    public CreateUserHandler(AppDbContext db) => _db = db;

    public async Task<Result<UserResponse>> Handle(
        CreateUserCommand command, CancellationToken ct)
    {
        // Business rule validation returns errors, not exceptions
        var existingUser = await _db.Users
            .AnyAsync(u => u.Email == command.Email, ct);

        if (existingUser)
            return Result<UserResponse>.Failure(
                Error.Conflict("User.DuplicateEmail", "A user with this email already exists"));

        var user = new User
        {
            Id = Guid.NewGuid(),
            Email = command.Email,
            Name = command.Name,
            CreatedAt = DateTime.UtcNow,
        };

        _db.Users.Add(user);
        await _db.SaveChangesAsync(ct);

        return Result<UserResponse>.Success(user.ToResponse());
    }
}

MediatR CQRS

Commands (Write Operations)

// Users.Application/Commands/CreateUserCommand.cs
public sealed record CreateUserCommand(
    string Email,
    string Name,
    string Password) : IRequest<Result<UserResponse>>;

Queries (Read Operations)

// Users.Application/Queries/GetUsersQuery.cs
public sealed record GetUsersQuery(
    int Page = 1,
    int PageSize = 20,
    string? Search = null) : IRequest<Result<PagedResult<UserResponse>>>;

public sealed class GetUsersHandler : IRequestHandler<GetUsersQuery, Result<PagedResult<UserResponse>>>
{
    private readonly AppDbContext _db;

    public GetUsersHandler(AppDbContext db) => _db = db;

    public async Task<Result<PagedResult<UserResponse>>> Handle(
        GetUsersQuery query, CancellationToken ct)
    {
        var dbQuery = _db.Users.AsNoTracking();

        if (!string.IsNullOrWhiteSpace(query.Search))
            dbQuery = dbQuery.Where(u =>
                u.Name.Contains(query.Search) || u.Email.Contains(query.Search));

        var total = await dbQuery.CountAsync(ct);

        var users = await dbQuery
            .OrderBy(u => u.Name)
            .Skip((query.Page - 1) * query.PageSize)
            .Take(query.PageSize)
            .Select(u => u.ToResponse())
            .ToListAsync(ct);

        return Result<PagedResult<UserResponse>>.Success(
            new PagedResult<UserResponse>(users, total, query.Page, query.PageSize));
    }
}

Validation Pipeline Behavior

public sealed class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
{
    private readonly IEnumerable<IValidator<TRequest>> _validators;

    public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators)
        => _validators = validators;

    public async Task<TResponse> Handle(
        TRequest request,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken ct)
    {
        if (!_validators.Any()) return await next();

        var context = new ValidationContext<TRequest>(request);
        var results = await Task.WhenAll(
            _validators.Select(v => v.ValidateAsync(context, ct)));

        var failures = results
            .SelectMany(r => r.Errors)
            .Where(f => f != null)
            .ToList();

        if (failures.Count > 0)
            throw new ValidationException(failures);

        return await next();
    }
}

FluentValidation

public sealed class CreateUserValidator : AbstractValidator<CreateUserCommand>
{
    public CreateUserValidator()
    {
        RuleFor(x => x.Email)
            .NotEmpty().WithMessage("Email is required")
            .EmailAddress().WithMessage("Invalid email format")
            .MaximumLength(255);

        RuleFor(x => x.Name)
            .NotEmpty().WithMessage("Name is required")
            .MinimumLength(2)
            .MaximumLength(100)
            .Matches(@"^[a-zA-Z\s'-]+$").WithMessage("Name contains invalid characters");

        RuleFor(x => x.Password)
            .NotEmpty()
            .MinimumLength(8)
            .Matches("[A-Z]").WithMessage("Password must contain uppercase letter")
            .Matches("[a-z]").WithMessage("Password must contain lowercase letter")
            .Matches("[0-9]").WithMessage("Password must contain a number")
            .Matches("[^a-zA-Z0-9]").WithMessage("Password must contain a special character");
    }
}

Entity Framework Core 9

DbContext

public sealed class AppDbContext : DbContext
{
    public DbSet<User> Users => Set<User>();
    public DbSet<Order> Orders => Set<Order>();
    public DbSet<OrderItem> OrderItems => Set<OrderItem>();

    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly);
    }

    public override async Task<int> SaveChangesAsync(CancellationToken ct = default)
    {
        // Auto-set audit fields
        foreach (var entry in ChangeTracker.Entries<IAuditable>())
        {
            if (entry.State == EntityState.Added)
                entry.Entity.CreatedAt = DateTime.UtcNow;

            if (entry.State == EntityState.Modified)
                entry.Entity.UpdatedAt = DateTime.UtcNow;
        }

        return await base.SaveChangesAsync(ct);
    }
}

Entity Configuration

public sealed class UserConfiguration : IEntityTypeConfiguration<User>
{
    public void Configure(EntityTypeBuilder<User> builder)
    {
        builder.ToTable("users");

        builder.HasKey(u => u.Id);

        builder.Property(u => u.Email)
            .HasMaxLength(255)
            .IsRequired();

        builder.HasIndex(u => u.Email).IsUnique();

        builder.Property(u => u.Name)
            .HasMaxLength(100)
            .IsRequired();

        builder.Property(u => u.PasswordHash)
            .HasMaxLength(255)
            .IsRequired();

        builder.HasMany(u => u.Orders)
            .WithOne(o => o.User)
            .HasForeignKey(o => o.UserId)
            .OnDelete(DeleteBehavior.Cascade);

        // Query filter for soft delete
        builder.HasQueryFilter(u => u.DeletedAt == null);
    }
}

Migrations

# Create migration
dotnet ef migrations add AddUserTable -p src/Users.Infrastructure -s src/Api

# Apply migration
dotnet ef database update -p src/Users.Infrastructure -s src/Api

# Generate SQL script (for production)
dotnet ef migrations script -p src/Users.Infrastructure -s src/Api -o migrations.sql --idempotent

Query Optimization

// ❌ BAD: N+1 queries
var users = await _db.Users.ToListAsync(ct);
foreach (var user in users)
{
    var orders = await _db.Orders.Where(o => o.UserId == user.Id).ToListAsync(ct);
}

// ✅ GOOD: Eager loading
var users = await _db.Users
    .Include(u => u.Orders)
    .ToListAsync(ct);

// ✅ BEST: Projection (only load what you need)
var users = await _db.Users
    .AsNoTracking()
    .Select(u => new UserResponse
    {
        Id = u.Id,
        Name = u.Name,
        Email = u.Email,
        OrderCount = u.Orders.Count,
    })
    .ToListAsync(ct);

ASP.NET Identity + JWT Auth

Identity Setup

builder.Services.AddIdentity<ApplicationUser, IdentityRole<Guid>>(options =>
{
    options.Password.RequireDigit = true;
    options.Password.RequiredLength = 8;
    options.Password.RequireUppercase = true;
    options.Password.RequireNonAlphanumeric = true;
    options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(15);
    options.Lockout.MaxFailedAccessAttempts = 5;
})
.AddEntityFrameworkStores<AppDbContext>()
.AddDefaultTokenProviders();

JWT Token Generation

public sealed class TokenService : ITokenService
{
    private readonly IConfiguration _config;

    public TokenService(IConfiguration config) => _config = config;

    public string GenerateAccessToken(ApplicationUser user, IList<string> roles)
    {
        var claims = new List<Claim>
        {
            new(ClaimTypes.NameIdentifier, user.Id.ToString()),
            new(ClaimTypes.Email, user.Email!),
            new(ClaimTypes.Name, user.UserName!),
        };

        claims.AddRange(roles.Select(role => new Claim(ClaimTypes.Role, role)));

        var key = new SymmetricSecurityKey(
            Encoding.UTF8.GetBytes(_config["Jwt:Key"]!));

        var token = new JwtSecurityToken(
            issuer: _config["Jwt:Issuer"],
            audience: _config["Jwt:Audience"],
            claims: claims,
            expires: DateTime.UtcNow.AddMinutes(15),
            signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256));

        return new JwtSecurityTokenHandler().WriteToken(token);
    }

    public string GenerateRefreshToken()
    {
        var randomBytes = new byte[64];
        using var rng = RandomNumberGenerator.Create();
        rng.GetBytes(randomBytes);
        return Convert.ToBase64String(randomBytes);
    }
}

Domain Entity Pattern

public sealed class Order : IAuditable
{
    public Guid Id { get; private set; }
    public Guid UserId { get; private set; }
    public OrderStatus Status { get; private set; }
    public decimal Total { get; private set; }
    public DateTime CreatedAt { get; set; }
    public DateTime? UpdatedAt { get; set; }

    private readonly List<OrderItem> _items = [];
    public IReadOnlyCollection<OrderItem> Items => _items.AsReadOnly();

    private Order() { } // EF Core

    public static Order Create(Guid userId)
    {
        return new Order
        {
            Id = Guid.NewGuid(),
            UserId = userId,
            Status = OrderStatus.Pending,
            Total = 0,
        };
    }

    public Result<OrderItem> AddItem(Guid productId, int quantity, decimal unitPrice)
    {
        if (Status != OrderStatus.Pending)
            return Result<OrderItem>.Failure(
                Error.Validation("Order.NotPending", "Cannot add items to a non-pending order"));

        if (quantity <= 0)
            return Result<OrderItem>.Failure(
                Error.Validation("Order.InvalidQuantity", "Quantity must be positive"));

        var item = new OrderItem(Guid.NewGuid(), Id, productId, quantity, unitPrice);
        _items.Add(item);
        RecalculateTotal();

        return Result<OrderItem>.Success(item);
    }

    public Result<bool> Submit()
    {
        if (_items.Count == 0)
            return Result<bool>.Failure(
                Error.Validation("Order.Empty", "Cannot submit an empty order"));

        Status = OrderStatus.Submitted;
        return Result<bool>.Success(true);
    }

    private void RecalculateTotal()
    {
        Total = _items.Sum(i => i.Quantity * i.UnitPrice);
    }
}

public enum OrderStatus { Pending, Submitted, Processing, Shipped, Delivered, Cancelled }

Anti-Patterns to Avoid

  1. ❌ Throwing exceptions for validation/business logic — use Result pattern
  2. ❌ Anemic domain models (entities with only properties) — put behavior in entities
  3. ❌ Fat controllers/endpoints — delegate to MediatR handlers
  4. .Result or .Wait() on async calls — async all the way
  5. ❌ Returning IQueryable from repositories — materialize queries in the handler
  6. ❌ Using AutoMapper for simple mappings — manual mapping or extension methods
  7. ❌ Catching Exception broadly — catch specific exceptions at infrastructure boundaries
  8. ❌ Hard-coding connection strings — use IConfiguration and environment variables
  9. ❌ Missing CancellationToken — pass it through the entire call chain
  10. ❌ Using DbContext without AsNoTracking() for read queries

适合场景

01

OpenClaw 用户查找和安装 Skill 时

02

用户想查找某类 Agent Skill 时

03

需要根据任务场景推荐可安装能力包时

04

需要对比不同来源的安装命令和来源信息时

能力概览

能力 1

按任务关键词查找相关 Skills

能力 2

展示可复制的安装命令

能力 3

保留来源站点、仓库和原始说明,方便继续核验

能力 4

补充不同宿主或平台的使用分布数据

能力 5

展示第三方安全扫描或审计结果

安装后应在对应宿主中按原始 README 的触发条件使用;具体调用方式请以来源页面和 README 为准。

平台分布

OpenClaw

73.44%
按下载量换算15,734

安全审计

VirusTotal

通过

ClawScan

通过

Static analysis

未展示

权限和风险

需要联网

该 Skill 可能需要联网访问来源站点、仓库或外部 API;具体网络访问范围需要结合源码和 README 复核。

安装前确认

本站仅展示第三方公开信息,不托管安装包,不提供自动安装或运行环境。安装前应自行审查源码、依赖和命令行为。当前只有一个来源,正式发布前建议补源仓库或其他目录站核验。

来源信息

继续浏览同类 Skills