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

作者:微信公众号:【架构师老卢】
11-10 14:17
68

在 .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 步:在启动时配置依赖关系注入

配置 MediatRFluentValidationEF 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
  • BaseRepositoryOrderRepository,用于通用和特定于订单的数据访问逻辑。
  • 用于管理事务和存储库交互的工作单元模式
  • 带有 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 MediatRFluentValidation,我们已将紧密耦合、易受攻击的代码库转换为干净、可扩展且专业的 .NET 解决方案:

  • Entity Framework Core 提供可靠、安全的数据访问。
  • Repository PatternBaseRepository 支持干净的 DRY 代码。
  • Unit of Work 确保跨多个操作的事务完整性。
  • 带有 MediatR 的 CQRS 将读取和写入问题分开,使应用程序更易于维护和测试。
  • FluentValidation 强制实施一致、可重用的验证规则。

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

相关留言评论
昵称:
邮箱:
阅读排行