C#进阶实战:异步编程、LINQ表达式树与依赖注入深度解析

📅 2026/8/2 9:12:27 👁️ 阅读次数 📝 编程学习
C#进阶实战:异步编程、LINQ表达式树与依赖注入深度解析

很多C#开发者都有这样的困惑:基础语法学完了,项目也做了几个,但总感觉自己停留在“会用”的层面,遇到复杂业务逻辑时还是无从下手。你可能会写List<T>,但面对IEnumerable<T>IQueryable<T>IAsyncEnumerable<T>时,却不知道它们背后的性能差异和适用场景;你或许能实现一个简单的接口,但当需要设计一个可扩展的插件系统时,却对依赖注入、反射和动态加载感到迷茫。

这恰恰是“中级开发者陷阱”——掌握了语法,却缺乏将语言特性转化为解决实际工程问题的能力。本文是《Learn Complete C# – Beginner to Advanced》系列的第二部分,我们将不再重复if-elsefor循环,而是直击那些让C#代码从“能跑”到“跑得好”的核心进阶主题。我们将深入探讨异步编程如何真正提升吞吐量、LINQ表达式树如何实现动态查询、依赖注入如何解耦复杂系统,以及如何利用反射和特性构建灵活框架。

读完本文,你将获得一套完整的“C#进阶工具箱”,不仅能理解这些高级概念,更能掌握它们在真实项目中的应用场景和最佳实践,让你的代码在性能、可维护性和扩展性上提升一个档次。

1. 从“语法熟悉”到“工程思维”的跨越

很多教程止步于语法,但真正的进阶始于理解C#如何解决工程问题。我们首先需要建立一个认知:C#不仅仅是一门语言,更是一个包含运行时、框架和丰富生态的完整平台。进阶学习的关键在于掌握其设计哲学和解决特定问题的模式。

例如,当你看到async/await时,不应只记住“这是异步关键字”,而应理解它背后是C#为高并发I/O密集型应用提供的解决方案。其核心价值在于用同步的代码风格编写异步逻辑,避免回调地狱,同时最大化线程池利用率。一个常见的误区是盲目地在所有方法前添加async,这反而可能因为不必要的状态机分配而降低性能。正确的工程思维是:识别真正的I/O操作(如数据库查询、文件读写、网络请求),并仅在这些边界处使用异步。

另一个跨越是理解“约定优于配置”的原则。在ASP.NET Core中,你不需要手动配置每一个控制器和路由,只要遵循命名和放置位置的约定,框架就能自动发现并注册。这种思维也体现在Entity Framework Core的约定映射、Minimal API的自动绑定等方面。进阶开发者需要从“我要写什么代码”转变为“框架期望我如何组织代码”。

2. 深入异步编程:超越await的性能与陷阱

异步编程是现代C#应用的基石,但async/await只是冰山一角。要真正掌握,必须理解其底层机制和性能特征。

2.1 Task状态机与线程池协作

当你标记一个方法为async时,编译器会将其重写为一个状态机类。这个状态机负责在异步操作挂起和恢复时保存局部变量和方法状态。理解这一点至关重要,因为它解释了为什么异步方法会有少量的内存开销(每个状态机对象),以及为什么在热路径(hot path)中应避免不必要的异步。

// 一个简单的异步方法 public async Task<string> FetchDataAsync(string url) { // 状态机在此处开始,同步执行直到第一个await using var httpClient = new HttpClient(); // 遇到await:检查Task是否已完成。 // 若未完成,则方法返回一个未完成的Task给调用者,状态机挂起。 // 线程池线程被释放,可去处理其他工作。 var response = await httpClient.GetAsync(url); // 当GetAsync完成时,线程池中的某个线程会恢复此状态机的执行。 // 这可能是与之前不同的线程(取决于同步上下文)。 var content = await response.Content.ReadAsStringAsync(); // 方法完成,状态机将结果设置到返回的Task中。 return content; }

关键点:await并不会阻塞线程。它向编译器发出信号:“这里可以异步等待,在等待期间,当前线程可以被释放去做其他工作。”

2.2 ConfigureAwait(false) 的恰当使用

在库代码或非UI上下文中,使用ConfigureAwait(false)是重要的性能最佳实践。它告诉任务调度器:“我不需要在原始的同步上下文(例如UI线程)上恢复执行。” 这可以避免不必要的线程切换和潜在的死锁。

public async Task<Data> GetDataFromServiceAsync() { // 这是一个类库方法,不涉及UI更新 using var client = new HttpClient(); var response = await client.GetAsync("https://api.example.com/data") .ConfigureAwait(false); // 重要:避免捕获上下文 var json = await response.Content.ReadAsStringAsync() .ConfigureAwait(false); return JsonSerializer.Deserialize<Data>(json); }

何时使用:在类库、后台服务、非UI相关的代码中。何时不用:在UI事件处理程序(如按钮点击事件)中,如果你需要在await之后更新UI控件,则不能使用ConfigureAwait(false),因为更新UI必须在UI线程上执行。

2.3 IAsyncEnumerable 与流式数据处理

对于需要异步枚举数据序列的场景(如分页查询数据库、流式读取大文件、实时消息推送),IAsyncEnumerable<T>是比返回Task<List<T>>更高效的选择。它允许你在数据可用时逐个产出(yield),而不是等待所有数据都加载到内存中。

public async IAsyncEnumerable<Product> StreamProductsAsync(int categoryId) { int page = 0; const int pageSize = 50; List<Product> products; do { // 模拟分页查询数据库 products = await _dbContext.Products .Where(p => p.CategoryId == categoryId) .Skip(page * pageSize) .Take(pageSize) .ToListAsync(); foreach (var product in products) { // 每查询到一批数据,就立即产出给调用者处理 yield return product; } page++; } while (products.Count == pageSize); // 还有下一页则继续 } // 消费端 await foreach (var product in StreamProductsAsync(1)) { Console.WriteLine($"Processing {product.Name}"); // 处理每个产品,内存压力小 }

3. LINQ的深度解析:表达式树与IQueryable的魔法

LINQ(Language Integrated Query)是C#的标志性特性之一。进阶使用要求我们理解其两种执行模式:LINQ to Objects(在内存中执行)和LINQ to Providers(如LINQ to SQL, Entity Framework Core,将查询转换为其他语言,如SQL)。

3.1 IEnumerable vs IQueryable

这是最核心的区别,理解错误会导致严重的性能问题(如从数据库读取全部数据到内存后再过滤)。

  • IEnumerable<T>:代表一个在内存中的序列。对它的LINQ操作(如Where,Select)使用的是C#的委托(Func<T, bool>),操作在客户端内存中执行。
  • IQueryable<T>:代表一个可远程执行的查询。对它的LINQ操作使用的是表达式树(Expression<Func<T, bool>>),这些操作可以被查询提供者(如数据库驱动)解析并转换为目标语言(如SQL),在数据源端执行。
// 错误示例:导致“SELECT * FROM Products”,然后在内存中过滤 var badQuery = _dbContext.Products.AsEnumerable() // 强制转换为IEnumerable .Where(p => p.Price > 100); // 正确示例:生成“SELECT * FROM Products WHERE Price > 100” var goodQuery = _dbContext.Products // 本身就是IQueryable<Product> .Where(p => p.Price > 100) .ToListAsync();

黄金法则:只要可能,应始终保持IQueryable<T>类型,直到真正需要具体结果(通过ToListAsync(),FirstOrDefaultAsync()等)时再执行查询。这允许Entity Framework Core构建最优化的SQL。

3.2 表达式树(Expression Trees)实战

表达式树允许你在运行时将代码逻辑表示为数据结构,然后对其进行解析、转换或编译。这是实现动态查询、规则引擎或特定领域语言(DSL)的基础。

假设我们需要构建一个动态的产品过滤系统,用户可以通过UI选择不同的过滤条件。

using System.Linq.Expressions; public Expression<Func<Product, bool>> BuildProductFilter( decimal? minPrice, decimal? maxPrice, string categoryName, bool? inStock) { // 从表达式“p => true”开始(即不过滤) Expression<Func<Product, bool>> filter = p => true; var parameter = Expression.Parameter(typeof(Product), "p"); if (minPrice.HasValue) { // 构建 p.Price >= minPrice 的表达式 var priceProperty = Expression.Property(parameter, nameof(Product.Price)); var minPriceConstant = Expression.Constant(minPrice.Value); var priceComparison = Expression.GreaterThanOrEqual(priceProperty, minPriceConstant); // 将新条件与现有过滤器用AND连接 filter = CombineFilters(filter, priceComparison, parameter); } if (maxPrice.HasValue) { var priceProperty = Expression.Property(parameter, nameof(Product.Price)); var maxPriceConstant = Expression.Constant(maxPrice.Value); var priceComparison = Expression.LessThanOrEqual(priceProperty, maxPriceConstant); filter = CombineFilters(filter, priceComparison, parameter); } if (!string.IsNullOrEmpty(categoryName)) { var categoryProperty = Expression.Property(parameter, nameof(Product.Category)); var nameProperty = Expression.Property(categoryProperty, nameof(Category.Name)); var categoryConstant = Expression.Constant(categoryName); var categoryComparison = Expression.Equal(nameProperty, categoryConstant); filter = CombineFilters(filter, categoryComparison, parameter); } if (inStock.HasValue) { var stockProperty = Expression.Property(parameter, nameof(Product.StockQuantity)); var zeroConstant = Expression.Constant(0); BinaryExpression stockComparison = inStock.Value ? Expression.GreaterThan(stockProperty, zeroConstant) // p.StockQuantity > 0 : Expression.LessThanOrEqual(stockProperty, zeroConstant); // p.StockQuantity <= 0 filter = CombineFilters(filter, stockComparison, parameter); } return filter; } private Expression<Func<Product, bool>> CombineFilters( Expression<Func<Product, bool>> existingFilter, BinaryExpression newCondition, ParameterExpression parameter) { // 将现有的lambda表达式体与新的条件用AND连接 var existingBody = existingFilter.Body; var combinedBody = Expression.AndAlso(existingBody, newCondition); // 创建新的lambda表达式 return Expression.Lambda<Func<Product, bool>>(combinedBody, parameter); } // 使用动态构建的表达式进行查询 var dynamicFilter = BuildProductFilter(minPrice: 50, maxPrice: 200, categoryName: "Electronics", inStock: true); var filteredProducts = await _dbContext.Products .Where(dynamicFilter) // 这里会生成正确的SQL WHERE子句 .ToListAsync();

通过表达式树,我们构建的查询最终会被Entity Framework Core翻译成类似下面的高效SQL,所有过滤都在数据库端完成:

SELECT * FROM Products p INNER JOIN Categories c ON p.CategoryId = c.Id WHERE p.Price >= 50 AND p.Price <= 200 AND c.Name = 'Electronics' AND p.StockQuantity > 0

4. 依赖注入(DI)与控制反转(IoC)高级模式

依赖注入是现代.NET应用架构的核心。进阶使用意味着不仅要会注册服务,更要理解其生命周期、设计模式以及如何解决复杂场景。

4.1 服务生命周期的深入理解

ASP.NET Core提供了三种生命周期:

  • Transient(瞬时):每次请求时创建新实例。适用于轻量、无状态的服务。
  • Scoped(作用域):在同一Web请求(或逻辑作用域)内共享同一个实例。这是最常用的,适用于数据库上下文(DbContext)、仓储等。
  • Singleton(单例):在整个应用生命周期内只有一个实例。适用于配置服务、缓存客户端等。

关键陷阱:将一个Scoped服务注入到Singleton服务中。这会导致Scoped服务实际上也变成了Singleton(因为它被Singleton持有),可能引发并发问题或资源泄漏。

// 错误示例 public class SingletonService { private readonly IScopedService _scopedService; // 危险! public SingletonService(IScopedService scopedService) { _scopedService = scopedService; } } // 解决方案1:将SingletonService改为Scoped // 解决方案2:使用IServiceScopeFactory在需要时创建作用域 public class SafeSingletonService { private readonly IServiceScopeFactory _scopeFactory; public SafeSingletonService(IServiceScopeFactory scopeFactory) { _scopeFactory = scopeFactory; } public void DoWork() { using (var scope = _scopeFactory.CreateScope()) { var scopedService = scope.ServiceProvider.GetRequiredService<IScopedService>(); // 安全地使用scopedService } } }

4.2 基于接口与实现的多种注册方式

// 1. 最基本:具体类注册为自身(很少用,不利于测试) services.AddSingleton<MyService>(); // 2. 接口映射实现(推荐,支持解耦和测试) services.AddScoped<IMyService, MyService>(); // 3. 工厂模式注册:可以基于运行时参数创建实例 services.AddSingleton<IConnectionFactory>(serviceProvider => { var config = serviceProvider.GetRequiredService<IConfiguration>(); var connectionString = config.GetConnectionString("Default"); return new ConnectionFactory(connectionString); }); // 4. 注册多个实现,并通过IEnumerable<T>或特定类型解析 services.AddScoped<INotificationService, EmailNotificationService>(); services.AddScoped<INotificationService, SmsNotificationService>(); services.AddScoped<INotificationService, PushNotificationService>(); // 使用时可以注入IEnumerable<INotificationService>获取所有实现 public class OrderProcessor { private readonly IEnumerable<INotificationService> _notifiers; public OrderProcessor(IEnumerable<INotificationService> notifiers) { _notifiers = notifiers; } public void CompleteOrder(Order order) { // ... 处理订单逻辑 foreach (var notifier in _notifiers) { notifier.SendNotification(order); } } }

4.3 选项模式(Options Pattern)与强类型配置

选项模式是管理应用程序配置的推荐方式,它提供了强类型访问和变更检测。

// 1. 定义选项类 public class ExternalApiOptions { public const string SectionName = "ExternalApi"; public string BaseUrl { get; set; } = string.Empty; public string ApiKey { get; set; } = string.Empty; public int TimeoutSeconds { get; set; } = 30; } // 2. 在appsettings.json中配置 { "ExternalApi": { "BaseUrl": "https://api.example.com", "ApiKey": "your-secret-key-here", "TimeoutSeconds": 45 } } // 3. 在Program.cs或Startup.cs中注册 builder.Services.Configure<ExternalApiOptions>( builder.Configuration.GetSection(ExternalApiOptions.SectionName)); // 4. 使用方式一:直接注入IOptions<T> public class ApiClient { private readonly ExternalApiOptions _options; private readonly HttpClient _httpClient; public ApiClient(IOptions<ExternalApiOptions> options, HttpClient httpClient) { _options = options.Value; // 注意:.Value获取配置实例 _httpClient = httpClient; _httpClient.BaseAddress = new Uri(_options.BaseUrl); _httpClient.Timeout = TimeSpan.FromSeconds(_options.TimeoutSeconds); _httpClient.DefaultRequestHeaders.Add("X-API-Key", _options.ApiKey); } } // 5. 使用方式二:注入IOptionsSnapshot<T>(支持配置热重载) public class ApiClientWithSnapshot { private readonly IOptionsSnapshot<ExternalApiOptions> _optionsSnapshot; // IOptionsSnapshot在Scoped生命周期内是固定的,但不同请求可能获取到新的配置(如果配置源支持热更新) }

5. 反射、特性与动态编程

反射允许在运行时检查类型、创建对象、调用方法等。特性(Attribute)则为代码元素添加元数据。两者结合,可以构建极其灵活的框架。

5.1 自定义特性与元数据驱动开发

假设我们要实现一个简单的权限检查系统,不同的控制器方法需要不同的权限。

// 1. 定义自定义特性 [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] public class RequiredPermissionAttribute : Attribute { public string PermissionName { get; } public RequiredPermissionAttribute(string permissionName) { PermissionName = permissionName; } } // 2. 在控制器方法上应用特性 public class OrderController : ControllerBase { [HttpGet("orders")] public IActionResult GetAllOrders() { // 任何人都可以查看订单列表 return Ok(); } [HttpPost("orders")] [RequiredPermission("Order.Create")] // 需要创建订单的权限 public IActionResult CreateOrder(OrderDto dto) { // 创建订单逻辑 return Created(); } [HttpDelete("orders/{id}")] [RequiredPermission("Order.Delete")] // 需要删除订单的权限 public IActionResult DeleteOrder(int id) { // 删除订单逻辑 return NoContent(); } } // 3. 创建Action过滤器来检查权限 public class PermissionAuthorizationFilter : IAsyncActionFilter { private readonly IPermissionService _permissionService; public PermissionAuthorizationFilter(IPermissionService permissionService) { _permissionService = permissionService; } public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) { // 获取当前Action方法上的RequiredPermissionAttribute var permissionAttribute = context.ActionDescriptor.EndpointMetadata .OfType<RequiredPermissionAttribute>() .FirstOrDefault(); if (permissionAttribute != null) { // 需要权限检查 var user = context.HttpContext.User; var hasPermission = await _permissionService.CheckPermissionAsync( user.Identity.Name, permissionAttribute.PermissionName); if (!hasPermission) { context.Result = new ForbidResult(); return; // 中断执行,不调用Action方法 } } // 继续执行Action方法 await next(); } } // 4. 在Program.cs中注册全局过滤器 builder.Services.AddScoped<PermissionAuthorizationFilter>(); builder.Services.Configure<MvcOptions>(options => { options.Filters.Add<PermissionAuthorizationFilter>(); });

5.2 使用反射实现插件系统

反射的一个强大应用是构建支持动态加载程序集的插件系统。

// 定义插件接口 public interface IPlugin { string Name { get; } string Description { get; } Task ExecuteAsync(CancellationToken cancellationToken); } // 插件加载器 public class PluginLoader { private readonly List<IPlugin> _plugins = new(); public IReadOnlyList<IPlugin> Plugins => _plugins.AsReadOnly(); public void LoadPluginsFromDirectory(string pluginsDirectory) { if (!Directory.Exists(pluginsDirectory)) { Directory.CreateDirectory(pluginsDirectory); return; } var dllFiles = Directory.GetFiles(pluginsDirectory, "*.dll"); foreach (var dllPath in dllFiles) { try { // 加载程序集 var assembly = Assembly.LoadFrom(dllPath); // 查找所有实现了IPlugin接口的类型 var pluginTypes = assembly.GetTypes() .Where(t => typeof(IPlugin).IsAssignableFrom(t) && !t.IsAbstract && !t.IsInterface); foreach (var pluginType in pluginTypes) { // 创建插件实例(假设有无参构造函数) if (Activator.CreateInstance(pluginType) is IPlugin plugin) { _plugins.Add(plugin); Console.WriteLine($"Loaded plugin: {plugin.Name}"); } } } catch (Exception ex) { Console.WriteLine($"Failed to load plugin from {dllPath}: {ex.Message}"); } } } public async Task ExecuteAllPluginsAsync(CancellationToken cancellationToken = default) { foreach (var plugin in _plugins) { try { Console.WriteLine($"Executing plugin: {plugin.Name}"); await plugin.ExecuteAsync(cancellationToken); } catch (Exception ex) { Console.WriteLine($"Plugin {plugin.Name} failed: {ex.Message}"); } } } } // 示例插件实现(在独立的类库项目中) public class ReportGeneratorPlugin : IPlugin { public string Name => "Report Generator"; public string Description => "Generates daily sales reports"; public async Task ExecuteAsync(CancellationToken cancellationToken) { // 模拟生成报告 await Task.Delay(1000, cancellationToken); Console.WriteLine($"[{DateTime.Now}] Daily report generated."); } }

6. 性能优化与高级调试技巧

6.1 使用Span 和Memory 进行零分配操作

在处理字符串或数组时,避免不必要的分配可以显著提升性能。Span<T>Memory<T>提供了对连续内存区域的类型安全访问,且通常分配在栈上(或使用池化内存)。

// 传统方式:产生多个字符串分配 public static string TruncateAndAppendOld(string original, int maxLength, string suffix) { if (original.Length <= maxLength) return original; // Substring创建新的字符串 var truncated = original.Substring(0, maxLength); // + 操作符创建另一个新的字符串 return truncated + suffix; } // 使用Span<T>的优化方式 public static string TruncateAndAppendNew(ReadOnlySpan<char> original, int maxLength, ReadOnlySpan<char> suffix) { if (original.Length <= maxLength) { // 如果不需要截取,直接返回新字符串(或考虑返回原字符串) return new string(original); } // 在栈上分配临时空间(不涉及堆分配) Span<char> buffer = stackalloc char[maxLength + suffix.Length]; // 复制原始字符串的前maxLength个字符 original.Slice(0, maxLength).CopyTo(buffer); // 复制后缀 suffix.CopyTo(buffer.Slice(maxLength)); // 一次性创建结果字符串 return new string(buffer); } // 使用示例 var longText = "This is a very long text that needs to be truncated"; var result = TruncateAndAppendNew(longText.AsSpan(), 20, "...".AsSpan()); Console.WriteLine(result); // 输出: "This is a very long..."

6.2 使用BenchmarkDotNet进行性能基准测试

猜测性能瓶颈是不可靠的。使用BenchmarkDotNet可以科学地测量代码性能。

首先,安装NuGet包:BenchmarkDotNet

using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Running; [MemoryDiagnoser] // 同时分析内存分配 [RankColumn] // 添加排名列 public class StringConcatenationBenchmark { private readonly string[] _words = Enumerable.Range(1, 100) .Select(i => $"Word{i}") .ToArray(); [Benchmark(Baseline = true)] public string ConcatenateWithPlus() { string result = string.Empty; for (int i = 0; i < _words.Length; i++) { result += _words[i]; // 每次循环都创建新字符串 } return result; } [Benchmark] public string ConcatenateWithStringBuilder() { var sb = new StringBuilder(); for (int i = 0; i < _words.Length; i++) { sb.Append(_words[i]); } return sb.ToString(); } [Benchmark] public string ConcatenateWithStringJoin() { return string.Join(string.Empty, _words); } } class Program { static void Main(string[] args) { var summary = BenchmarkRunner.Run<StringConcatenationBenchmark>(); } }

运行此基准测试将生成详细的报告,显示每种方法的执行时间、内存分配等,帮助你做出基于数据的优化决策。

6.3 高级调试:使用条件断点、跟踪点和性能诊断工具

条件断点:当满足特定条件时才中断。右键点击断点 → 条件。例如,在循环中设置条件i == 50,只在第50次迭代时中断。

跟踪点(Tracepoint):在不中断执行的情况下记录信息。右键点击行号 → 断点 → 插入跟踪点。可以输出变量值、调用堆栈等到输出窗口。

性能诊断工具(Performance Profiler):在Visual Studio中,选择“调试” → “性能探查器”。可以分析CPU使用率、内存分配、异步操作等,直观地找到性能热点。

7. 实战:构建一个可扩展的缓存抽象层

让我们综合运用上述概念,构建一个支持多种后端(内存、Redis、分布式)且可通过配置切换的缓存抽象层。

7.1 定义缓存接口与选项

// 缓存接口 public interface ICacheService { Task<T?> GetAsync<T>(string key, CancellationToken cancellationToken = default); Task SetAsync<T>(string key, T value, TimeSpan? expiration = null, CancellationToken cancellationToken = default); Task RemoveAsync(string key, CancellationToken cancellationToken = default); Task<bool> ExistsAsync(string key, CancellationToken cancellationToken = default); } // 缓存选项 public class CacheOptions { public string Provider { get; set; } = "Memory"; // Memory, Redis public string? RedisConnectionString { get; set; } public TimeSpan DefaultExpiration { get; set; } = TimeSpan.FromMinutes(30); }

7.2 实现内存缓存

public class MemoryCacheService : ICacheService { private readonly IMemoryCache _memoryCache; private readonly CacheOptions _options; public MemoryCacheService(IMemoryCache memoryCache, IOptions<CacheOptions> options) { _memoryCache = memoryCache; _options = options.Value; } public Task<T?> GetAsync<T>(string key, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); var value = _memoryCache.Get<T>(key); return Task.FromResult(value); } public Task SetAsync<T>(string key, T value, TimeSpan? expiration = null, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); var cacheOptions = new MemoryCacheEntryOptions { AbsoluteExpirationRelativeToNow = expiration ?? _options.DefaultExpiration }; _memoryCache.Set(key, value, cacheOptions); return Task.CompletedTask; } public Task RemoveAsync(string key, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); _memoryCache.Remove(key); return Task.CompletedTask; } public Task<bool> ExistsAsync(string key, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); var exists = _memoryCache.TryGetValue(key, out _); return Task.FromResult(exists); } }

7.3 实现Redis缓存

public class RedisCacheService : ICacheService { private readonly IConnectionMultiplexer _redis; private readonly IDatabase _database; private readonly CacheOptions _options; public RedisCacheService(IConnectionMultiplexer redis, IOptions<CacheOptions> options) { _redis = redis; _database = redis.GetDatabase(); _options = options.Value; } public async Task<T?> GetAsync<T>(string key, CancellationToken cancellationToken = default) { var value = await _database.StringGetAsync(key); if (value.IsNullOrEmpty) return default; return JsonSerializer.Deserialize<T>(value!); } public async Task SetAsync<T>(string key, T value, TimeSpan? expiration = null, CancellationToken cancellationToken = default) { var json = JsonSerializer.Serialize(value); var expiry = expiration ?? _options.DefaultExpiration; await _database.StringSetAsync(key, json, expiry); } public Task RemoveAsync(string key, CancellationToken cancellationToken = default) { return _database.KeyDeleteAsync(key); } public async Task<bool> ExistsAsync(string key, CancellationToken cancellationToken = default) { return await _database.KeyExistsAsync(key); } }

7.4 使用工厂模式动态选择实现

public interface ICacheServiceFactory { ICacheService CreateCacheService(); } public class CacheServiceFactory : ICacheServiceFactory { private readonly IServiceProvider _serviceProvider; private readonly CacheOptions _options; public CacheServiceFactory(IServiceProvider serviceProvider, IOptions<CacheOptions> options) { _serviceProvider = serviceProvider; _options = options.Value; } public ICacheService CreateCacheService() { return _options.Provider.ToLowerInvariant() switch { "redis" => _serviceProvider.GetRequiredService<RedisCacheService>(), "memory" or _ => _serviceProvider.GetRequiredService<MemoryCacheService>() }; } } // 扩展方法,简化服务注册 public static class CacheServiceExtensions { public static IServiceCollection AddCacheService(this IServiceCollection services, Action<CacheOptions> configureOptions) { services.Configure(configureOptions); // 注册两种实现 services.AddScoped<MemoryCacheService>(); services.AddScoped<RedisCacheService>(); // 注册工厂 services.AddScoped<ICacheServiceFactory, CacheServiceFactory>(); // 注册一个便捷的ICacheService,通过工厂解析 services.AddScoped<ICacheService>(sp => { var factory = sp.GetRequiredService<ICacheServiceFactory>(); return factory.CreateCacheService(); }); return services; } } // 在Program.cs中使用 builder.Services.AddCacheService(options => { options.Provider = builder.Configuration["Cache:Provider"] ?? "Memory"; options.RedisConnectionString = builder.Configuration.GetConnectionString("Redis"); options.DefaultExpiration = TimeSpan.FromMinutes( builder.Configuration.GetValue<int>("Cache:DefaultExpirationMinutes", 30)); }); // 在控制器中使用 public class ProductController : ControllerBase { private readonly ICacheService _cache; private readonly IProductRepository _repository; public ProductController(ICacheService cache, IProductRepository repository) { _cache = cache; _repository = repository; } [HttpGet("{id}")] public async Task<IActionResult> GetProduct(int id, CancellationToken cancellationToken) { var cacheKey = $"product_{id}"; // 尝试从缓存获取 var product = await _cache.GetAsync<Product>(cacheKey, cancellationToken); if (product != null) { return Ok(product); } // 缓存未命中,从数据库获取 product = await _repository.GetByIdAsync(id, cancellationToken); if (product == null) { return NotFound(); } // 存入缓存,下次请求可直接命中 await _cache.SetAsync(cacheKey, product, TimeSpan.FromMinutes(10), cancellationToken); return Ok(product); } }

这个缓存抽象层展示了如何结合接口、依赖注入、选项模式、工厂模式来构建一个可扩展、可配置的组件。通过修改配置,你可以在内存缓存和Redis缓存之间无缝切换,而业务代码无需任何修改。

8. 常见问题与排查思路

问题现象可能原因排查方式解决方案
InvalidOperationException: Cannot resolve scoped service from root provider在单例服务中直接注入了Scoped服务检查服务注册生命周期,查看堆栈跟踪使用IServiceScopeFactory在需要时创建作用域,或将单例服务改为Scoped
System.InvalidOperationException: A second operation was started on this context instance多个线程同时使用同一个DbContext实例检查是否在单例服务中注入了DbContext,或是否在异步方法中未正确使用await确保DbContext以Scoped生命周期注册,异步操作正确使用await
System.Text.Json.JsonException: The JSON value could not be converted to...JSON反序列化时类型不匹配检查API返回的JSON结构与目标类型是否一致,特别是日期格式、枚举值等使用JsonSerializerOptions配置自定义转换器,或调整模型属性
async方法返回Task但忘记使用await遗漏await关键字,导致异常无法被捕获或任务未完成编译器警告CS4014,或运行时任务未按预期完成检查所有异步调用是否都正确使用了await,或明确使用Task.WhenAll处理多个任务
IQueryable查询性能慢,内存占用高在内存中执行了本应在数据库执行的过滤操作检查是否在IQueryable上过早调用了ToList()ToArray()AsEnumerable()保持IQueryable类型直到最后,让ORM生成高效SQL
依赖注入时出现System.InvalidOperationException: Unable to resolve service服务未在DI容器中注册,或注册的生命周期不匹配检查Program.csStartup.cs中的服务注册代码确保所有需要的服务都已正确注册,注意接口与实现的映射关系

9. 最佳实践与工程建议

  1. 异步全栈:对于I/O密集型操作(数据库、HTTP请求、文件读写),始终使用异步方法。从控制器到仓储层,保持异步调用链的完整性。

  2. 合理使用生命周期

    • DbContext、仓储、业务服务注册为Scoped
    • 将配置服务、HttpClientFactory、缓存客户端注册为Singleton
    • 避免将Scoped服务注入到Singleton服务中。
  3. 表达式树的谨慎使用:表达式树强大但复杂。仅在需要将C#代码转换为其他查询语言(如SQL)或构建动态查询时使用。对于简单的动态条件,考虑使用PredicateBuilder等库。

  4. 缓存策略

    • 明确缓存粒度:按需缓存,避免过度缓存。
    • 设置合理的过期时间,平衡数据新鲜度与性能。
    • 考虑缓存穿透、缓存击穿、缓存雪崩问题,可使用空值缓存、互斥锁、随机过期时间等策略。
  5. 错误处理与日志记录

    • 使用结构化日志(如Serilog)而非Console.WriteLine
    • 在异步方法中始终传递CancellationToken以支持取消操作。
    • 使用try-catch时,只捕获你能处理的异常,其他异常应向上传播。
  6. 测试策略

    • 为业务逻辑编写单元测试。
    • 为API端点编写集成测试。
    • 使用Moq、NSubstitute等框架模拟依赖。
    • 对性能敏感代码使用BenchmarkDotNet进行基准测试。
  7. 代码可维护性

    • 遵循单一职责原则,每个类/方法只做一件事。
    • 使用有意义的命名,避免缩写。
    • 编写清晰的XML注释,特别是公共API。
    • 定期进行代码审查,使用SonarQube等工具进行静态代码分析。

C#的进阶之路是一个从“会写代码”到“会设计系统”的转变过程。掌握这些高级特性不是目的,而是手段,目的是为了构建更健壮、更高效、更易维护的应用程序。真正的进阶体现在你能够根据具体场景,在语言的众多特性中做出恰当的选择,并理解这些选择背后的权衡。建议你在实际项目中尝试应用这些模式,从重构一个小模块开始,逐步积累经验。同时,关注.NET社区的最新动态,如.NET 8/9的新特性、Source Generators、Minimal APIs等,持续学习才能保持竞争力。