Web Service .NET开发实例 从基础概念到实际项目部署的完整开发流程详解

引言:Web Service在现代软件架构中的重要性
Web Service作为一种基于网络的分布式计算技术,已经成为现代软件系统中不可或缺的组成部分。在.NET生态系统中,Web Service的开发经历了从传统的ASMX Web Services到WCF (Windows Communication Foundation),再到现代的ASP.NET Core Web API的演进过程。本文将详细介绍基于.NET平台的Web Service开发,重点聚焦于当前最主流的ASP.NET Core Web API技术栈,并通过一个完整的实际项目案例,展示从基础概念到生产部署的全流程。
第一部分:Web Service基础概念解析
1.1 Web Service的核心概念
Web Service是一种基于HTTP协议的软件系统,它通过标准化的XML或JSON格式进行数据交换,支持机器到机器的交互。在.NET平台上,Web Service主要分为以下几种类型:
ASMX Web Services
(.NET Framework时代的传统技术)
WCF Services
(Windows Communication Foundation,功能强大但配置复杂)
ASP.NET Core Web API
(现代.NET平台的首选,轻量、高性能、跨平台)
1.2 RESTful API设计原则
现代Web Service开发主要遵循RESTful架构风格,其核心原则包括:
资源定位
:使用URI表示资源
无状态通信
:每个请求包含完整信息
统一接口
:使用标准HTTP方法(GET、POST、PUT、DELETE等)
表述性
:数据格式标准化(JSON/XML)
1.3 .NET Web Service技术选型对比
技术栈
适用场景
优点
缺点
ASMX
遗留系统维护
简单易用
功能有限,已过时
WCF
企业级复杂通信
功能全面
配置复杂,学习曲线陡峭
ASP.NET Core Web API
现代Web/移动应用
跨平台、高性能、易维护
需要.NET Core环境
第二部分:开发环境准备
2.1 安装必要的软件
.NET SDK
:下载并安装最新的.NET 8 SDK
IDE
:Visual Studio 2022 或 Visual Studio Code
数据库
:SQL Server Express 或 PostgreSQL
API测试工具
:Postman 或 Swagger UI
2.2 创建第一个Web API项目
# 使用命令行创建新项目
dotnet new webapi -n ProductService –framework net8.0

# 进入项目目录
cd ProductService

# 运行项目
dotnet run
2.3 项目结构解析
ProductService/
├── Controllers/ # API控制器
├── Models/ # 数据模型
├── Services/ # 业务逻辑服务
├── Data/ # 数据访问层
├── wwwroot/ # 静态文件
├── appsettings.json # 配置文件
├── Program.cs # 应用入口
└── ProductService.csproj # 项目文件
第三部分:构建完整的Web Service实例
3.1 项目需求分析
我们将构建一个
产品管理API
,包含以下功能:
产品列表查询
单个产品详情
新增产品
更新产品
删除产品
产品分类管理
3.2 数据模型设计
首先创建数据模型,在
Models
文件夹下:
// Models/Product.cs
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ProductService.Models
{
public class Product
{
[Key]
public int Id { get; set; }

[Required]
[StringLength(100)]
public string Name { get; set; } = string.Empty;

[StringLength(500)]
public string Description { get; set; } = string.Empty;

[Range(0, 1000000)]
[Column(TypeName = “decimal(18,2)”)]
public decimal Price { get; set; }

[Range(0, 10000)]
public int StockQuantity { get; set; }

public bool IsActive { get; set; } = true;

public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime? UpdatedAt { get; set; }

// 外键关联分类
public int CategoryId { get; set; }
public Category? Category { get; set; }
}

public class Category
{
[Key]
public int Id { get; set; }

[Required]
[StringLength(50)]
public string Name { get; set; } = string.Empty;

public ICollection Products { get; set; } = new List();
}
}
3.3 数据库上下文和配置
在
Data
文件夹下创建数据库上下文:
// Data/ApplicationDbContext.cs
using Microsoft.EntityFrameworkCore;
using ProductService.Models;

namespace ProductService.Data
{
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions options)
: base(options)
{
}

public DbSet Products { get; set; }
public DbSet Categories { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// 配置关系
modelBuilder.Entity()
.HasOne(p => p.Category)
.WithMany(c => c.Products)
.HasForeignKey(p => p.CategoryId)
.OnDelete(DeleteBehavior.Restrict);

// 配置索引
modelBuilder.Entity()
.HasIndex(p => p.Name)
.IsUnique();

// 配置默认数据
modelBuilder.Entity().HasData(
new Category { Id = 1, Name = “Electronics” },
new Category { Id = 2, Product = “Books” }
);
}
}
}
3.4 配置数据库连接
在
appsettings.json
中添加数据库连接字符串:
{
“ConnectionStrings”: {
“DefaultConnection”: “Server=(localdb)\\mssqllocaldb;Database=ProductServiceDB;Trusted_Connection=True;MultipleActiveResultSets=true”
},
“Logging”: {
“LogLevel”: {
“Default”: “Information”,
“Microsoft.AspNetCore”: “Warning”
}
},
“AllowedHosts”: “*”
}
在
Program.cs
中配置数据库上下文:
// Program.cs
using Microsoft.EntityFrameworkCore;
using ProductService.Data;
using ProductService.Services;

var builder = WebApplication.CreateBuilder(args);

// 添加数据库上下文
builder.Services.AddDbContext(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString(“DefaultConnection”)));

// 注册服务
builder.Services.AddScoped();
builder.Services.AddScoped();

// 添加控制器服务
builder.Services.AddControllers();

// 添加Swagger(API文档)
builder.Services.AddEndpointsApiExplorer();
builder�ervices.AddSwaggerGen();

var app = builder.Build();

// 配置HTTP请求管道
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}

app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();

app.Run();
3.5 创建服务层(业务逻辑)
在
Services
文件夹下创建服务接口和实现:
// Services/IProductService.cs
using ProductService.Models;

namespace ProductService.Services
{
public interface IProductService
{
Task> GetAllProductsAsync();
Task GetProductByIdAsync(int id);
Task CreateProductAsync(Product product);
Task UpdateProductAsync(int id, Product product);
Task DeleteProductAsync(int id);
Task> GetProductsByCategoryAsync(int categoryId);
}
}

// Services/ProductService.cs
using Microsoft.EntityFrameworkCore;
using ProductService.Data;
using ProductService.Models;

namespace ProductService.Services
{
public class ProductService : IProductService
{
private readonly ApplicationDbContext _context;
private readonly ILogger _logger;

public ProductService(ApplicationDbContext context, ILogger logger)
{
_context = context;
_logger = logger;
}

public async Task> GetAllProductsAsync()
{
_logger.LogInformation(“获取所有产品列表”);
return await _context.Products
.Include(p => p.Category)
.Where(p => p.IsActive)
.OrderBy(p => p.Name)
.ToListAsync();
}

public async Task GetProductByIdAsync(int id)
{
_logger.LogInformation(“获取产品ID: {ProductId}”, id);
return await _context.Products
.Include(p => p.Category)
.FirstOrDefaultAsync(p => p.Id == id && p.IsActive);
}

public async Task CreateProductAsync(Product product)
{
_logger.LogInformation(“创建新产品: {ProductName}”, product.Name);

// 验证分类是否存在
var categoryExists = await _context.Categories
.AnyAsync(c => c.Id == product.CategoryId);

if (!categoryExists)
{
throw new ArgumentException(“指定的分类不存在”);
}

product.CreatedAt = DateTime.UtcNow;
_context.Products.Add(product);
await _context.SaveChangesAsync();

// 重新加载包含导航属性的数据
await _context.Entry(product).Reference(p => p.Category).LoadAsync();

return product;
}

public async Task UpdateProductAsync(int id, Product product)
{
_logger.LogInformation(“更新产品ID: {ProductId}”, id);

if (id != product.Id)
{
return false;
}

var existingProduct = await _context.Products.FindAsync(id);
if (existingProduct == null)
{
return false;
}

// 验证分类是否存在
if (product.CategoryId != existingProduct.CategoryId)
{
var categoryExists = await _context.Categories
.AnyAsync(c => c.Id == product.CategoryId);
if (!categoryExists)
{
throw new ArgumentException(“指定的分类不存在”);
}
}

existingProduct.Name = product.Name;
existingProduct.Description = product.Description;
existingProduct.Price = product.Price;
existingProduct.StockQuantity = product.StockQuantity;
existingProduct.CategoryId = product.CategoryId;
existingProduct.UpdatedAt = DateTime.UtcNow;

try
{
await _context.SaveChangesAsync();
return true;
}
catch (DbUpdateConcurrencyException)
{
_logger.LogWarning(“并发冲突发生在产品更新: {ProductId}”, id);
return false;
}
}

public async Task DeleteProductAsync(int id)
{
_logger.LogInformation(“删除产品ID: {ProductId}”, id);
var product = await _context.Products.FindAsync(id);
if (product == null)
{
return false;
}

// 软删除
product.IsActive = false;
product.UpdatedAt = DateTime.UtcNow;

await _context.SaveChangesAsync();
return true;
}

public async Task> GetProductsByCategoryAsync(int categoryId)
{
_logger.LogInformation(“获取分类ID: {CategoryId} 的产品”, categoryId);
return await _context.Products
.Include(p => p.Category)
.Where(p => p.IsActive && p.CategoryId == categoryId)
.OrderBy(p => p.Name)
.ToListAsync();
}
}
}
3.6 创建API控制器
在
Controllers
文件夹下创建控制器:
// Controllers/ProductsController.cs
using Microsoft.AspNetCore.Mvc;
using ProductService.Models;
using ProductService.Services;

namespace ProductService.Controllers
{
[ApiController]
[Route(“api/[controller]”)]
public class ProductsController : ControllerBase
{
private readonly IProductService _productService;
private readonly ILogger _logger;

public ProductsController(IProductService productService, ILogger logger)
{
_productService = productService;
_logger = logger;
}

// GET: api/products
[HttpGet]
public async Task>> GetProducts()
{
try
{
var products = await _productService.GetAllProductsAsync();
return Ok(products);
}
catch (Exception ex)
{
_logger.LogError(ex, “获取产品列表时发生错误”);
return StatusCode(500, “内部服务器错误”);
}
}

// GET: api/products/5
[HttpGet(“{id}”)]
public async Task> GetProduct(int id)
{
try
{
var product = await _productService.GetProductByIdAsync(id);
if (product == null)
{
return NotFound($”ID为{id}的产品未找到”);
}
return Ok(product);
}
catch (Exception ex)
{
_logger.LogError(ex, “获取产品详情时发生错误”);
return StatusCode(500, “内部服务器错误”);
}
}

// POST: api/products
[HttpPost]
public async Task> PostProduct(Product product)
{
try
{
var createdProduct = await _productService.CreateProductAsync(product);
return CreatedAtAction(nameof(GetProduct), new { id = createdProduct.Id }, createdProduct);
}
catch (ArgumentException ex)
{
return BadRequest(ex.Message);
}
catch (Exception ex)
{
_logger.LogError(ex, “创建产品时发生错误”);
return StatusCode(500, “内部服务器错误”);
}
}

// PUT: api/products/5
[HttpPut(“{id}”)]
public async Task PutProduct(int id, Product product)
{
try
{
var success = await _productService.UpdateProductAsync(id, product);
if (!success)
{
return NotFound($”ID为{id}的产品未找到或更新失败”);
}
return NoContent();
}
catch (ArgumentException ex)
{
return BadRequest(ex.Message);
}
catch (Exception ex)
{
_logger.LogError(ex, “更新产品时发生错误”);
return StatusCode(500, “内部服务器错误”);
}
}

// DELETE: api/products/5
[HttpDelete(“{id}”)]
public async Task DeleteProduct(int id)
{
try
{
var success = await _productService.DeleteProductAsync(id);
if (!success)
{
return NotFound($”ID为{id}的产品未找到”);
}
return NoContent();
}
catch (Exception ex)
{
_logger.LogError(ex, “删除产品时发生错误”);
return StatusCode(500, “内部服务器错误”);
}
}

// GET: api/products/category/2
[HttpGet(“category/{categoryId}”)]
public async Task>> GetProductsByCategory(int categoryId)
{
try
{
var products = await _productService.GetProductsByCategoryAsync(categoryId);
return Ok(products);
}
catch (Exception ex)
{
_logger.LogError(ex, “按分类获取产品时发生错误”);
return StatusCode(500, “内部服务器错误”);
}
}
}
}
3.7 创建分类控制器
// Controllers/CategoriesController.cs
using Microsoft.AspNetCore.Mvc;
using ProductService.Models;
using ProductService.Services;

namespace ProductService.Controllers
{
[ApiController]
[Route(“api/[controller]”)]
public class CategoriesController : ControllerBase
{
private readonly ICategoryService _categoryService;
private readonly ILogger _logger;

public CategoriesController(ICategoryService categoryService, ILogger logger)
{
_categoryService = categoryService;
_logger = logger;
}

// GET: api/categories
[HttpGet]
public async Task>> GetCategories()
{
try
管理
var categories = await _categoryService.GetAllCategoriesAsync();
return Ok(categories);
catch (Exception ex)
{
_logger.LogError(ex, “获取分类列表时发生错误”);
return StatusCode(500, “内部服务器错误”);
}
}

// GET: api/categories/5
[HttpGet(“{id}”)]
public async Task> GetCategory(int id)
{
try
{
var category = await _categoryService.GetCategoryByIdAsync(id);
if (category == null)
{
return NotFound($”ID为{id}的分类未找到”);
}
return Ok(category);
}
catch (Exception ex)
{
_logger.LogError(ex, “获取分类详情时发生错误”);
return StatusCode(500, “内部服务器错误”);
}
}

// POST: api/categories
[HttpPost]
public async Task> PostCategory(Category category)
{
try
{
var createdCategory = await _categoryService.CreateCategoryAsync(category);
return CreatedAtAction(nameof(GetCategory), new { id = createdCategory.Id }, createdCategory);
}
catch (ArgumentException ex)
{
return BadRequest(ex.Message);
}
catch (Exception ex)
{
_logger.LogError(ex, “创建分类时发生错误”);
return StatusCode(500, “内部服务器错误”);
}
}
}
}
3.8 完整的服务实现
// Services/ICategoryService.cs
using ProductService.Models;

namespace ProductService.Services
{
public interface ICategoryService
{
Task> GetAllCategoriesAsync();
Task GetCategoryByIdAsync(int id);
Task CreateCategoryAsync(Category category);
}

public class CategoryService : ICategoryService
{
private readonly ApplicationDbContext _context;
private readonly ILogger _logger;

public CategoryService(ApplicationDbContext context, ILogger logger)
{
_context = context;
_logger = logger;
}

public async Task> GetAllCategoriesAsync()
{
_logger.LogInformation(“获取所有分类”);
return await _context.Categories
.Include(c => c.Products)
.OrderBy(c => c.Name)
.ToListAsync();
}

public async Task GetCategoryByIdAsync(int id)
{
_logger.LogInformation(“获取分类ID: {CategoryId}”, id);
return await _context.Categories
.Include(c => c.Products)
.FirstOrDefaultAsync(c => c.Id == id);
}

public async Task CreateCategoryAsync(Category category)
{
_logger.LogInformation(“创建新分类: {CategoryName}”, category.Name);

// 检查名称是否已存在
var exists = await _context.Categories
.AnyAsync(c => c.Name.ToLower() == category.Name.ToLower());

if (exists)
{
throw new ArgumentException($”分类名称 ‘{category.Name}’ 已存在”);
}

_context.Categories.Add(category);
await _context.SaveChangesAsync();

return category;
}
}
}
第四部分:数据库迁移和初始化
4.1 创建数据库迁移
在项目根目录运行以下命令:
# 添加初始迁移
dotnet ef migrations add InitialCreate

# 应用迁移到数据库
dotnet ef database update
4.2 使用FluentValidation进行数据验证
安装FluentValidation包:
dotnet add package FluentValidation.AspNetCore
创建验证规则:
// Validators/ProductValidator.cs
using FluentValidation;
using ProductService.Models;

namespace ProductService.Validators
{
public class ProductValidator : AbstractValidator
{
public ProductValidator()
{
RuleFor(p => p.Name)
.NotEmpty().WithMessage(“产品名称不能为空”)
.Length(1, 100).WithMessage(“产品名称长度必须在1-100字符之间”);

RuleFor(p => p.Description)
.MaximumLength(500).WithMessage(“产品描述不能超过500字符”);

RuleFor(p => p.Price)
.GreaterThanOrEqualTo(0).WithMessage(“价格不能为负数”)
.LessThanOrEqualTo(1000000).WithMessage(“价格不能超过100万”);

RuleFor(p => p.StockQuantity)
.GreaterThanOrEqualTo(0).WithMessage(“库存不能为负数”);

RuleFor(p => p.CategoryId)
.GreaterThan(0).WithMessage(“必须指定有效的分类ID”);
}
}
}

// Validators/CategoryValidator.cs
using FluentValidation;
using ProductService.Models;

namespace ProductService.Validators
{
public class CategoryValidator : AbstractValidator
{
public CategoryValidator()
{
RuleFor(c => c.Name)
.NotEmpty().WithMessage(“分类名称不能为空”)
.Length(1, 50).WithMessage(“分类名称长度必须在1-50字符之间”);
}
}
}
4.3 在Program.cs中注册验证器
// Program.cs
using FluentValidation.AspNetCore;
using ProductService.Validators;

// … 其他配置

builder.Services.AddFluentValidationAutoValidation()
.AddFluentValidationClientsideAdapters()
.AddValidatorsFromAssemblyContaining();
第五部分:高级功能实现
5.1 全局异常处理中间件
创建全局异常处理中间件:
// Middleware/GlobalExceptionHandlerMiddleware.cs
using System.Text.Json;

namespace ProductService.Middleware
{
public class GlobalExceptionHandlerMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger _logger;

public GlobalExceptionHandlerMiddleware(RequestDelegate next, ILogger logger)
{
_next = next;
_logger = logger;
}

public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception ex)
{
_logger.LogError(ex, “发生未处理的异常”);
await HandleExceptionAsync(context, ex);
}
}

private static Task HandleExceptionAsync(HttpContext context, Exception exception)
{
var statusCode = exception switch
{
ArgumentException => StatusCodes.Status400BadRequest,
UnauthorizedAccessException => StatusCodes.Status401Unauthorized,
KeyNotFoundException => StatusCodes.Status404NotFound,
_ => StatusCodes.Status500InternalServerError
};

context.Response.ContentType = “application/json”;
context.Response.StatusCode = statusCode;

var response = new
{
error = exception.Message,
timestamp = DateTime.UtcNow,
path = context.Request.Path
};

return context.Response.WriteAsync(JsonSerializer.Serialize(response));
}
}

// 扩展方法
public static class MiddlewareExtensions
{
public static IApplicationBuilder UseGlobalExceptionHandler(this IApplicationBuilder builder)
{
return builder.UseMiddleware();
}
}
}
5.2 请求日志中间件
// Middleware/RequestLoggingMiddleware.cs
using System.Diagnostics;

namespace ProductService.Middleware
{
public class RequestLoggingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger _logger;

public RequestLoggingMiddleware(RequestDelegate next, ILogger logger)
{
_next = next;
_logger = logger;
}

public async Task InvokeAsync(HttpContext context)
{
var stopwatch = Stopwatch.StartNew();

// 记录请求信息
_logger.LogInformation(
“收到请求 {Method} {Path} {QueryString}”,
context.Request.Method,
context.Request.Path,
context.Request.QueryString);

await _next(context);

stopwatch.Stop();

// 记录响应信息
_logger.LogInformation(
“请求完成 {Method} {Path} {StatusCode} {ElapsedMs}ms”,
context.Request.Method,
context.Request.Path,
context.Response.StatusCode,
stopwatch.ElapsedMilliseconds);
}
}
}
5.3 JWT身份验证
安装必要的包:
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
创建JWT服务:
// Services/IJwtService.cs
using System.Security.Claims;

namespace ProductService.Services
{
public interface IJwtService
{
string GenerateToken(string username, string role);
ClaimsPrincipal? ValidateToken(string token);
}

public class JwtService : IJwtService
{
private readonly IConfiguration _configuration;

public JwtService(IConfiguration configuration)
{
_configuration = configuration;
}

public string GenerateToken(string username, string role)
{
var claims = new[]
{
new Claim(ClaimTypes.Name, username),
new Claim(ClaimTypes.Role, role),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
};

var key = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(_configuration[“Jwt:Key”] ?? throw new InvalidOperationException(“JWT Key not configured”)));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

var token = new JwtSecurityToken(
issuer: _configuration[“Jwt:Issuer”],
audience: _configuration[“Jwt:Audience”],
claims: claims,
expires: DateTime.Now.AddHours(1),
signingCredentials: creds);

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

public ClaimsPrincipal? ValidateToken(string token)
{
try
{
var tokenHandler = new JwtSecurityTokenHandler();
var validationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = _configuration[“Jwt:Issuer”],
ValidAudience = _configuration[“Jwt:Audience”],
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(_configuration[“Jwt:Key”] ?? throw new InvalidOperationException(“JWT Key not configured”)))
};

var principal = tokenHandler.ValidateToken(token, validationParameters, out _);
return principal;
}
catch
{
return null;
}
}
}
}
5.4 添加JWT认证配置
在
Program.cs
中:
// Program.cs
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;

// … 其他配置

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”] ?? throw new InvalidOperationException(“JWT Key not configured”)))
};
});

builder.Services.AddAuthorization();

// … 在app配置中
app.UseAuthentication();
app.UseAuthorization();
5.5 创建认证控制器
// Controllers/AuthController.cs
using Microsoft.AspNetCore.Mvc;
using ProductService.Services;

namespace ProductService.Controllers
{
[ApiController]
[Route(“api/[controller]”)]
public class AuthController : ControllerBase
{
private readonly IJwtService _jwtService;
private readonly ILogger _logger;

public AuthController(IJwtService jwtService, ILogger logger)
{
_jwtService = jwtService;
_logger = logger;
}

[HttpPost(“login”)]
public IActionResult Login([FromBody] LoginRequest request)
{
// 简化的认证逻辑,实际项目中应该验证数据库
if (request.Username == “admin” && request.Password == “admin123”)
{
var token = _jwtService.GenerateToken(request.Username, “Admin”);
_logger.LogInformation(“用户 {Username} 登录成功”, request.Username);
return Ok(new { token });
}

if (request.Username == “user” && request.Password == “user123”)
{
var token = _jwtService.GenerateToken(request.Username, “User”);
_logger.LogInformation(“用户 {Username} 登录成功”, request.Username);
return Ok(new { token });
}

_logger.LogWarning(“登录失败: {Username}”, request.Username);
return Unauthorized(new { message = “用户名或密码错误” });
}
}

public class LoginRequest
{
public string Username { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
}
}
5.6 保护API端点
在需要认证的控制器或方法上添加授权属性:
[Authorize]
[HttpPost]
public async Task> PostProduct(Product product)
{
// … 原有代码
}

[Authorize(Roles = “Admin”)]
[HttpDelete(“{id}”)]
public async Task DeleteProduct(int id)
{
// … 原有代码
}
第六部分:API文档和测试
6.1 使用Swagger生成API文档
在
Program.cs
中配置Swagger:
// Program.cs
builder.Services.AddSwaggerGen(c =>
{
c.SwaggerDoc(“v1”, new Microsoft.OpenApi.Models.OpenApiInfo
{
Title = “Product Service API”,
Version = “v1”,
Description = “产品管理API文档”,
Contact = new Microsoft.OpenApi.Models.OpenApiContact
{
Name = “开发团队”,
Email = “dev@example.com”
}
});

// 添加JWT认证支持
c.AddSecurityDefinition(“Bearer”, new Microsoft.OpenApi.Models.OpenApiSecurityScheme
{
Description = “JWT Authorization header using the Bearer scheme. Example: \”Authorization: Bearer {token}\””,
Name = “Authorization”,
In = Microsoft.OpenApi.Models.ParameterLocation.Header,
Type = Microsoft.OpenApi.Models.SecuritySchemeType.ApiKey,
Scheme = “Bearer”
});

c.AddSecurityRequirement(new Microsoft.OpenApi.Models.OpenApiSecurityRequirement
{
{
new Microsoft.OpenApi.Models.OpenApiSecurityScheme
{
Reference = new Microsoft.OpenApi.Models.OpenApiReference
{
Type = Microsoft.OpenApi.Models.ReferenceType.SecurityScheme,
Id = “Bearer”
}
},
Array.Empty()
}
});
});
6.2 创建API测试脚本
使用Postman或curl进行测试:
# 1. 获取所有产品(无需认证)
curl -X GET https://localhost:7000/api/products

# 2. 登录获取Token
curl -X POST https://localhost:7000/api/auth/login \
-H “Content-Type: application/json” \
-d ‘{“username”:”admin”,”password”:”admin123″}’

# 3. 使用Token创建产品(需要认证)
curl -X POST https://localhost:7000/api/products \
-H “Authorization: Bearer YOUR_TOKEN_HERE” \
-H “Content-Type: application/json” \
-d ‘{
“name”: “测试产品”,
“description”: “这是一个测试产品”,
“price”: 99.99,
“stockQuantity”: 100,
“categoryId”: 1
}’

# 4. 删除产品(需要Admin角色)
curl -X DELETE https://localhost:7000/api/products/1 \
-H “Authorization: Bearer YOUR_TOKEN_HERE”
第七部分:单元测试和集成测试
7.1 创建单元测试项目
dotnet new xunit -n ProductService.Tests
cd ProductService.Tests
dotnet add reference ../ProductService.csproj
dotnet add package Moq
dotnet add package Microsoft.EntityFrameworkCore.InMemory
7.2 编写单元测试
// Tests/ProductServiceTests.cs
using Microsoft.EntityFrameworkCore;
using Moq;
using ProductService.Data;
using ProductService.Models;
using ProductService.Services;
using Xunit;

namespace ProductService.Tests
{
public class ProductServiceTests
{
private readonly ApplicationDbContext _context;
private readonly Mock> _loggerMock;
private readonly ProductService _service;

public ProductServiceTests()
{
var options = new DbContextOptionsBuilder()
.UseInMemoryDatabase(databaseName: “TestDatabase”)
.Options;

_context = new ApplicationDbContext(options);
_loggerMock = new Mock>();
_service = new ProductService(_context, _loggerMock.Object);

// 初始化测试数据
SeedTestData();
}

private void SeedTestData()
{
_context.Categories.AddRange(
new Category { Id = 1, Name = “Electronics” },
new Category { Id = 2, Name = “Books” }
);

_context.Products.AddRange(
new Product { Id = 1, Name = “Laptop”, Price = 999.99m, CategoryId = 1, IsActive = true },
new Product { Id = 2, Name = “Phone”, Price = 499.99m, CategoryId = 1, IsActive = true },
new Product { Id = 3, Name = “Novel”, Price = 19.99m, CategoryId = 2, IsActive = false } // Inactive
);

_context.SaveChanges();
}

[Fact]
public async Task GetAllProductsAsync_ShouldReturnOnlyActiveProducts()
{
// Act
var result = await _service.GetAllProductsAsync();

// Assert
Assert.Equal(2, result.Count());
Assert.All(result, p => Assert.True(p.IsActive));
}

[Fact]
public async Task GetProductByIdAsync_ShouldReturnProduct_WhenExists()
{
// Act
var result = await _service.GetProductByIdAsync(1);

// Assert
Assert.NotNull(result);
Assert.Equal(“Laptop”, result.Name);
}

[Fact]
public async Task GetProductByIdAsync_ShouldReturnNull_WhenInactive()
{
// Act
var result = await _service.GetProductByIdAsync(3);

// Assert
Assert.Null(result);
}

[Fact]
public async Task CreateProductAsync_ShouldAddProduct_WhenValid()
{
// Arrange
var newProduct = new Product
{
Name = “Tablet”,
Description = “New tablet”,
Price = 299.99m,
StockQuantity = 50,
CategoryId = 1
};

// Act
var result = await _service.CreateProductAsync(newProduct);

// Assert
Assert.NotNull(result);
Assert.True(result.Id > 0);
Assert.Equal(“Tablet”, result.Name);
Assert.NotNull(result.Category);
}

[Fact]
public async Task CreateProductAsync_ShouldThrowException_WhenCategoryNotExists()
{
// Arrange
var invalidProduct = new Product
{
Name = “Invalid Product”,
CategoryId = 999 // Non-existent category
};

// Act & Assert
await Assert.ThrowsAsync(() => _service.CreateProductAsync(invalidProduct));
}

[Fact]
public async Task UpdateProductAsync_ShouldReturnTrue_WhenValid()
{
// Arrange
var product = new Product
{
Id = 1,
Name = “Updated Laptop”,
Description = “Updated description”,
Price = 1099.99m,
StockQuantity = 25,
CategoryId = 1
};

// Act
var result = await _service.UpdateProductAsync(1, product);

// Assert
Assert.True(result);
var updated = await _service.GetProductByIdAsync(1);
Assert.Equal(“Updated Laptop”, updated.Name);
Assert.Equal(1099.99m, updated.Price);
}

[Fact]
public async Task DeleteProductAsync_ShouldSoftDelete_WhenExists()
{
// Act
var result = await _service.DeleteProductAsync(1);

// Assert
Assert.True(result);
var product = await _service.GetProductByIdAsync(1);
Assert.Null(product); // Should not return inactive products

// Verify it’s still in database but inactive
var dbProduct = await _context.Products.FindAsync(1);
Assert.NotNull(dbProduct);
Assert.False(dbProduct.IsActive);
}

[Fact]
public async Task GetProductsByCategoryAsync_ShouldReturnCorrectProducts()
{
// Act
var result = await _service.GetProductsByCategoryAsync(1);

// Assert
Assert.Equal(2, result.Count()); // Laptop and Phone
Assert.All(result, p => Assert.Equal(1, p.CategoryId));
}
}
}
7.3 运行测试
dotnet test
第八部分:性能优化和最佳实践
8.1 数据库查询优化
// 使用AsNoTracking提升只读查询性能
public async Task> GetAllProductsAsync()
{
return await _context.Products
.AsNoTracking() // 只读查询时使用
.Include(p => p.Category)
.Where(p => p.IsActive)
.OrderBy(p => p.Name)
.ToListAsync();
}

// 使用投影减少数据传输
public async Task> GetProductSummariesAsync()
{
return await _context.Products
.Where(p => p.IsActive)
.Select(p => new ProductDto
{
Id = p.Id,
Name = p.Name,
Price = p.Price,
CategoryName = p.Category.Name
})
.ToListAsync();
}
8.2 缓存策略
安装缓存包:
dotnet add package Microsoft.Extensions.Caching.Memory
实现缓存服务:
// Services/CachedProductService.cs
using Microsoft.Extensions.Caching.Memory;
using ProductService.Models;

namespace ProductService.Services
{
public class CachedProductService : IProductService
2. 缓存策略
{
private readonly IProductService _decorated;
private readonly IMemoryCache _cache;
private readonly ILogger _logger;
private static readonly string CacheKeyPrefix = “Product_”;

public CachedProductService(IProductService decorated, IMemoryCache cache, ILogger logger)
{
_decorated = decorated;
_cache = cache;
_logger = logger;
}

public async Task> GetAllProductsAsync()
{
var cacheKey = $”{CacheKeyPrefix}All”;

return await _cache.GetOrCreateAsync(cacheKey, async entry =>
{
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);
_logger.LogInformation(“Cache miss for {CacheKey}, fetching from database”, cacheKey);
return await _decorated.GetAllProductsAsync();
});
}

public async Task GetProductByIdAsync(int id)
{
var cacheKey = $”{CacheKeyPrefix}{id}”;

return await _cache.GetOrCreateAsync(cacheKey, async entry =>
{
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10);
_logger.LogInformation(“Cache miss for {CacheKey}, fetching from database”, cacheKey);
return await _decorated.GetProductByIdAsync(id);
});
}

public async Task CreateProductAsync(Product product)
{
var result = await _decorated.CreateProductAsync(product);
// Invalidate relevant caches
_cache.Remove($”{CacheKeyPrefix}All”);
_logger.LogInformation(“Cache invalidated for product list”);
return result;
}

public async Task UpdateProductAsync(int id, Product product)
{
var result = await _decorated.UpdateProductAsync(id, product);
if (result)
{
_cache.Remove($”{CacheKeyPrefix}{id}”);
_cache.Remove($”{CacheKeyPrefix}All”);
_logger.LogInformation(“Cache invalidated for product {ProductId} and list”, id);
}
return result;
}

public async Task DeleteProductAsync(int id)
{
var result = await _decorated.DeleteProductAsync(id);
if (result)
{
_cache.Remove($”{CacheKeyPrefix}{id}”);
_cache.Remove($”{CacheKeyPrefix}All”);
_logger.LogInformation(“Cache invalidated for product {ProductId} and list”, id);
}
return result;
}

public async Task> GetProductsByCategoryAsync(int categoryId)
{
var cacheKey = $”{CacheKeyPrefix}Category_{categoryId}”;

return await _cache.GetOrCreateAsync(cacheKey, async entry =>
{
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);
_logger.LogInformation(“Cache miss for {CacheKey}, fetching from database”, cacheKey);
return await _decorated.GetProductsByCategoryAsync(categoryId);
});
}
}
}
8.3 在Program.cs中注册缓存服务
// Program.cs
builder.Services.AddMemoryCache();
builder.Services.AddSingleton();

// 替换原有的服务注册
// builder.Services.AddScoped();
builder.Services.AddScoped();
builder.Services.Decorate();
8.4 异步编程最佳实践
// 正确的异步编程模式
public async Task CreateProductAsync(Product product)
{
// 使用ConfigureAwait(false)避免不必要的上下文捕获
var categoryExists = await _context.Categories
.AnyAsync(c => c.Id == product.CategoryId)
.ConfigureAwait(false);

if (!categoryExists)
{
throw new ArgumentException(“指定的分类不存在”);
}

_context.Products.Add(product);

// 异步保存
await _context.SaveChangesAsync().ConfigureAwait(false);

// 异步加载导航属性
await _context.Entry(product)
.Reference(p => p.Category)
.LoadAsync()
.ConfigureAwait(false);

return product;
}
8.5 使用Polly实现弹性策略
安装Polly包:
dotnet add package Microsoft.Extensions.Http.Polly
dotnet add package Polly
实现重试和断路器:
// Services/ExternalApiService.cs
using Polly;
using Polly.Retry;
using System.Net.Http;

namespace ProductService.Services
{
public class ExternalApiService
{
private readonly HttpClient _httpClient;
private readonly AsyncRetryPolicy _retryPolicy;
private readonly AsyncCircuitBreakerPolicy _circuitBreakerPolicy;

public ExternalApiService(HttpClient httpClient)
{
_httpClient = httpClient;

// 重试策略:最多重试3次,每次间隔1秒
_retryPolicy = Policy
.Handle()
.WaitAndRetryAsync(
retryCount: 3,
sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
onRetry: (exception, timespan, retryCount, context) =>
{
Console.WriteLine($”Retry {retryCount} after {timespan.TotalSeconds}s due to: {exception.Message}”);
});

// 断路器策略:连续失败5次后打开电路,30秒后尝试恢复
_circuitBreakerPolicy = Policy
.Handle()
.CircuitBreakerAsync(
exceptionsAllowedBeforeBreaking: 5,
durationOfBreak: TimeSpan.FromSeconds(30),
onBreak: (exception, breakDelay) =>
{
Console.WriteLine($”Circuit broken for {breakDelay.TotalSeconds}s”);
},
onReset: () =>
{
Console.WriteLine(“Circuit reset”);
});
}

public async Task GetDataFromExternalApi()
{
// 组合策略:先重试,如果仍然失败则触发断路器
return await _retryPolicy.ExecuteAsync(async () =>
{
return await _circuitBreakerPolicy.ExecuteAsync(async () =>
{
var response = await _httpClient.GetAsync(“https://api.example.com/data”);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
});
});
}
}
}
第九部分:Docker容器化部署
9.1 创建Dockerfile
在项目根目录创建
Dockerfile
:
# 使用 .NET 8 SDK 作为构建阶段
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src

# 复制项目文件并还原依赖
COPY [“ProductService.csproj”, “./”]
RUN dotnet restore “ProductService.csproj”

# 复制其余文件并构建
COPY . .
RUN dotnet build “ProductService.csproj” -c Release -o /app/build

# 发布阶段
FROM build AS publish
RUN dotnet publish “ProductService.csproj” -c Release -o /app/publish /p:UseAppHost=false

# 运行时阶段
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final
WORKDIR /app
COPY –from=publish /app/publish .

# 暴露端口
EXPOSE 80
EXPOSE 443

# 设置环境变量
ENV ASPNETCORE_ENVIRONMENT=Production
ENV ASPNETCORE_URLS=http://+:80

# 启动应用
ENTRYPOINT [“dotnet”, “ProductService.dll”]
9.2 创建docker-compose.yml
version: ‘3.8’

services:
productservice:
build:
context: .
dockerfile: Dockerfile
container_name: productservice-api
ports:
– “8080:80”
– “8443:443”
environment:
– ASPNETCORE_ENVIRONMENT=Production
– ConnectionStrings__DefaultConnection=Server=sqlserver;Database=ProductServiceDB;User=sa;Password=YourStrong!Passw0rd;TrustServerCertificate=true;
depends_on:
– sqlserver
networks:
– product-network

sqlserver:
image: mcr.microsoft.com/mssql/server:2022-latest
container_name: productservice-db
environment:
– ACCEPT_EULA=Y
– SA_PASSWORD=YourStrong!Passw0rd
– MSSQL_PID=Express
ports:
– “1433:1433”
volumes:
– sqlserver-data:/var/opt/mssql
networks:
– product-network

# 可选:添加Redis缓存
redis:
image: redis:7-alpine
container_name: productservice-redis
ports:
– “6379:6379”
networks:
– product-network

networks:
product-network:
driver: bridge

volumes:
sqlserver-data:
9.3 修改数据库连接以支持Docker
在
appsettings.Production.json
中:
{
“ConnectionStrings”: {
“DefaultConnection”: “Server=sqlserver;Database=ProductServiceDB;User=sa;Password=YourStrong!Passw0rd;TrustServerCertificate=true;”
},
“Logging”: {
“LogLevel”: {
“Default”: “Warning”,
“Microsoft.AspNetCore”: “Warning”
}
}
}
9.4 构建和运行容器
# 构建镜像
docker build -t productservice:1.0 .

# 运行容器
docker run -d -p 8080:80 –name productservice-container productservice:1.0

# 使用docker-compose运行完整环境
docker-compose up -d

# 查看日志
docker logs -f productservice-api

# 停止并删除
docker-compose down
第十部分:CI/CD和生产部署
10.1 GitHub Actions CI/CD 配置
创建
.github/workflows/deploy.yml
:
name: Build, Test and Deploy

on:
push:
branches: [ main ]
pull_request:
branches: [ main ]

jobs:
build-and-test:
runs-on: ubuntu-latest

steps:
– uses: actions/checkout@v3

– name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: ‘8.0.x’

– name: Restore dependencies
run: dotnet restore

– name: Build
run: dotnet build –no-restore –configuration Release

– name: Run tests
run: dotnet test –no-build –verbosity normal –logger trx –results-directory TestResults

– name: Upload test results
uses: actions/upload-artifact@v3
if: always()
with:
name: test-results
path: TestResults

– name: Publish
run: dotnet publish –no-build –configuration Release –output publish

– name: Upload artifact
uses: actions/upload-artifact@v3
with:
name: api-artifact
path: publish

deploy-to-staging:
runs-on: ubuntu-latest
needs: build-and-test
if: github.ref == ‘refs/heads/main’

steps:
– name: Download artifact
uses: actions/download-artifact@v3
with:
name: api-artifact
path: publish

– name: Deploy to Azure Web App
uses: azure/webapps-deploy@v2
with:
app-name: productservice-staging
slot-name: ‘staging’
package: ./publish
publish-profile: ${{ secrets.AZURE_PUBLISH_PROFILE_STAGING }}

– name: Run smoke tests
run: |
sleep 30
curl -f https://productservice-staging.azurewebsites.net/api/health || exit 1

deploy-to-production:
runs-on: ubuntu-latest
needs: deploy-to-staging
if: github.ref == ‘refs/heads/main’

steps:
– name: Download artifact
uses: actions/download-artifact@v3
with:
name: api-artifact
path: publish

– name: Deploy to Production
uses: azure/webapps-deploy@v2
with:
app-name: productservice-prod
slot-name: ‘production’
package: ./publish
publish-profile: ${{ secrets.AZURE_PUBLISH_PROFILE_PROD }}

– name: Notify deployment success
uses: slackapi/slack-github-action@v1
with:
webhook: ${{ secrets.SLACK_WEBHOOK }}
message: “✅ Production deployment successful: ProductService API”
10.2 Azure部署配置
创建
azure-pipelines.yml
用于Azure DevOps:
trigger:
– main

pool:
vmImage: ‘ubuntu-latest’

variables:
buildConfiguration: ‘Release’

stages:
– stage: Build
jobs:
– job: Build
steps:
– task: UseDotNet@2
inputs:
version: ‘8.0.x’

– task: DotNetCoreCLI@2
inputs:
command: ‘restore’
projects: ‘**/*.csproj’

– task: DotNetCoreCLI@2
inputs:
command: ‘build’
projects: ‘**/*.csproj’
arguments: ‘–configuration $(buildConfiguration)’

– task: DotNetCoreCLI@2
inputs:
command: ‘test’
projects: ‘**/*Tests/*.csproj’
arguments: ‘–configuration $(buildConfiguration) –no-build’

– task: DotNetCoreCLI@2
inputs:
command: ‘publish’
publishWebProjects: true
arguments: ‘–configuration $(buildConfiguration) –output $(Build.ArtifactStagingDirectory)’
zipAfterPublish: true

– task: PublishBuildArtifacts@1
inputs:
PathtoPublish: ‘$(Build.ArtifactStagingDirectory)’
ArtifactName: ‘drop’
publishLocation: ‘Container’

– stage: Deploy
dependsOn: Build
jobs:
– deployment: DeployToAzure
environment: ‘production’
strategy:
runOnce:
deploy:
steps:
– task: AzureWebAppContainer@1
inputs:
appName: ‘productservice-prod’
containers: ‘productservice:$(Build.BuildId)’
dockerRegistryEndpoint: ‘dockerhub-service-connection’
10.3 Kubernetes部署配置
创建
k8s-deployment.yml
:
apiVersion: apps/v1
kind: Deployment
metadata:
name: productservice-api
spec:
replicas: 3
selector:
matchLabels:
app: productservice
template:
metadata:
labels:
app: productservice
spec:
containers:
– name: productservice
image: yourdockerhub/productservice:1.0
ports:
– containerPort: 80
env:
– name: ASPNETCORE_ENVIRONMENT
value: “Production”
– name: ConnectionStrings__DefaultConnection
valueFrom:
secretKeyRef:
name: db-secret
key: connection-string
resources:
requests:
memory: “128Mi”
cpu: “250m”
limits:
memory: “256Mi”
cpu: “500m”
livenessProbe:
httpGet:
path: /api/health
port: 80
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /api/health
port: 80
initialDelaySeconds: 5
periodSeconds: 5

—
apiVersion: v1
kind: Service
metadata:
name: productservice-service
spec:
type: LoadBalancer
ports:
– port: 80
targetPort: 80
protocol: TCP
selector:
app: productservice

—
apiVersion: v1
kind: Secret
metadata:
name: db-secret
type: Opaque
data:
connection-string: U2VydmVyPXNxbHNlcnZlcjtEYXRhYmFzZT1Qcm9kdWN0U2VydmljZURCO1VzZXI9c2E7UGFzc3dvcmQ9WW91clN0cm9uZyFQYXNzd29yZDEyMztUcnVzdFNlcnZlckNlcnRpZmljYXRlPXRydWU7
10.4 创建健康检查端点
// Controllers/HealthController.cs
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Diagnostics.HealthChecks;

namespace ProductService.Controllers
{
[ApiController]
[Route(“api/[controller]”)]
public class HealthController : ControllerBase
{
private readonly HealthCheckService _healthCheckService;

public HealthController(HealthCheckService healthCheckService)
{
_healthCheckService = healthCheckService;
}

[HttpGet]
public async Task HealthCheck()
{
var report = await _healthCheckService.CheckHealthAsync();

var status = report.Status == HealthStatus.Healthy ?
Ok() : StatusCode(503);

return status;
}

[HttpGet(“detailed”)]
public async Task DetailedHealthCheck()
{
var report = await _healthCheckService.CheckHealthAsync();

var result = new
{
status = report.Status.ToString(),
totalDuration = report.TotalDuration.TotalSeconds,
checks = report.Entries.Select(e => new
{
name = e.Key,
status = e.Value.Status.ToString(),
description = e.Value.Description,
duration = e.Value.Duration.TotalSeconds,
exception = e.Value.Exception?.Message
})
};

return report.Status == HealthStatus.Healthy ?
Ok(result) : StatusCode(503, result);
}
}
}
10.5 在Program.cs中配置健康检查
// Program.cs
using Microsoft.Extensions.Diagnostics.HealthChecks;

// … 其他配置

builder.Services.AddHealthChecks()
.AddSqlServer(
connectionString: builder.Configuration.GetConnectionString(“DefaultConnection”),
name: “SQL Server”,
failureStatus: HealthStatus.Degraded | HealthStatus.Unhealthy,
tags: new[] { “database” })
.AddCheck(“Custom Check”, tags: new[] { “custom” });

// … 在app配置中
app.MapHealthChecks(“/api/health”);
app.MapHealthChecks(“/api/health/detailed”, new HealthCheckOptions
{
ResponseWriter = async (context, report) =>
{
context.Response.ContentType = “application/json”;
var result = System.Text.Json.JsonSerializer.Serialize(new
{
status = report.Status.ToString(),
checks = report.Entries.Select(e => new
{
name = e.Key,
status = e.Value.Status.ToString(),
description = e.Value.Description,
duration = e.Value.Duration.TotalSeconds
})
});
await context.Response.WriteAsync(result);
}
});
10.6 自定义健康检查
// Services/CustomHealthCheck.cs
using Microsoft.Extensions.Diagnostics.HealthChecks;

namespace ProductService.Services
{
public class CustomHealthCheck : IHealthCheck
{
public Task CheckHealthAsync(
HealthCheckContext context,
CancellationToken cancellationToken = default)
{
// 自定义检查逻辑,例如检查外部API、磁盘空间等
var isHealthy = true;
var message = “All systems operational”;

// 示例:检查内存使用情况
var memoryInfo = GC.GetTotalMemory(false);
if (memoryInfo > 100 * 1024 * 1024) // 100MB
{
isHealthy = false;
message = $”High memory usage: {memoryInfo / 1024 / 1024}MB”;
}

return Task.FromResult(isHealthy
? HealthCheckResult.Healthy(message)
: HealthCheckResult.Degraded(message));
}
}
}
第十一部分:监控和日志
11.1 使用Serilog进行结构化日志
安装Serilog包:
dotnet add package Serilog.AspNetCore
dotnet add package Serilog.Sinks.Console
dotnet add package Serilog.Sinks.File
dotnet add package Serilog.Sinks.Seq
dotnet add package Serilog.Expressions
配置Serilog:
// Program.cs
using Serilog;
using Serilog.Events;

// 配置Serilog
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Information()
.MinimumLevel.Override(“Microsoft”, LogEventLevel.Warning)
.MinimumLevel.Override(“System”, LogEventLevel.Warning)
.Enrich.FromLogContext()
.Enrich.WithMachineName()
.Enrich.WithEnvironmentName()
.WriteTo.Console(
outputTemplate: “[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} {Properties:j}{NewLine}{Exception}”)
.WriteTo.File(
“Logs/log-.txt”,
rollingInterval: RollingInterval.Day,
retainedFileCountLimit: 7,
outputTemplate: “{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj} {Properties:j}{NewLine}{Exception}”)
.WriteTo.Seq(“http://localhost:5341”)
.CreateLogger();

try
{
Log.Information(“Starting web application”);

var builder = WebApplication.CreateBuilder(args);

// 使用Serilog代替默认日志
builder.Host.UseSerilog();

// … 其他配置

var app = builder.Build();

// … 配置管道

app.Run();
}
catch (Exception ex)
{
Log.Fatal(ex, “Application terminated unexpectedly”);
}
finally
{
Log.CloseAndFlush();
}
11.2 在控制器中使用Serilog
// Controllers/ProductsController.cs
using Serilog;

namespace ProductService.Controllers
{
[ApiController]
[Route(“api/[controller]”)]
public class ProductsController : ControllerBase
{
private readonly IProductService _productService;
private readonly ILogger _logger;
private readonly Serilog.ILogger _serilog;

public ProductsController(IProductService productService, ILogger logger)
{
_productService = productService;
_logger = logger;
_serilog = Log.ForContext();
}

[HttpGet(“{id}”)]
public async Task> GetProduct(int id)
{
// 使用Serilog记录结构化日志
_serilog.Information(“获取产品详情 ID: {ProductId}”, id);

try
{
var product = await _productService.GetProductByIdAsync(id);
if (product == null)
{
_serilog.Warning(“产品未找到 ID: {ProductId}”, id);
return NotFound($”ID为{id}的产品未找到”);
}

_serilog.Information(“成功获取产品 ID: {ProductId}, Name: {ProductName}”,
id, product.Name);
return Ok(product);
}
catch (Exception ex)
{
_serilog.Error(ex, “获取产品详情时发生错误 ID: {ProductId}”, id);
return StatusCode(500, “内部服务器错误”);
}
}
}
}
11.3 使用Application Insights进行监控
安装Application Insights包:
dotnet add package Microsoft.ApplicationInsights.AspNetCore
配置Application Insights:
// Program.cs
builder.Services.AddApplicationInsightsTelemetry(options =>
{
options.EnableAdaptiveSampling = true;
options.EnableHeartbeat = true;
options.InstrumentationKey = builder.Configuration[“ApplicationInsights:InstrumentationKey”];
});

// 自定义遥测初始化器
builder.Services.AddSingleton();

// CustomTelemetryInitializer.cs
using Microsoft.ApplicationInsights.Channel;
using Microsoft.ApplicationInsights.Extensibility;

namespace ProductService
{
public class CustomTelemetryInitializer : ITelemetryInitializer
{
public void Initialize(ITelemetry telemetry)
{
telemetry.Context.Cloud.RoleName = “ProductService-API”;
telemetry.Context.Component.Version = “1.0.0”;
}
}
}
11.4 自定义指标和跟踪
// Services/TelemetryService.cs
using Microsoft.ApplicationInsights;
using Microsoft.ApplicationInsights.DataContracts;

namespace ProductService.Services
{
public class TelemetryService
{
private readonly TelemetryClient _telemetryClient;

public TelemetryService(TelemetryClient telemetryClient)
{
_telemetryClient = telemetryClient;
}

public void TrackProductCreated(int productId, string productName, decimal price)
{
var properties = new Dictionary
{
{ “ProductId”, productId.ToString() },
{ “ProductName”, productName },
{ “Category”, “Electronics” }
};

var metrics = new Dictionary
{
{ “ProductPrice”, (double)price },
{ “StockQuantity”, 1 }
};

_telemetryClient.TrackEvent(“ProductCreated”, properties, metrics);
}

public void TrackApiPerformance(string endpoint, double durationMs)
{
_telemetryClient.TrackRequest(
name: endpoint,
startTime: DateTime.UtcNow,
duration: TimeSpan.FromMilliseconds(durationMs),
responseCode: “200”,
success: true);
}

public void TrackBusinessMetric(string metricName, double value)
{
_telemetryClient.TrackMetric(metricName, value);
}
}
}
第十二部分:安全最佳实践
12.1 输入验证和净化
// Services/InputSanitizationService.cs
using System.Text.RegularExpressions;

namespace ProductService.Services
{
public interface IInputSanitizationService
{
string SanitizeInput(string input);
bool IsValidInput(string input, string pattern);
}

public class InputSanitizationService : IInputSanitizationService
{
// 防止XSS攻击
public string SanitizeInput(string input)
{
if (string.IsNullOrEmpty(input))
return input;

// 移除潜在的危险HTML标签
var sanitized = Regex.Replace(input, @”)<[^<]*)*“, “”, RegexOptions.IgnoreCase);
sanitized = Regex.Replace(sanitized, @”javascript:”, “”, RegexOptions.IgnoreCase);
sanitized = Regex.Replace(sanitized, @”on\w+=”, “”, RegexOptions.IgnoreCase);

// 转义HTML特殊字符
sanitized = System.Net.WebUtility.HtmlEncode(sanitized);

return sanitized;
}

public bool IsValidInput(string input, string pattern)
{
if (string.IsNullOrEmpty(input))
return false;

return Regex.IsMatch(input, pattern);
}
}
}
12.2 SQL注入防护
// 在ProductService中使用参数化查询
public async Task> SearchProductsAsync(string searchTerm)
{
_logger.LogInformation(“搜索产品: {SearchTerm}”, searchTerm);

// 使用EF Core的参数化查询,自动防止SQL注入
return await _context.Products
.Include(p => p.Category)
.Where(p => p.IsActive &&
(p.Name.Contains(searchTerm) ||
p.Description.Contains(searchTerm)))
.OrderBy(p => p.Name)
.ToListAsync();
}
12.3 Rate Limiting 限流
安装限流包:
dotnet add package AspNetCoreRateLimit
配置限流:
// Program.cs
using AspNetCoreRateLimit;

// 配置IP限流
builder.Services.Configure(options =>
{
options.EnableEndpointRateLimiting = true;
options.StackBlockedRequests = false;
options.HttpStatusCode = 429;
options.GeneralRules = new List
{
new RateLimitRule
{
Endpoint = “*”,
Period = “1m”,
Limit = 60
},
new RateLimitRule
{
Endpoint = “api/products”,
Period = “1m”,
Limit = 30
}
};
});

// 配置客户端限流
builder.Services.Configure(options =>
{
options.EnableEndpointRateLimiting = true;
options.StackBlockedRequests = false;
options.HttpStatusCode = 429;
options.GeneralRules = new List
{
new RateLimitRule
{
Endpoint = “*”,
Period = “1h”,
Limit = 1000
}
};
});

builder.Services.AddSingleton();
builder.Services.AddSingleton();
builder.Services.AddSingleton();
builder.Services.AddSingleton();
builder.Services.AddSingleton();

// 在app配置中
app.UseIpRateLimiting();
app.UseClientRateLimiting();
12.4 CORS配置
// Program.cs
builder.Services.AddCors(options =>
{
options.AddPolicy(“AllowSpecificOrigins”,
policy =>
{
policy.WithOrigins(
“https://yourfrontend.com”,
“https://app.yourfrontend.com”)
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials();
});

options.AddPolicy(“AllowAll”,
policy =>
{
policy.AllowAnyOrigin()
.AllowAnyHeader()
.AllowAnyMethod();
});
});

// 在app配置中
app.UseCors(“AllowSpecificOrigins”);
12.5 安全头配置
// Middleware/SecurityHeadersMiddleware.cs
namespace ProductService.Middleware
{
public class SecurityHeadersMiddleware
{
private readonly RequestDelegate _next;

public SecurityHeadersMiddleware(RequestDelegate next)
{
_next = next;
}

public async Task InvokeAsync(HttpContext context)
{
// 移除服务器信息头
context.Response.Headers.Remove(“Server”);
context.Response.Headers.Remove(“X-Powered-By”);
context.Response.Headers.Remove(“X-AspNet-Version”);

// 添加安全头
context.Response.Headers.Add(“X-Content-Type-Options”, “nosniff”);
context.Response.Headers.Add(“X-Frame-Options”, “DENY”);
context.Response.Headers.Add(“X-XSS-Protection”, “1; mode=block”);
context.Response.Headers.Add(“Strict-Transport-Security”, “max-age=31536000; includeSubDomains”);
context.Response.Headers.Add(“Content-Security-Policy”, “default-src ‘self’”);
context.Response.Headers.Add(“Referrer-Policy”, “strict-origin-when-cross-origin”);

await _next(context);
}
}

public static class SecurityHeadersExtensions
{
public static IApplicationBuilder UseSecurityHeaders(this IApplicationBuilder builder)
{
return builder.UseMiddleware();
}
}
}
第十三部分:性能监控和调优
13.1 使用MiniProfiler进行性能分析
安装MiniProfiler包:
dotnet add package MiniProfiler.AspNetCore.Mvc
dotnet add package MiniProfiler.EntityFrameworkCore
配置MiniProfiler:
// Program.cs
builder.Services.AddMiniProfiler(options =>
{
options.RouteBasePath = “/profiler”;
options.EnableConnectionTracking = true;
options.EnableMvcFilterProfiling = true;
options.EnableMvcViewProfiling = true;
}).AddEntityFramework();

// 在app配置中
app.UseMiniProfiler();
在控制器中使用:
[HttpGet]
public async Task>> GetProducts()
{
using (MiniProfiler.Current.Step(“获取产品数据”))
{
using (MiniProfiler.Current.Step(“数据库查询”))
{
var products = await _productService.GetAllProductsAsync();
}

using (MiniProfiler.Current.Step(“数据处理”))
{
// 额外的处理逻辑
}

return Ok(products);
}
}
13.2 数据库索引优化
— 在迁移中添加索引
— 在OnModelCreating中配置
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// 复合索引
modelBuilder.Entity()
.HasIndex(p => new { p.CategoryId, p.Price, p.IsActive })
.IsDescending(false, true, false);

// 包含列索引(SQL Server特有)
modelBuilder.Entity()
.HasIndex(p => p.Name)
.IncludeProperties(p => new { p.Price, p.Description });
}
13.3 查询性能分析
// Services/QueryPerformanceService.cs
using Microsoft.EntityFrameworkCore;
using System.Diagnostics;

namespace ProductService.Services
{
public class QueryPerformanceService
{
private readonly ApplicationDbContext _context;
private readonly ILogger _logger;

public QueryPerformanceService(ApplicationDbContext context, ILogger logger)
{
_context = context;
_logger = logger;
}

public async Task AnalyzeQueryPerformance()
{
var stopwatch = Stopwatch.StartNew();

// 启用SQL日志
_context.Database.Log = (sql) =>
{
_logger.LogInformation(“SQL执行: {SQL}”, sql);
};

// 分析查询
var query = _context.Products
.Include(p => p.Category)
.Where(p => p.IsActive)
.OrderBy(p => p.Name)
.AsNoTracking();

// 获取执行计划
var sql = query.ToQueryString();
_logger.LogInformation(“生成的SQL: {SQL}”, sql);

// 执行查询
var results = await query.ToListAsync();

stopwatch.Stop();
_logger.LogInformation(“查询执行时间: {ElapsedMs}ms”, stopwatch.ElapsedMilliseconds);

// 分析N+1问题
await AnalyzeNPlusOneProblem();
}

private async Task AnalyzeNPlusOneProblem()
{
// 错误的N+1查询示例
var categories = await _context.Categories.ToListAsync();

foreach (var category in categories)
{
// 每次循环都会产生新的数据库查询 – N+1问题
var products = await _context.Products
.Where(p => p.CategoryId == category.Id)
.ToListAsync();

_logger.LogWarning(“N+1问题: 分类 {CategoryName} 有 {ProductCount} 个产品”,
category.Name, products.Count);
}

// 正确的解决方案:使用Include或Join
var correctQuery = await _context.Categories
.Include(c => c.Products)
.ToListAsync();
}
}
}
第十四部分:错误处理和恢复
14.1 重试策略和熔断器
// Services/ResilientHttpClient.cs
using Polly;
using Polly.Retry;
using System.Net.Http;

namespace ProductService.Services
{
public class ResilientHttpClient
{
private readonly HttpClient _httpClient;
private readonly AsyncRetryPolicy _retryPolicy;
private readonly AsyncCircuitBreakerPolicy _circuitBreakerPolicy;

public ResilientHttpClient(HttpClient httpClient)
{
_httpClient = httpClient;

// 重试策略
_retryPolicy = Policy
.Handle()
.OrResult(msg => msg.StatusCode == System.Net.HttpStatusCode.InternalServerError)
.OrResult(msg => msg.StatusCode == System.Net.HttpStatusCode.TooManyRequests)
.WaitAndRetryAsync(
retryCount: 3,
sleepDurationProvider: attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt)),
onRetry: (outcome, timespan, retryCount, context) =>
{
Console.WriteLine($”Retry {retryCount} after {timespan.TotalSeconds}s”);
});

// 熔断器策略
_circuitBreakerPolicy = Policy
.Handle()
.OrResult(msg => msg.StatusCode == System.Net.HttpStatusCode.InternalServerError)
.CircuitBreakerAsync(
exceptionsAllowedBeforeBreaking: 5,
durationOfBreak: TimeSpan.FromSeconds(30));
}

public async Task GetAsync(string url)
{
return await _retryPolicy.ExecuteAsync(async () =>
{
return await _circuitBreakerPolicy.ExecuteAsync(async () =>
{
return await _httpClient.GetAsync(url);
});
});
}
}
}
14.2 数据备份和恢复策略
// Services/BackupService.cs
using Microsoft.Data.SqlClient;
using System.IO.Compression;

namespace ProductService.Services
{
public class BackupService
{
private readonly IConfiguration _configuration;
private readonly ILogger _logger;

public BackupService(IConfiguration configuration, ILogger logger)
{
_configuration = configuration;
_logger = logger;
}

public async Task CreateDatabaseBackupAsync()
{
try
{
var connectionString = _configuration.GetConnectionString(“DefaultConnection”);
var databaseName = “ProductServiceDB”;
var backupPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
“Backups”,
$”Backup_{DateTime.Now:yyyyMMdd_HHmmss}.bak”);

// 确保备份目录存在
Directory.CreateDirectory(Path.GetDirectoryName(backupPath));

using (var connection = new SqlConnection(connectionString))
{
await connection.OpenAsync();
using (var command = new SqlCommand($”BACKUP DATABASE [{databaseName}] TO DISK = @backupPath”, connection))
{
command.Parameters.AddWithValue(“@backupPath”, backupPath);
await command.ExecuteNonQueryAsync();
}
}

// 压缩备份文件
var compressedPath = backupPath + “.zip”;
using (var archive = ZipFile.Open(compressedPath, ZipArchiveMode.Create))
{
archive.CreateEntryFromFile(backupPath, Path.GetFileName(backupPath));
}

File.Delete(backupPath);

_logger.LogInformation(“数据库备份成功: {BackupPath}”, compressedPath);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, “数据库备份失败”);
return false;
}
}

public async Task RestoreDatabaseBackupAsync(string backupFilePath)
{
try
{
var connectionString = _configuration.GetConnectionString(“DefaultConnection”);
var databaseName = “ProductServiceDB”;

// 解压缩备份文件
var tempBackupPath = Path.Combine(Path.GetTempPath(), Path.GetFileNameWithoutExtension(backupFilePath) + “.bak”);
ZipFile.ExtractToDirectory(backupFilePath, Path.GetTempPath(), overwriteFiles: true);

using (var connection = new SqlConnection(connectionString))
{
await connection.OpenAsync();

// 断开所有连接
using (var command = new SqlCommand($@”
ALTER DATABASE [{databaseName}]
SET SINGLE_USER
WITH ROLLBACK IMMEDIATE”, connection))
{
await command.ExecuteNonQueryAsync();
}

// 恢复数据库
using (var command = new SqlCommand($@”
RESTORE DATABASE [{databaseName}]
FROM DISK = @backupPath
WITH REPLACE”, connection))
{
command.Parameters.AddWithValue(“@backupPath”, tempBackupPath);
await command.ExecuteNonQueryAsync();
}

// 恢复多用户模式
using (var command = new SqlCommand($@”
ALTER DATABASE [{databaseName}]
SET MULTI_USER”, connection))
{
await command.ExecuteNonQueryAsync();
}
}

File.Delete(tempBackupPath);
_logger.LogInformation(“数据库恢复成功: {BackupFilePath}”, backupFilePath);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, “数据库恢复失败”);
return false;
}
}
}
}
第十五部分:总结和最佳实践清单
15.1 项目结构最佳实践
分层架构
:严格分离Controller、Service、Data层
依赖注入
:所有依赖通过构造函数注入
异步编程
:全程使用async/await
异常处理
:全局异常处理 + 业务异常分类
日志记录
:结构化日志 + 不同级别
配置管理
:环境特定配置 + 密钥管理
15.2 性能优化清单
[ ] 使用AsNoTracking用于只读查询
[ ] 实现缓存策略(内存/分布式)
[ ] 使用投影减少数据传输
[ ] 数据库索引优化
[ ] 启用压缩(Gzip/Brotli)
[ ] 使用连接池
[ ] 实现请求限流
[ ] 异步所有I/O操作
15.3 安全最佳实践清单
[ ] 输入验证和净化
[ ] SQL参数化查询
[ ] JWT认证和授权
[ ] CORS配置
[ ] 安全HTTP头
[ ] Rate Limiting
[ ] 敏感信息加密
[ ] 定期安全审计
15.4 部署清单
[ ] Docker容器化
[ ] 环境变量配置
[ ] 健康检查端点
[ ] 日志聚合
[ ] 监控和告警
[ ] 自动化CI/CD
[ ] 备份策略
[ ] 灾难恢复计划
15.5 测试策略
[ ] 单元测试(Service层)
[ ] 集成测试(API端点)
[ ] 负载测试
[ ] 安全测试
[ ] 端到端测试
结语
本文详细介绍了基于.NET平台的Web Service开发全流程,从基础概念到实际项目部署。我们通过一个完整的产品管理API实例,展示了现代.NET Web API开发的最佳实践。
关键要点总结:
技术选型
:优先使用ASP.NET Core Web API,它提供了最佳的性能、跨平台支持和开发体验
架构设计
:采用清晰的分层架构,确保代码的可维护性和可测试性
安全性
:实施全面的安全措施,包括认证、授权、输入验证和限流
性能
:通过缓存、异步编程和数据库优化确保高性能
可观测性
:完善的日志、监控和健康检查
部署
:容器化部署 + 自动化CI/CD
可靠性
:重试策略、熔断器和备份恢复机制
这个完整的开发流程可以作为企业级.NET Web Service项目的标准模板,根据具体需求进行调整和扩展。记住,好的软件开发不仅仅是编写代码,更是关于设计、测试、部署和维护的完整生命周期管理。