You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何优雅实现测验评分策略的分级回退机制?基于.NET EF Core的优化方案问询

Cleaner Implementation for Hierarchical Quiz Scoring Strategy with Fallback

Great question! Your current setup gets the job done, but coupling business logic directly to EF Core entities violates the Single Responsibility Principle, and the lookup logic can be streamlined for better performance and maintainability. Let's walk through a more elegant approach that aligns with .NET conventions and SOLID principles.

Core Issues with Current Implementation

  1. Entity Responsibility Creep: EvaluationStrategy is an EF Core entity (data model) but now handles business logic (strategy lookup), making it harder to test and maintain.
  2. Inefficient Lookups: Using FirstOrDefault on collections every time you need a strategy adds unnecessary overhead.
  3. Rigid Fallback Configuration: The default strategy is hardcoded, not leveraging .NET's flexible configuration system.

Proposed Solution

We'll split the concerns:

  • Keep EF entities as pure data models
  • Create a dedicated service to handle strategy lookup (with caching for efficiency)
  • Use .NET's IOptions pattern for configurable fallback behavior

Step 1: Define Configurable Fallback Options

First, create a configuration class for the default strategy, plus an extension method to make setup feel idiomatic to .NET:

public class QuizScoreFallbackOptions
{
    public decimal Correct { get; set; } = 1;
    public decimal Incorrect { get; set; } = 0;
    public decimal Unattempted { get; set; } = 0;
}

// Extension method for fluent configuration
public static class QuizScoreFallbackOptionsExtensions
{
    public static void UseFallbackStrategy(this QuizScoreFallbackOptions options, decimal correct, decimal incorrect, decimal unattempted)
    {
        options.Correct = correct;
        options.Incorrect = incorrect;
        options.Unattempted = unattempted;
    }
}

Register this in your startup/Program.cs (for .NET 5, use Startup.cs):

services.Configure<QuizScoreFallbackOptions>(options =>
{
    // Configure directly, or bind from appsettings.json
    options.UseFallbackStrategy(correct: 1, incorrect: 0, unattempted: 0);
});

Step 2: Create a Strategy Resolver Service

This service will handle all strategy lookup logic, with caching to avoid reprocessing strategies for the same EvaluationStrategy multiple times:

public interface IQuizScoreStrategyResolver
{
    ScoreStrategy GetStrategyForQuestion(EvaluationStrategy evaluationStrategy, Question question);
}

public class QuizScoreStrategyResolver : IQuizScoreStrategyResolver
{
    private readonly IOptions<QuizScoreFallbackOptions> _fallbackOptions;
    private readonly ConcurrentDictionary<int, StrategyLookupCache> _strategyCache = new();

    public QuizScoreStrategyResolver(IOptions<QuizScoreFallbackOptions> fallbackOptions)
    {
        _fallbackOptions = fallbackOptions;
    }

    public ScoreStrategy GetStrategyForQuestion(EvaluationStrategy evaluationStrategy, Question question)
    {
        // Get or build cached strategy mappings for this evaluation strategy
        var cache = _strategyCache.GetOrAdd(evaluationStrategy.Id, _ => BuildStrategyCache(evaluationStrategy));

        // 1. Check question-specific strategy first
        if (cache.QuestionStrategies.TryGetValue(question.Id, out var questionStrategy))
        {
            return questionStrategy;
        }

        // 2. Check difficulty-specific strategy
        if (cache.DifficultyStrategies.TryGetValue(question.Difficulty, out var difficultyStrategy))
        {
            return difficultyStrategy;
        }

        // 3. Check global quiz strategy
        if (cache.GlobalStrategy != null)
        {
            return cache.GlobalStrategy;
        }

        // 4. Fall back to configured default
        var fallback = _fallbackOptions.Value;
        return new ScoreStrategy
        {
            Correct = fallback.Correct,
            Incorrect = fallback.Incorrect,
            Unattempted = fallback.Unattempted
        };
    }

    // Preprocess strategies into fast-lookup dictionaries
    private StrategyLookupCache BuildStrategyCache(EvaluationStrategy evaluationStrategy)
    {
        var cache = new StrategyLookupCache();

        foreach (var strategy in evaluationStrategy.ScoreStrategies)
        {
            switch (strategy)
            {
                case QuestionScoreStrategy qs:
                    cache.QuestionStrategies.TryAdd(qs.Question.Id, qs);
                    break;
                case DifficultyScoreStrategy ds:
                    cache.DifficultyStrategies.TryAdd(ds.Difficulty, ds);
                    break;
                case ScoreStrategy s when cache.GlobalStrategy == null:
                    cache.GlobalStrategy = s;
                    break;
            }
        }

        return cache;
    }

    // Internal cache class to store grouped strategies
    private class StrategyLookupCache
    {
        public Dictionary<int, QuestionScoreStrategy> QuestionStrategies { get; } = new();
        public Dictionary<QuestionDifficulty, DifficultyScoreStrategy> DifficultyStrategies { get; } = new();
        public ScoreStrategy GlobalStrategy { get; set; }
    }
}

Register the resolver in your DI container:

services.AddScoped<IQuizScoreStrategyResolver, QuizScoreStrategyResolver>();

Step 3: Simplify the EF Entity

Strip all business logic from EvaluationStrategy to return it to its role as a pure data model:

public class EvaluationStrategy {
    public int Id { get; set; }
    public int QuizId { get; set; }
    public decimal MaxScore { get; set; }
    public decimal PassingScore { get; set; }
    public IEnumerable<ScoreStrategy> ScoreStrategies { get; set; }
}

Step 4: Usage Example

Inject the resolver wherever you need to calculate scores:

public class QuizScoringService
{
    private readonly IQuizScoreStrategyResolver _strategyResolver;

    public QuizScoringService(IQuizScoreStrategyResolver strategyResolver)
    {
        _strategyResolver = strategyResolver;
    }

    public decimal CalculateQuestionScore(EvaluationStrategy evalStrategy, Question question, bool isCorrect, bool isAttempted)
    {
        var strategy = _strategyResolver.GetStrategyForQuestion(evalStrategy, question);
        
        return isAttempted 
            ? isCorrect ? strategy.Correct : strategy.Incorrect 
            : strategy.Unattempted;
    }
}

Key Benefits

  • Single Responsibility: Entities handle data, services handle logic—easier to test and maintain.
  • Efficient Lookups: Dictionaries provide O(1) access, and caching avoids reprocessing strategies.
  • Idiomatic Configuration: Uses .NET's IOptions pattern, supporting binding from appsettings.json, environment variables, etc.
  • Testability: Mock the resolver or fallback options to test edge cases without hitting the database.
  • Extensibility: Adding new strategy types only requires updating the BuildStrategyCache method and cache class—no changes to core lookup logic.

Your existing EF Core TPH mapping remains unchanged, as we're only modifying how we consume the data, not how it's stored.

内容的提问来源于stack exchange,提问作者Amal K

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.29 18:02:36