如何重构和清理 .NET 代码:编写安全且可维护的代码

科技   2024-11-25 05:19   上海  


在 .NET 开发中,很容易陷入编码实践,这些实践可能会悄无声息地降低应用程序的质量、安全性和可维护性。这些“无声代码剧透”可能会引入错误,导致安全漏洞,并使代码难以阅读和更新。在本文中,我们将探讨 .NET 应用程序中的不良代码示例,并逐步演示如何根据干净的代码原则重构它,包括命名约定、配置管理、SQL 注入预防和更好的结构。

我们将探讨关键原则,例如依赖项注入、关注点分离、错误处理和结构化日志记录,同时我们将示例重构为干净、专业的解决方案。

错误代码

让我们从 .NET 中订单处理工作流的基本示例开始。此示例存在几个影响可读性、可维护性和安全性的问题。我们将以此为起点,并在整篇文章中将其转换为干净、可维护的代码。

错误代码示例

此示例代码执行订单处理、验证并更新数据库中的订单状态。但是,它充满了问题,包括命名不一致、硬编码值、缺乏关注点分离以及 SQL 注入漏洞。

using System.Data.SqlClient;
public class order_service
{
private string conn_string = "your_connection_string_here";
public bool processOrder(Order order)
{
if (order != null)
{
if (order.Items != null && order.Items.Count > 0)
{
if (order.CustomerId != null)
{
decimal discount = 0.0m;
if (order.TotalAmount > 100)
{
discount = 0.05m; // Apply discount for orders over 100
}
else if (order.TotalAmount > 500)
{
discount = 0.10m; // Apply discount for orders over 500
}
order.TotalAmount -= order.TotalAmount * discount;
if (order.ShippingAddress == null || order.ShippingAddress == "")
{
Console.WriteLine("Shipping address is required.");
return false;
}
Console.WriteLine("Processing payment...");
// Assume payment is processed
UpdateOrderStatus(order.OrderId, "Processed");
Console.WriteLine("Order processed for customer " + order.CustomerId);
return true;
}
else
{
Console.WriteLine("Invalid customer.");
return false;
}
}
else
{
Console.WriteLine("No items in order.");
return false;
}
}
else
{
Console.WriteLine("Order is null.");
return false;
}
}
private void UpdateOrderStatus(int orderId, string status)
{
SqlConnection connection = new SqlConnection(conn_string);
connection.Open();
// SQL Injection Vulnerability
string query = $"UPDATE Orders SET Status = '{status}' WHERE OrderId = {orderId}";
SqlCommand command = new SqlCommand(query, connection);
command.ExecuteNonQuery();

connection.Close();
}
}

代码的问题

  1. 命名不一致:类和方法 (, ) 不遵循 PascalCase 约定,使代码更难阅读且不专业。order_serviceprocessOrder

  2. 硬编码值:折扣阈值 ( 和 ) 和费率 (, ) 是硬编码的,这使得跨环境更新变得困难。1005000.050.10

  3. Lack of Separation of Concerns:处理从验证到更新数据库和日志记录的所有事情。processOrder

  4. SQL 注入漏洞:该方法直接将参数合并到 SQL 查询中,因此容易受到 SQL 注入的影响。UpdateOrderStatus

  5. No Using Statements:数据库连接是手动打开和关闭的,没有块,如果出现异常,连接可能会被取消关闭。using

  6. 详细 ADO.NET 代码:SQL 执行的 ADO.NET 样板代码很详细,可以简化。

使用 Clean Code 原则重构代码

要重构此代码,我们将:

  • 实施正确的命名约定。

  • 将配置值移动到 JSON 文件。

  • 使用 Dapper 进行安全的参数化 SQL 查询。

  • 通过创建专用方法和类来分离关注点。

  • 使用语句进行自动资源管理。using

让我们来演练一下重构过程的每个步骤。

第 1 步:将配置移动到 JSON 文件

为避免硬编码值,让我们将折扣阈值和费率移动到文件中。这种方法无需修改代码即可轻松更新,并提高跨环境的一致性。discountSettings.json

discountSettings.json

{  
"ConnectionStrings": {
"DefaultConnection": "your_connection_string_here"
},
"DiscountSettings": {
"SmallOrderDiscount": 0.05,
"LargeOrderDiscount": 0.10,
"SmallOrderThreshold": 100.0,
"LargeOrderThreshold": 500.0
}
}

第 2 步:定义配置 POCO 类

定义一个类以从 JSON 文件映射折扣设置。

public class DiscountSettings  
{
public decimal SmallOrderDiscount { get; set; }
public decimal LargeOrderDiscount { get; set; }
public decimal SmallOrderThreshold { get; set; }
public decimal LargeOrderThreshold { get; set; }
}

第 3 步:在Startup.cs

将配置部分绑定到类,并在 的依赖项注入容器中注册它。DiscountSettingsStartup.cs

public class Startup  
{
public void ConfigureServices(IServiceCollection services)
{
services.Configure<DiscountSettings>(Configuration.GetSection("DiscountSettings"));
services.AddTransient<OrderService>();
services.AddScoped<IPaymentProcessor, PaymentProcessor>(); // Example dependency
services.AddScoped<OrderRepository>();
services.AddLogging();
}
}

使用 Dapper 的 OrderRepository 类

让我们将数据库交互移动到单独的类 中,以使用 Dapper 处理数据库交互。这可确保安全的参数化 SQL 查询,从而防止 SQL 注入攻击。OrderRepository

using System.Data;
using Dapper;
using Microsoft.Extensions.Configuration;
using System.Data.SqlClient;
public class OrderRepository
{
private readonly string _connectionString;
public OrderRepository(IConfiguration configuration)
{
_connectionString = configuration.GetConnectionString("DefaultConnection");
}
public void UpdateOrderStatus(int orderId, string status)
{
using (IDbConnection connection = new SqlConnection(_connectionString))
{
// Using Dapper with parameterized queries to prevent SQL injection
string query = "UPDATE Orders SET Status = @Status WHERE OrderId = @OrderId";
connection.Execute(query, new { Status = status, OrderId = orderId });
}
}
}

使用 Dapper 和 Repository 模式重构了 OrderService

现在,我们将重构以使用 for database 交互,以及其他干净的代码改进,例如依赖项注入和关注点分离。OrderServiceOrderRepository

using Microsoft.Extensions.Options;
public class OrderService
{
private readonly decimal _smallOrderDiscount;
private readonly decimal _largeOrderDiscount;
private readonly decimal _smallOrderThreshold;
private readonly decimal _largeOrderThreshold;
private readonly IPaymentProcessor _paymentProcessor;
private readonly ILogger<OrderService> _logger;
private readonly OrderRepository _orderRepository;
public OrderService(IOptions<DiscountSettings> discountSettings, IPaymentProcessor paymentProcessor, ILogger<OrderService> logger, OrderRepository orderRepository)
{
var settings = discountSettings.Value;
_smallOrderDiscount = settings.SmallOrderDiscount;
_largeOrderDiscount = settings.LargeOrderDiscount;
_smallOrderThreshold = settings.SmallOrderThreshold;
_largeOrderThreshold = settings.LargeOrderThreshold;
_paymentProcessor = paymentProcessor;
_logger = logger;
_orderRepository = orderRepository;
}
public bool ProcessOrder(Order order)
{
if (!ValidateOrder(order)) return false;
ApplyDiscount(order);
if (!ProcessPayment(order)) return false;
// Update order status in the database using Dapper
_orderRepository.UpdateOrderStatus(order.OrderId, "Processed");
_logger.LogInformation($"Order processed successfully for customer {order.CustomerId}");

return true;
}
private bool ValidateOrder(Order order)
{
if (order == null)
{
_logger.LogError("Order is null.");
return false;
}
if (string.IsNullOrWhiteSpace(order.CustomerId))
{
_logger.LogError("Invalid customer.");
return false;
}
if (order.Items == null || !order.Items.Any())
{
_logger.LogError("Order has no items.");
return false;
}
if (string.IsNullOrWhiteSpace(order.ShippingAddress))
{
_logger.LogError("Shipping address is required.");
return false;
}
return true;
}
private void ApplyDiscount(Order order)
{
decimal discount = order.TotalAmount > _largeOrderThreshold ? _largeOrderDiscount :
order.TotalAmount > _smallOrderThreshold ? _smallOrderDiscount : 0;
order.TotalAmount -= order.TotalAmount * discount;
}
private bool ProcessPayment(Order order)
{
try
{
_paymentProcessor.Process(order);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "Payment processing failed.");
return false;
}
}
}

重构和清理代码改进的说明

SQL 注入预防

  • 该类将 Dapper 与参数化查询结合使用,通过安全地处理和参数来防止 SQL 注入。OrderRepositoryorderIdstatus

存储库模式

  • 该类封装了所有数据库操作,将数据访问层与业务逻辑分离。这种关注点分离提高了可维护性和可测试性。OrderRepositoryOrderService

配置管理

  • 连接字符串存储在 section 下,并使用依赖关系注入进行访问。这提高了灵活性和安全性。discountSettings.json"ConnectionStrings"

依赖注入

  • OrderRepository被注入到 中,使其在测试期间更容易替换为 mock 存储库。OrderService

改进的日志记录

  • 结构化日志记录提供详细的反馈,从而更好地了解订单处理的每个步骤。ILogger

更简洁的代码结构

  • 代码现在是模块化的,每个方法都处理一个责任。 经过简化,可为每个任务调用不同的方法 (、、、),从而提高可读性和可维护性。ProcessOrderValidateOrderApplyDiscountProcessPaymentUpdateOrderStatus

使用干净的代码和现代模式进行高级重构

为了重构此代码,我们将使用 Entity Framework Core 实现一个干净的体系结构,用于数据访问,使用 Unit of Work 和 Repository Pattern 来组织数据逻辑,使用 CQRS with MediatR 来分离读取和写入操作,并使用 FluentValidation 进行验证。

这种方法将产生一个结构良好、可维护和可测试的解决方案。让我们来了解一下每个步骤。

步骤 1:使用 DbContext 设置 Entity Framework Core

使用 Entity Framework Core 使我们能够使用强类型 ORM 处理数据库交互,从而消除了对原始 SQL 和手动连接管理的需求。

OrderDbContext

using Microsoft.EntityFrameworkCore;
public class OrderDbContext : DbContext
{
public DbSet<Order> Orders { get; set; }
public OrderDbContext(DbContextOptions<OrderDbContext> options) : base(options) { }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Order>().ToTable("Orders");
// Additional configurations if needed
}
}

第 2 步:实施 BaseRepository 和 OrderRepository

BaseRepository 类将处理任何实体的常见 CRUD 操作,而 OrderRepository 将根据需要继承以包含特定于订单的逻辑。BaseRepository

BaseRepository 仓库

using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Threading.Tasks;
public class BaseRepository<T> : IRepository<T> where T : class
{
protected readonly DbContext _context;
protected readonly DbSet<T> _dbSet;
public BaseRepository(DbContext context)
{
_context = context;
_dbSet = context.Set<T>();
}
public virtual async Task<T> GetByIdAsync(int id) => await _dbSet.FindAsync(id);
public virtual async Task AddAsync(T entity) => await _dbSet.AddAsync(entity);
public virtual async Task UpdateAsync(T entity)
{
_dbSet.Attach(entity);
_context.Entry(entity).State = EntityState.Modified;
}
public virtual async Task DeleteAsync(T entity) => _dbSet.Remove(entity);
public virtual async Task<IEnumerable<T>> GetAllAsync() => await _dbSet.ToListAsync();
}

OrderRepository (订单仓库)

public class OrderRepository : BaseRepository<Order>  
{
public OrderRepository(OrderDbContext context) : base(context)
{
}
// Add any Order-specific methods here if needed
}

步骤 3:实现 Unit of Work 模式

Unit of Work 模式有助于协调跨多个存储库保存更改,从而允许所有操作作为单个事务完成。

IUnitOfWork 接口

public interface IUnitOfWork : IDisposable  
{
IRepository<Order> Orders { get; }
Task<int> CompleteAsync();
}

UnitOfWork 实现

public class UnitOfWork : IUnitOfWork
{
private readonly OrderDbContext _context;
public IRepository<Order> Orders { get; }
public UnitOfWork(OrderDbContext context, OrderRepository orderRepository)
{
_context = context;
Orders = orderRepository;
}
public async Task<int> CompleteAsync() => await _context.SaveChangesAsync();
public void Dispose() => _context.Dispose();
}

步骤 4:使用 MediatR 应用 CQRS

实施 **CQRS(命令查询责任分离)**允许我们将读取和写入操作分开,使每个操作更易于测试、修改和扩展。我们将使用 MediatR 来处理命令和查询,将业务逻辑与控制器解耦。

定义 ProcessOrderCommand

using MediatR;  
public class ProcessOrderCommand : IRequest\<bool>
{
public int OrderId { get; set; }
}

ProcessOrderCommandHandler

using System.Threading;
using System.Threading.Tasks;
using MediatR;
using Microsoft.Extensions.Logging;
public class ProcessOrderCommandHandler : IRequestHandler<ProcessOrderCommand, bool>
{
private readonly IUnitOfWork _unitOfWork;
private readonly IPaymentProcessor _paymentProcessor;
private readonly ILogger<ProcessOrderCommandHandler> _logger;
public ProcessOrderCommandHandler(IUnitOfWork unitOfWork, IPaymentProcessor paymentProcessor, ILogger<ProcessOrderCommandHandler> logger)
{
_unitOfWork = unitOfWork;
_paymentProcessor = paymentProcessor;
_logger = logger;
}
public async Task<bool> Handle(ProcessOrderCommand request, CancellationToken cancellationToken)
{
var order = await _unitOfWork.Orders.GetByIdAsync(request.OrderId);

if (order == null)
{
_logger.LogError("Order not found.");
return false;
}
ApplyDiscount(order);
if (!await ProcessPayment(order))
{
return false;
}
order.Status = "Processed";
await _unitOfWork.Orders.UpdateAsync(order);
await _unitOfWork.CompleteAsync();
_logger.LogInformation($"Order processed successfully for customer {order.CustomerId}");
return true;
}
private void ApplyDiscount(Order order)
{
// Discount logic based on thresholds from config
}
private async Task<bool> ProcessPayment(Order order)
{
try
{
await _paymentProcessor.ProcessAsync(order);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "Payment processing failed.");
return false;
}
}
}

步骤 5:添加 FluentValidation 进行订单验证

使用 FluentValidation 使我们能够编写干净且可重用的验证逻辑,这些逻辑可以很容易地进行单元测试并在整个应用程序中应用。

订单验证器

using FluentValidation;  
public class OrderValidator : AbstractValidator<Order>
{
public OrderValidator()
{
RuleFor(order => order.CustomerId).NotEmpty().WithMessage("Customer ID is required.");
RuleFor(order => order.Items).NotEmpty().WithMessage("Order must contain at least one item.");
RuleFor(order => order.ShippingAddress).NotEmpty().WithMessage("Shipping address is required.");
RuleFor(order => order.TotalAmount).GreaterThan(0).WithMessage("Total amount must be greater than zero.");
}
}

第 6 步:在启动时配置依赖关系注入

配置 MediatRFluentValidation 和 EF Core 以进行依赖项注入,确保所有内容都已注册并可供使用。Startup.cs

public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<OrderDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddScoped<IRepository<Order>, OrderRepository>();
services.AddScoped<IUnitOfWork, UnitOfWork>();
services.AddMediatR(typeof(ProcessOrderCommandHandler).Assembly);
services.AddTransient<IValidator<Order>, OrderValidator>();
services.AddControllers();
}
}

最终重构代码结构

完成上述步骤后,我们的代码现在组织如下:

  • Entity Framework Core for ORM,使用 .OrderDbContext

  • BaseRepository 和 OrderRepository,用于通用和特定于订单的数据访问逻辑。

  • 用于管理事务和存储库交互的工作单元模式

  • 带有 MediatR 的 CQRS,用于处理命令、解耦操作并实现可扩展性。

  • FluentValidation 用于可重用、可测试的验证逻辑。

控制器中 MediatR 命令的示例用法

设置 MediatR 后,控制器可以轻松发送命令并处理响应。

[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
private readonly IMediator _mediator;
public OrdersController(IMediator mediator)
{
_mediator = mediator;
}
[HttpPost("order/process/{id}")]
public async Task<IActionResult> ProcessOrder(int id)
{
var result = await _mediator.Send(new ProcessOrderCommand { OrderId = id });
return result ? Ok("Order processed successfully") : BadRequest("Order processing failed");
}
}

通过重构原始代码以使用 Entity Framework CoreUnit of WorkRepository PatternCQRS with MediatR 和 FluentValidation,我们已将紧密耦合、易受攻击的代码库转换为干净、可扩展且专业的 .NET 解决方案:

  • Entity Framework Core 提供可靠、安全的数据访问。

  • Repository Pattern 和 BaseRepository 支持干净的 DRY 代码。

  • Unit of Work 确保跨多个操作的事务完整性。

  • 带有 MediatR 的 CQRS 将读取和写入问题分开,使应用程序更易于维护和测试。

  • FluentValidation 强制实施一致、可重用的验证规则。

这种方法可确保您的应用程序易于维护可扩展具有弹性,从而为长期成功做好准备。

如果你喜欢我的文章,请给我一个赞!谢谢

架构师老卢
资深软件架构师, 分享编程、软件设计经验, 教授前沿技术, 分享技术资源(每天发布电子书),每天进步一点点...
 最新文章