ASP.NET 核心自定义验证创建模型的新实例

ASP.NET Core custom validation creates new instance of model

我正在玩 ASP.NET Core 并试图为一个简单的文字游戏想出一个 UI。您收到一个随机生成的长词,您需要根据长词提供的字母提交较短的词。

该应用程序还没有使用任何类型的存储库,它只是暂时将模型实例作为静态字段存储在控制器中。

我目前面临一个问题,每次验证新提交的单词时,都会创建一个新的游戏实例,这自然会保证呈现验证错误,因为每个游戏都会提供一个新的长单词。

我一定是对模型验证的工作方式有一些误解,但调试并没有给我任何比每次都显示一个新的长单词的验证上下文更好的线索。

我卡住了,请帮忙。

这是控制器:

public class HomeController : Controller
{
    private static WordGameModel _model;

    public IActionResult Index()
    {
        if (_model == null)
        {
            _model = new WordGameModel();
        }
        return View(_model);
    }

    [HttpPost]
    public IActionResult Index(WordGameModel incomingModel)
    {
        if (ModelState.IsValid)
        {
            _model.Words.Add(incomingModel.ContainedWordCandidate);
            return RedirectToAction(nameof(Index), _model);
        }
        return View(_model);
    }
}

游戏模型:

public class WordGameModel
{
    public WordGameModel()
    {
        if (DictionaryModel.Dictionary == null) DictionaryModel.LoadDictionary();
        LongWord = DictionaryModel.GetRandomLongWord();
        Words = new List<string>();
    }

    public string LongWord { get; set; }
    public List<string> Words { get; set; }

    [Required(ErrorMessage = "Empty word is not allowed")]
    [MinLength(5, ErrorMessage = "A word shouldn't be shorter than 5 characters")]
    [MatchesLettersInLongWord]
    [NotSubmittedPreviously]
    public string ContainedWordCandidate { get; set; }

    public bool WordWasNotSubmittedPreviously() => !Words.Contains(ContainedWordCandidate);
    public bool WordMatchesLettersInLongWord()
    {
        if (string.IsNullOrWhiteSpace(ContainedWordCandidate)) return false;
        return ContainedWordCandidate.All(letter => LongWord.Contains(letter));
    }
}

验证失败的自定义验证属性:

internal class MatchesLettersInLongWord : ValidationAttribute
{
    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        WordGameModel model = (WordGameModel) validationContext.ObjectInstance;

        if (model.WordMatchesLettersInLongWord()) return ValidationResult.Success;

        return new ValidationResult("The submitted word contains characters that the long word doesn't contain");
    }
}

查看:

@model WordGameModel

<div class="row">
    <div class="col-md-12">
        <h2>@Model.LongWord</h2>
    </div>
</div>

<div class="row">
    <div class="col-md-6">
        <form id="wordForm" method="post">
            <div>
                <input id="wordInput" asp-for="ContainedWordCandidate"/>
                <input type="submit" name="Add" value="Add"/>
                <span asp-validation-for="ContainedWordCandidate"></span>
            </div>

        </form>
    </div>
</div>

<div class="row">
    <div class="col-md-6">
        <ul>
            @foreach (var word in @Model.Words)
            {
                <li>@word</li>
            }
        </ul>
    </div>
</div>

谢谢。

对于您在 HomeController 中的每个操作请求,mvc 框架都会为此创建一个新的控制器实例。返回响应后,它将处理控制器。

控制器字段和对象不能在请求之间共享。在您调用每个操作的情况下,您的 WordGameModel 将再次实例化,并且它的构造函数创建一个新的长字。
您可以将您的对象保存在某个数据库中,以供每个用户提供该功能。

您的视图需要包含 LongWord 的隐藏输入,以便在 POST 方法中,以便在 ModelBinder 调用您的构造函数后,设置 LongWord基于表单值(即您发送到视图的值)

<form id="wordForm" method="post">
    <div>
        <input type="hidden" asp-for="LongWord" /> // add hidden input
        <input id="wordInput" asp-for="ContainedWordCandidate"/>
        <input type="submit" name="Add" value="Add"/>
        <span asp-validation-for="ContainedWordCandidate"></span>
    </div>
</form>

作为旁注,在您的 post 方法中它应该只是 return RedirectToAction(nameof(Index)); - GET 方法没有(也不应该)有模型参数,所以没有意义传递它(无论如何它只会创建一个难看的查询字符串)

不要在控制器中使用静态字段来存储您的话。将状态保留在控制器中不是一个好主意,因为正如另一个答案中所述,控制器是 transient 并且会为每个请求创建一个新控制器。因此,即使您的静态变量应该仍然可用,但将其与控制器一起保存并不是一件好事。您还想保持模型干净,即不要将任何 business/game 逻辑放入其中。为此使用不同的 class。仅使用模型来确保值有效,即最小长度、必需等。

解决您问题的更好方法是创建一个 singleton 服务来存储数据。作为单例,在应用程序的生命周期内只会创建一个服务。您可以使用依赖注入将它注入您的控制器并将它用于每个请求,因为知道它对于每个请求都是相同的服务实例。

例如:

public interface IWordService
{
    IEnumerable<String> Words { get; }

    bool WordWasNotSubmittedPreviously(string word);

    bool WordMatchesLettersInLongWord(string longWord, string containedWordCandidate);

    void AddWordToList(string word);
}

public class WordService : IWordService
{
    private List<string> _words;

    public IEnumerable<string> Words => _words;

    public WordService()
    {
        _words = new List<string>();
    }

    public bool WordWasNotSubmittedPreviously(string containedWordCandidate) => !_words.Contains(containedWordCandidate);

    public bool WordMatchesLettersInLongWord(string longWord, string containedWordCandidate)
    {
        if (string.IsNullOrWhiteSpace(containedWordCandidate)) return false;
        return containedWordCandidate.All(letter => longWord.Contains(letter));
    }

    public void AddWordToList(string word)
    {
        _words.Add(word);
    }
}

此服务完成您 ValidationAttribute 所做的所有工作,但我们可以使用依赖注入来确保我们只为整个应用程序创建一个。

在你 Startup.cs 中将此添加到 ConfigureServices 方法中:

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<IWordService, WordService>();

    ....
}

现在我们可以将其注入我们的控制器,因为我们已将其注册为 singleton 我们每次都会获得相同的实例,即使我们获得了控制器的不同实例:

public class HomeController : Controller
{
    private readonly IWordService _wordService;

    public HomeController(IWordService wordService)
    {
        _wordService = wordService;
    }

    [HttpPost]
    public IActionResult Index(WordGameModel incomingModel)
    {
        if (ModelState.IsValid)
        {
            // Use the `_wordService instance to perform your checks and validation
            ...
        }

        ...
    }
}

我已经将 _wordService 的实际用法留给您来实现 :-) 但它应该相当简单。

您可以阅读有关依赖注入 (DI) 的更多信息here

还有ConfigureServices方法here