Repository navigation
Pass an explicit context to contact attributes and message e-mails - #939
Merged
KrzysztofPajak merged 4 commits intoOct 17, 2026
Merged
Conversation
added 2 commits
October 17, 2026 17:16
MessageProviderService took the link host from the current request, so a message sent from the admin panel for another store, or from a background task, linked to the wrong place. It now uses the primary domain of the message's store and no longer reads IContextAccessor.
GetAllContactAttributes takes a CatalogAccess and keys its cache by the customer's groups, so one visitor's attributes are no longer served to everyone. The contact parser takes the access and the host for file links; the parameterless coordinate overloads are gone, and a customer without coordinates no longer throws.
Store.PrimaryHost() replaces the private StoreHost helper in MessageProviderService and tolerates a store saved without domains. The contact form and order placement now use it too, so file upload links in a contact e-mail and in saved order attribute descriptions point to the same host as the rest of the message. Without a host, a contact file upload is named with its download id instead of being dropped. MessageProviderService resolves the host once per message, and IContactAttributeParser.FormatAttributes no longer takes the unused customer.
Messages sent from a storefront request now receive the current host, and their links use it when it is one of the store's domains; any other or missing host falls back to the store's primary domain.
KrzysztofPajak
deleted the
refactor/business-explicit-context-marketing
branch
October 17, 2026 17:26
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of the explicit-context work in Business (after #930, #931, #932, #936, #937, #938)
Type: feature
Issue
Four services in
Grand.Business.MarketingandGrand.Business.Messagesstill read the current request throughIContextAccessor. Outside a storefront request (admin panel for another store, API, scheduled tasks) they used the wrong customer, store or host:MessageProviderServicebuilt every link in e-mails (password recovery, order details, product pages…) from the host of the current request. An e-mail sent from the admin panel about an order of another store linked to the admin's host. A background task used whatever store the empty context fell back to.ContactAttributeService.GetAllContactAttributesfiltered by the groups of the current customer, but cached the result per store only. The first visitor's attribute list was served to everyone, so group-limited contact attributes leaked to guests, or disappeared for the customers allowed to see them.ContactAttributeParser.GetParseContactAttributesloaded the attributes without a store, so attributes limited to other stores were accepted from the contact form.FormatAttributestook the file upload link host from the request.CustomerCoordinatesServicehad overloads that read the current customer, andGetGeoCoordinatethrew aNullReferenceExceptionfor a customer without coordinates.Solution
MessageProviderServiceno longer depends onIContextAccessor. The 25Send*methods a storefront request can trigger take an optionalDomainHost host, which the storefront passes (OrderNotificationCommand.Hostfor order placement). The newStore.LinkHost(host)uses that host only when it is one of the store's domains; any other or missing host (admin panel, API, scheduled tasks, a forgedHostheader) falls back toStore.PrimaryHost(): the primary domain, then the first domain, thenStore.Url.GetAllContactAttributes(CatalogAccess access), the same shape asGetAllCheckoutAttributes. The cache key holds the store,ShowHiddenand the customer's group ids. The storefront passesCatalogAccess.For(customer, store), the admin panelCatalogAccess.IncludingHidden(staffStoreId).GetParseContactAttributes(model, access)andFormatAttributes(language, attributes, host, …)(the unused customer is gone). The contact form passesStore.LinkHost(request host); without a host the file upload is named by its download id instead of a link.ContactUsSendCommandcarries the customer, store, language and host, so its handler no longer reads the context.GetGeoCoordinate()andSaveGeoCoordinate(longitude, latitude)are removed (nothing in the repository called them). A customer without coordinates gets(0, 0).After this PR,
grep -rln -E 'IContextAccessor|IWorkContext\b' src/Business --include=*.cs | grep -v Startupreturns onlyTranslationServiceandPermissionService, the remainder documented in #937.Behavior change: messages sent from the admin panel, the API or a background task link to the primary domain of the message's store, not to the host the admin happens to use. Storefront messages keep linking to the domain of the visit.
Breaking changes
IContactAttributeService.GetAllContactAttributes(CatalogAccess access)replaces(string storeId = "", bool ignoreAcl = false).IContactAttributeParser.GetParseContactAttributes(model, CatalogAccess access);FormatAttributes(language, customAttributes, DomainHost host, separator, htmlEncode, allowHyperlinks)(nocustomer).IMessageProviderService: 25Send*methods take a trailing optionalDomainHost host = null(source compatible, binary breaking).OrderNotificationCommand.Host.ICustomerCoordinatesService:GetGeoCoordinate()andSaveGeoCoordinate(double, double)removed.MessageProviderService,ContactAttributeService,ContactAttributeParserandCustomerCoordinatesServiceno longer takeIContextAccessor.CacheKey.CONTACTATTRIBUTES_ALL_KEYhas three parameters (store, show hidden, group ids).Testing
Automated: the solution builds, and
dotnet test GrandNode.slnxpasses (22 projects). New or changed tests cover:IncludingHiddenreturning group-limited attributes, and a store id leaving out attributes of other stores;GetParseContactAttributesreading the attributes of the given access;GetGeoCoordinatefor a customer without coordinates;Store.Urlfor a store without domains.Manual: run on 2026-10-17 against a fresh install with sample data (port 44480, curl and Playwright). The store has two domains:
localhost:44480(primary) and127.0.0.1:44480. Contact attributes: "Attachment" (file upload), "Member topic" (limited to Registered) and "Other store field" (limited to another store). E-mails were read from theQueuedEmailcollection.127.0.0.1with a file, and posts "Member topic" and "Other store field" by handhttps://127.0.0.1:44480/download/getfileupload/?downloadId=…; the two forged fields are not in the e-mailHost: evil.example:44480header (not a store domain)https://localhost:44480/…127.0.0.1, fromlocalhost, and with the forgedHostheaderhttps://127.0.0.1:44480/passwordrecovery/confirm…,https://localhost:44480/…,https://localhost:44480/…(no host injection)127.0.0.1: re-send activation e-mail; contact attribute listhttps://localhost:44480/account/activation…(primary domain); the list shows all three attributes127.0.0.1(Cash On Delivery)https://127.0.0.1:44480/orderdetails/…{{Store.URL}}always printsStore.Url, unchanged by this PR. The server log showed no errors other than e-mail sending (no SMTP server locally).