Web API返回散点图数据:如何用匿名类型规避大量分支判断?
Awesome question—having to handle 30 combinations with a giant block of if/else statements is a surefire way to write code that’s hard to maintain and debug. Let’s break down a clean, scalable approach using the strategy pattern paired with dictionary-based dispatch, plus anonymous types to skip creating dozens of unnecessary DTOs:
1. Abstract Score Retrieval Logic with Strategy Pattern
First, we’ll define a common interface for all score retrieval strategies. This lets us encapsulate the logic for each axis type (named score, attribute ID, domain ID) into separate, focused classes.
public interface IScoreProvider { // Returns employee IDs paired with their scores for the given axis IEnumerable<(int EmployeeId, decimal Score)> GetScores(Axis axis, int companyId); }
Next, implement a provider class for each axis type. This keeps your data access logic isolated and easy to test:
// Provider for named scores public class NamedScoreProvider : IScoreProvider { private readonly IEmployeeScoreRepository _scoreRepo; public NamedScoreProvider(IEmployeeScoreRepository scoreRepo) { _scoreRepo = scoreRepo; } public IEnumerable<(int EmployeeId, decimal Score)> GetScores(Axis axis, int companyId) { return _scoreRepo.GetNamedScores( companyId: companyId, teamId: axis.TeamId, scoreName: axis.NamedScore ); } } // Provider for attribute ID scores public class AttributeScoreProvider : IScoreProvider { private readonly IEmployeeAttributeRepository _attributeRepo; public AttributeScoreProvider(IEmployeeAttributeRepository attributeRepo) { _attributeRepo = attributeRepo; } public IEnumerable<(int EmployeeId, decimal Score)> GetScores(Axis axis, int companyId) { // Assume AttributeId is validated to be non-null before this point return _attributeRepo.GetAttributeScores( companyId: companyId, teamId: axis.TeamId, attributeId: axis.AttributeId.Value ); } } // Provider for domain ID scores public class DomainScoreProvider : IScoreProvider { private readonly IEmployeeDomainRepository _domainRepo; public DomainScoreProvider(IEmployeeDomainRepository domainRepo) { _domainRepo = domainRepo; } public IEnumerable<(int EmployeeId, decimal Score)> GetScores(Axis axis, int companyId) { // Assume DomainId is validated to be non-zero before this point return _domainRepo.GetDomainScores( companyId: companyId, teamId: axis.TeamId, domainId: axis.DomainId.Value ); } }
2. Use a Dictionary to Map Axis Types to Providers
Instead of if/else checks, we’ll use a dictionary to match each axis’s configuration to the correct provider. This makes adding new axis types trivial (just add a new entry to the dictionary).
In your controller’s constructor (using dependency injection to inject the providers):
private readonly Dictionary<Func<Axis, bool>, IScoreProvider> _scoreProviderMap; public EmployeeScoresController( NamedScoreProvider namedProvider, AttributeScoreProvider attributeProvider, DomainScoreProvider domainProvider ) { _scoreProviderMap = new Dictionary<Func<Axis, bool>, IScoreProvider> { // Match axis with non-empty NamedScore { ax => !string.IsNullOrWhiteSpace(ax.NamedScore), namedProvider }, // Match axis with non-null AttributeId { ax => ax.AttributeId.HasValue, attributeProvider }, // Match axis with non-zero DomainId { ax => ax.DomainId.HasValue && ax.DomainId != 0, domainProvider } }; }
3. Add Validation to Ensure Valid Axis Configurations
Before processing, add a validation step to make sure each axis has exactly one valid score type specified (no mixed values):
private bool IsAxisValid(Axis axis) { var validFlags = new List<bool> { !string.IsNullOrWhiteSpace(axis.NamedScore), axis.AttributeId.HasValue, axis.DomainId.HasValue && axis.DomainId != 0 }; // Ensure exactly one score type is set return validFlags.Count(flag => flag) == 1; }
4. Implement the Controller Logic
Now, tie it all together to fetch scores for both axes, merge them, and return anonymous types for the scatterplot:
[HttpPost("{id}/scatterplot")] public ActionResult Scatterplot([FromRoute] int id, [FromBody] ScatterplotAxis axisRequest) { // Validate both axes first if (!IsAxisValid(axisRequest.XAxis) || !IsAxisValid(axisRequest.YAxis)) { return BadRequest("Each axis must specify exactly one score type (named score, attribute ID, or domain ID)."); } // Helper method to get scores for a single axis IEnumerable<(int EmployeeId, decimal Score)> GetAxisScores(Axis axis) { var provider = _scoreProviderMap.First(pair => pair.Key(axis)).Value; return provider.GetScores(axis, id); } // Fetch scores for X and Y axes, convert to dictionaries for fast lookup var xScores = GetAxisScores(axisRequest.XAxis).ToDictionary(s => s.EmployeeId); var yScores = GetAxisScores(axisRequest.YAxis).ToDictionary(s => s.EmployeeId); // Find employees present in both datasets and generate scatterplot data var commonEmployeeIds = xScores.Keys.Intersect(yScores.Keys); var scatterplotData = commonEmployeeIds.Select(empId => new { EmployeeId = empId, XScore = xScores[empId].Score, YScore = yScores[empId].Score }); return Ok(scatterplotData); }
Key Benefits of This Approach
- No giant if/else blocks: All logic is encapsulated in focused provider classes.
- Scalable: Adding a new score type only requires a new provider class and a dictionary entry—no changes to existing logic (follows the Open/Closed Principle).
- Clean data return: Anonymous types let you return exactly the data the scatterplot needs without creating 30+ custom DTO classes.
- Testable: Each provider can be tested independently, making unit tests easier to write and maintain.
内容的提问来源于stack exchange,提问作者Paolo B

