Skip to content

Fix six defects found testing a LiteDB install and the admin - #1004

Merged
KrzysztofPajak merged 11 commits into
developfrom
feature/litedb
Oct 31, 2026
Merged

KrzysztofPajak merged 11 commits into
developfrom
feature/litedb

Conversation

@KrzysztofPajak

Copy link
Copy Markdown
Member

Type: bugfix

Issue

An end-to-end run of a fresh LiteDB install (storefront, checkout, about 180 admin scenarios) found six defects. There is no linked issue, so they are described here.

On LiteDB only

  1. Failures right after every start. BsonMapper publishes an entity mapper before filling its members and reads published mappers without a lock. Threads meeting a type for the first time together threw Collection was modified or Member Id not found ... ParentEntity, or silently read an object with fields missing. With 36 parallel storefront requests after a start, 2 to 7 returned a 500 on every start. Scheduled tasks failed too, and the admin dashboard failed with a single user.
  2. Database:Singleton=false could not work. Each request opened its own LiteDatabase on the same file, so all of them failed on the file lock.
  3. Order delete returned a 500 once a gift voucher had been created by hand. GetGiftVoucherQueryHandler read PurchasedWithOrderItem.Id without a null check, and LiteDB filters in memory.

On any database

  1. Saving the general settings moved every date in the shop. The installer leaves the time zone empty, meaning the server's. The select has no empty option, so it showed the first zone, and any save stored UTC-12. An order placed at 21:09 then showed 08:09.
  2. Saving a guest customer in the admin returned a 500. So did clearing a registered customer's email. CustomerValidator lowercased the posted email without checking it.
  3. The first click on Checkout as Guest, Register or Forgot password did nothing on the login page. The autofocused email field lost focus on mousedown, its required message moved the layout, and the button slid from under the pointer before mouseup.

Solution

One commit per defect:

  • LiteDBMapperWarmup builds the mappers during the single-threaded part of startup, for every entity with its nested types, base types and interfaces. It only runs with UseLiteDb.
  • The Singleton option is gone; LiteDB is always one shared instance.
  • GetGiftVoucherQueryHandler gets the same null check GiftVoucherService already has.
  • The settings form preselects the zone really in use: the configured one, or the server's when none is set or the machine does not know it. This covers the Admin and Store panels. Nothing is written until someone saves.
  • CustomerValidator compares the email only when one was posted, as the create path does. The existing required rule reports a missing one.
  • ValidationProvider no longer reports a field nobody has touched when it loses focus. Submitting and typing still validate. app.runtime.bundle.js is rebuilt.

LiteDBMapperWarmup uses reflection, which .ai/constraints.md limits: LiteDB exposes mapper building only through the generic BsonMapper.Entity<T>(), and the entity types come from ITypeSearcher. It stays inside Grand.Data.

Not changed:

  • A store that already saved UTC-12 keeps it, because that cannot be told from a deliberate choice. It needs correcting in the settings.
  • A guest still cannot be saved without an email. That rule existed before; it now shows a message, FluentValidation's default text, the same as on create.
  • A type reached only polymorphically, through _type, is not warmed up.

Breaking changes

  • DatabaseConfig.Singleton is removed, along with Database:Singleton in appsettings.json. A value left in configuration is ignored. The only other value it allowed never worked.
  • On the storefront, leaving an empty required field untouched no longer shows its message until the form is submitted. This applies to every form using validation-provider.

Testing

Run with Database__UseLiteDb=true and install with the LiteDB provider, with sample data. Restart once after the install.

  1. Start-up: restart the application and straight away send about 50 parallel requests to category, product and search pages. All return 200, and the log has no Collection was modified or Member Id not found. Verified: 0 of 540 failed over 10 starts.
  2. Old option: start with Database__Singleton=false. The application works normally.
  3. Time zone: on a fresh install open Settings > General. The select shows the server's zone. Save without changing anything: order dates do not move. Choose another zone and save: it is kept.
  4. Gift voucher: create a gift voucher in the admin, place an order, cancel it and delete it. The order is deleted and the voucher stays.
  5. Guest customer: open a guest from Online customers and save. The email message appears. Enter an email and save: it is stored.
  6. Registered customer: clear the email and save: the message appears and the old email stays. Enter an email already in use: it is refused.
  7. Login page: as a guest, add a product to the cart, accept the terms and click Login and checkout. On the login page click Checkout as Guest once: it opens checkout. Register and Forgot password also work on the first click. Submitting the login form empty still reports both fields.

Automated: new tests are LiteDBMapperWarmupTests, GetGiftVoucherQueryHandlerTests, DateTimeServiceExtensionsTests and new cases in CustomerValidatorTests. On a full dotnet test GrandNode.slnx run after rebasing on develop, 23 tests failed in Grand.Business.Messages, Checkout, Marketing, Authentication and Catalog; the failing set varies between parallel runs. Run one by one, all five projects pass. The other 18 projects passed in the full run.

Not tested: online payments, real e-mail delivery, the Store and Vendor panels in a browser, and running in a Linux container, where a server zone missing from the list would still show the first option.

support added 6 commits October 31, 2026 11:00
With Singleton false every request opened its own LiteDatabase on the same
file. LiteDB locks the file, so every request failed with an IOException and
the application could not serve a single page. There is no working setup
behind the option, so it goes. A value left in an existing configuration is
ignored.
BsonMapper publishes a mapper before filling its members and reads published
mappers without a lock. Threads meeting a type for the first time together
either threw "Collection was modified" or "Member Id not found", or silently
read an object with fields missing. Requests right after every start got a
500, and so did the scheduled tasks and the admin dashboard.

The mappers are now built during the single-threaded part of startup, for
every entity with its nested types, base types and interfaces. Base types
matter because LINQ resolves x.Id on the type declaring it, ParentEntity.
The installer leaves the store time zone empty, which means the server's. The
settings form has no empty option, so its select showed the first zone in the
list, and saving the page for any other reason stored UTC-12 and moved every
date in the shop by hours.

The form now preselects the configured zone, or the server's when none is set
or the machine does not know it.
On the login page the email field has focus from the start. Pressing the mouse
on Checkout as Guest blurred it, the required message appeared and moved the
layout, and the button slid from under the pointer before mouseup, so the first
click did nothing. Register and Forgot password were hit the same way.

Leaving a field nobody has touched is no attempt to fill it in, so it now
reports nothing. Submitting and typing still validate.
A voucher created by hand has no PurchasedWithOrderItem. LiteDB filters in
memory, so one such voucher made the query throw and deleting any order
returned a 500.
The edit validator lowercased the posted email without checking it, so saving
a guest, who has none, or clearing a registered customer's email threw. The
existing required rule now reports it, as it already does on create.
Copilot AI balanced review requested due to automatic review settings October 31, 2026 10:06

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

@KrzysztofPajak KrzysztofPajak changed the title Fix LiteDB start-up failures and six defects found testing the admin Fix six defects found testing a LiteDB install and the admin Oct 31, 2026
Comment on lines +44 to +47
catch (Exception exc)
{
failures.Add(exc.Message);
}
Comment on lines +104 to +107
catch (Exception exc)
{
failures.Add(exc.Message);
}
support added 5 commits October 31, 2026 18:13
Starting from installed BaseEntity classes left two ways to build a mapper
during live traffic, on the dictionary every other request reads without a
lock. API queries read their models through TableCollection, and no entity
leads to those models. Installing a plugin writes its entities on the running
site, before the restart that would have warmed them.
Preselecting the server's zone still turned "not configured" into a fixed zone
on any save, so the store stopped following the host. It also depended on
TimeZoneInfo.Local.Id being one of the listed options. An empty option keeps
the value empty. A configured zone that resolves but is not listed, such as a
Windows id on Linux, gets its own option so that saving does not change it.
Replacing the 500 with a required error still left a guest uneditable unless
an address was invented for them. The email is now required unless the customer
has none stored and is not being moved to the Registered group. The required
message is also localized now: WithMessage covered only EmailAddress, so an
empty field showed the FluentValidation default.
Table on LiteDB is LINQ over objects, so one address with no last name made
the admin order list return a 500 when filtered by it.
An unticked box reports false and a group reports an empty list, neither of
which counted as empty. Their message could still appear on mousedown, move the
layout and lose the click, as it did for text fields. Submitting still reports
them.
public void ConfigureServices_OnLiteDb_BuildsTheMappersOfAnUninstalledPlugin()
{
//installing a plugin uses its entities on the running site, among requests reading the same mappers
DataSettingsManager.Initialize(Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}.cfg"));
@KrzysztofPajak
KrzysztofPajak merged commit 906668f into develop Oct 31, 2026
9 checks passed
@KrzysztofPajak
KrzysztofPajak deleted the feature/litedb branch October 31, 2026 18:04
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.

2 participants