Skip to content

Forward ValidationAttribute.ErrorMessage to EntryValidation - #1454

Open
Poseidonas wants to merge 2 commits into
PHOENIXCONTACT:devfrom
Poseidonas:forward-validation-error-messages
Open

Poseidonas wants to merge 2 commits into
PHOENIXCONTACT:devfrom
Poseidonas:forward-validation-error-messages

Conversation

@Poseidonas

Copy link
Copy Markdown

Fixes #1450.

DefaultSerialization.CreateValidation() walks the validation attributes on a member and reads each one for its bounds, pattern or data type. Everything else on the attribute is dropped, so a message written by the author never leaves the server and the entry editor has only its generic wording to show.

EntryValidation now carries an ErrorMessage, filled from ValidationAttribute.ErrorMessage and left null when no attribute supplied one — which keeps the generic messages as the fallback you asked for rather than replacing them.

The one decision I had to make

A member can carry several validation attributes, and there is no obvious winner among their messages. I kept the first one that has a message rather than concatenating, on the grounds that a joined string reads badly in a form field. If you would rather have all of them, or the message belonging to the rule that actually failed, that is a different shape — say the word and I will rework it.

Verification

  • dotnet build src/Moryx/Moryx.csproj: 0 errors.
  • Full Moryx.Tests suite: 356 passed, 2 skipped.
  • Two tests added to ValidationTests: one asserting the message reaches EntryValidation, one asserting it stays null for an attribute without a message, so the fallback path is guarded as well. ValidationDummy gains one property carrying ErrorMessage, and the sub-entry count in Setup moves from 8 to 9.
  • Both new tests fail to compile against dev without the change (EntryValidation has no ErrorMessage), which is the intended dependency.

Clone() copies the new field along with the rest.

I have not touched the consuming side. ngx-moryx-web would need to prefer validation.errorMessage over its generic string where it renders the message, and I have not looked at what that involves — happy to follow up there if it is wanted.

CreateValidation reads each validation attribute for its bounds, pattern or
data type and discards everything else, so a message written on the
attribute never reaches the consumer and the entry editor has only its
generic wording to fall back on.

EntryValidation gains an ErrorMessage which stays null when no attribute
supplied one, keeping the generic messages as the fallback asked for in
PHOENIXCONTACT#1450. The first message wins: a member carrying several attributes has no
obvious winner, and picking one beats concatenating them.

Fixes PHOENIXCONTACT#1450
@CLAassistant

CLAassistant commented Aug 23, 2026 •

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

Comment on lines +152 to +153
if (validation.ErrorMessage is null && attribute.ErrorMessage is not null)
validation.ErrorMessage = attribute.ErrorMessage;

@dbeuchler dbeuchler Aug 23, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your first contribution! EntrySerialize is full localizable. The ErrorMessage is used for non-localizable error messages. I would prefer also keep the localization consistent and use result of localized ErrorMessageResourceName with ErrorMessageResourceType like we already do that in DisplayAttribute.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While I did not explicitly say that, it was definitely the intend to cover the localized properties as well (or primarily even) 😅

Reading ErrorMessage directly returns null whenever the message was given as
a resource, which is the case the localized properties exist for.
FormatErrorMessage resolves ErrorMessageResourceName and ErrorMessageResourceType
and falls back to the literal string, the same way DisplayAttribute.GetName
does for the display name.

It is only called when the attribute declares a message. Without that guard
every member would carry the framework's own English default and the consumer
would lose the choice of fallback.
@Poseidonas

Copy link
Copy Markdown
Author

You are both right, and the localized case was the more important half of it. Fixed.

FormatErrorMessage is the analogue of the GetName() call the display name already goes through, so the resolution now follows the same route the rest of the code takes. Measured against .NET 10 before changing anything:

attribute .ErrorMessage FormatErrorMessage(name)
nothing set null The Name field is required.
ErrorMessage set the string the string
ErrorMessageResourceName + Type null the resource, translated

The third row is what I was reading, so a message given as a resource arrived as nothing at all.

One decision I would like checked

FormatErrorMessage is only called when the attribute actually declares a message:

if (validation.ErrorMessage is null &&
    (attribute.ErrorMessage is not null || attribute.ErrorMessageResourceName is not null))
    validation.ErrorMessage = attribute.FormatErrorMessage(attributeProvider.GetDisplayName() ?? string.Empty);

Without that guard, row one applies to every member that never asked for a message, and each one would arrive carrying the framework's own English default. That would take the choice of fallback away from the consumer and hand it a string that ignores the current culture. Leaving it null keeps that decision where it was. If you would rather have the framework default there, it is one condition to remove.

The display name comes from GetDisplayName() so a message containing {0} gets the same name the entry itself shows, and falls back to an empty string rather than the raw member name when no display attribute is present.

Tests

Added a case for the resource path alongside the literal one, with a static class standing in for a generated resource. Worth noting for anyone reading it later: the resource type has to expose a static property, not a const — I had it as a constant first and every test in the fixture failed with does not have an accessible static property named ....

Moryx.Tests is green: 357 passed, 2 skipped.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Forward ValidationAttribute.ErrorMessage via EntryValidation

4 participants