如何检测不可变对象中构造函数参数与属性名的不匹配问题?
Great question! This is a super common pitfall with immutable POCOs and Json.NET—especially in large codebases where manual checks just aren't feasible. Let's walk through two practical approaches to automate detection of these problematic classes:
Approach 1: Runtime Validation Tool
This method simulates the actual serialization/deserialization round-trip for every immutable POCO in your assembly, then checks if the object state is preserved. It's straightforward and catches exactly the issues you'd see at runtime.
How it works:
- Identify immutable POCOs: Classes with all public instance properties read-only (no public
setters) and at least one public constructor. - Generate test instances for each class (we'll use AutoFixture to handle this automatically).
- Serialize and deserialize the instance.
- Compare the original and deserialized object's property values—if any don't match, flag the class as problematic.
Example Code:
using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using Newtonsoft.Json; using AutoFixture; public class ImmutablePocoValidator { public List<string> ScanAssemblyForIssues(Assembly targetAssembly) { var problematicClasses = new List<string>(); var fixture = new Fixture(); // Find all immutable POCO candidates var immutableTypes = targetAssembly.GetTypes() .Where(t => t.IsClass && !t.IsAbstract && t.GetProperties(BindingFlags.Public | BindingFlags.Instance) .All(p => !p.CanWrite) && t.GetConstructors(BindingFlags.Public | BindingFlags.Instance).Any()); foreach (var type in immutableTypes) { try { // Create a test instance with random valid data var original = fixture.Create(type); // Serialize to JSON var json = JsonConvert.SerializeObject(original); // Deserialize back to an object var deserialized = JsonConvert.DeserializeObject(json, type); // Check if all properties match between original and deserialized var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance); var hasMismatch = properties.Any(p => { var originalVal = p.GetValue(original); var deserializedVal = p.GetValue(deserialized); return !Equals(originalVal, deserializedVal); }); if (hasMismatch) { problematicClasses.Add($"{type.FullName}: Property values don't match after deserialization"); } } catch (Exception ex) { problematicClasses.Add($"{type.FullName}: Test failed - {ex.Message}"); } } return problematicClasses; } } // Usage example: // var validator = new ImmutablePocoValidator(); // var issues = validator.ScanAssemblyForIssues(typeof(SomeImmutablePoco).Assembly); // foreach (var issue in issues) Console.WriteLine(issue);
Pros: Directly mimics real runtime behavior, catches edge cases you might miss with static analysis.
Cons: Requires runtime execution, depends on test data generation (AutoFixture simplifies this a lot).
Approach 2: Static Code Analysis (Roslyn Analyzer)
If you want to catch issues during compilation (before they hit runtime), a Roslyn analyzer is perfect. It scans your code as you write it and flags problematic immutable POCOs immediately.
Detection Logic:
- Identify immutable POCOs (same criteria as above).
- For each constructor parameter:
- Check if it has a
[JsonProperty]attribute explicitly mapping to a class property. - If not, check if the parameter name (case-insensitive) matches any public property name.
- Check if it has a
- Flag parameters that fail both checks—these will cause Json.NET to fail to map values during deserialization.
Example Analyzer Code:
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; using System.Collections.Immutable; [DiagnosticAnalyzer(LanguageNames.CSharp)] public class ImmutablePocoJsonAnalyzer : DiagnosticAnalyzer { public const string DiagnosticId = "ImmutablePocoJsonWarning"; private const string Title = "Potential Json.NET Deserialization Issue"; private const string MessageFormat = "Constructor parameter '{0}' doesn't map to a class property (add [JsonProperty] or rename to match a property)"; private const string Description = "Immutable POCO constructor parameters must match a property name (case-insensitive) or use [JsonProperty] to define mapping."; private const string Category = "Serialization"; private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor( DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Warning, isEnabledByDefault: true, description: Description); public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule); public override void Initialize(AnalysisContext context) { context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.EnableConcurrentExecution(); context.RegisterSymbolAction(AnalyzeNamedType, SymbolKind.NamedType); } private void AnalyzeNamedType(SymbolAnalysisContext context) { var namedType = (INamedTypeSymbol)context.Symbol; if (!namedType.IsClass || namedType.IsAbstract) return; // Verify all public instance properties are read-only var publicProperties = namedType.GetMembers() .OfType<IPropertySymbol>() .Where(p => p.IsPublic && p.Instance && !p.IsStatic); if (!publicProperties.All(p => !p.SetMethod?.IsPublic ?? true)) return; // Check each public constructor foreach (var ctor in namedType.GetMembers().OfType<IMethodSymbol>().Where(m => m.IsConstructor && m.IsPublic)) { foreach (var param in ctor.Parameters) { // Skip if parameter has explicit JsonProperty mapping if (param.GetAttributes().Any(a => a.AttributeClass?.Name == "JsonPropertyAttribute")) continue; // Check if parameter name matches any property (case-insensitive) var matchingProperty = publicProperties.FirstOrDefault(p => string.Equals(p.Name, param.Name, StringComparison.OrdinalIgnoreCase)); if (matchingProperty == null) { // Report the issue var diagnostic = Diagnostic.Create(Rule, param.Locations[0], param.Name); context.ReportDiagnostic(diagnostic); } } } } }
Pros: Catches issues during development/compilation, integrates with IDEs (Visual Studio will show warnings in real-time), works great in CI/CD pipelines.
Cons: Requires writing Roslyn analyzer code (a bit more complex, but well worth it for large teams).
Bonus Best Practice
To avoid these issues entirely, get into the habit of explicitly mapping constructor parameters to properties using the [JsonProperty] attribute, like this:
class SomeImmutablePoco { public SomeImmutablePoco([JsonProperty("Id")] int pocoId, string name) { Id = pocoId; Name = name; } public int Id { get; } public string Name { get; } }
This removes ambiguity and makes your code's intent crystal clear for both Json.NET and other developers.
内容的提问来源于stack exchange,提问作者spender

