{"ArticleId":null,"Name":"Architecture overview","Content":"\u003Cp\u003EGrandNode is an ASP.NET Core application backed by MongoDB. The source is one solution, \u003Ccode\u003EGrandNode.slnx\u003C/code\u003E, split into layers that depend only inward: the domain knows nothing about the database, the business layer knows nothing about HTTP, and the core never references a plugin. This article is the map you need before you change or extend anything.\u003C/p\u003E\n\n\u003Ch2 id=\u0022solution-layout\u0022\u003ESolution layout\u003C/h2\u003E\n\u003Cp\u003EEverything lives under \u003Ccode\u003Esrc/\u003C/code\u003E:\u003C/p\u003E\n\u003Ctable\u003E\n\u003Cthead\u003E\u003Ctr\u003E\u003Cth\u003EFolder\u003C/th\u003E\u003Cth\u003EWhat it contains\u003C/th\u003E\u003C/tr\u003E\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\u003Ctd\u003E\u003Ccode\u003Esrc/Core\u003C/code\u003E\u003C/td\u003E\u003Ctd\u003E\u003Ccode\u003EGrand.Domain\u003C/code\u003E (entities and settings classes, no dependencies), \u003Ccode\u003EGrand.Data\u003C/code\u003E (the \u003Ccode\u003EIRepository\u0026lt;T\u0026gt;\u003C/code\u003E abstraction with MongoDB and LiteDB implementations), \u003Ccode\u003EGrand.Infrastructure\u003C/code\u003E (startup, plugin and module loading, caching, migrations infrastructure), \u003Ccode\u003EGrand.Mediator\u003C/code\u003E, \u003Ccode\u003EGrand.Mapping\u003C/code\u003E, \u003Ccode\u003EGrand.SharedKernel\u003C/code\u003E.\u003C/td\u003E\u003C/tr\u003E\n\u003Ctr\u003E\u003Ctd\u003E\u003Ccode\u003Esrc/Business\u003C/code\u003E\u003C/td\u003E\u003Ctd\u003E\u003Ccode\u003EGrand.Business.Core\u003C/code\u003E holds the interfaces, commands and queries; \u003Ccode\u003EGrand.Business.Catalog\u003C/code\u003E, \u003Ccode\u003E.Checkout\u003C/code\u003E, \u003Ccode\u003E.Customers\u003C/code\u003E, \u003Ccode\u003E.Marketing\u003C/code\u003E, \u003Ccode\u003E.Messages\u003C/code\u003E, \u003Ccode\u003E.Cms\u003C/code\u003E, \u003Ccode\u003E.Common\u003C/code\u003E, \u003Ccode\u003E.Authentication\u003C/code\u003E and \u003Ccode\u003E.Storage\u003C/code\u003E hold the implementations.\u003C/td\u003E\u003C/tr\u003E\n\u003Ctr\u003E\u003Ctd\u003E\u003Ccode\u003Esrc/Web\u003C/code\u003E\u003C/td\u003E\u003Ctd\u003E\u003Ccode\u003EGrand.Web\u003C/code\u003E (the storefront and the host process), \u003Ccode\u003EGrand.Web.Admin\u003C/code\u003E (administrator panel), \u003Ccode\u003EGrand.Web.Store\u003C/code\u003E (store manager panel), \u003Ccode\u003EGrand.Web.Vendor\u003C/code\u003E (vendor panel), \u003Ccode\u003EGrand.Web.AdminShared\u003C/code\u003E (models, validators and mappings shared by the panels), \u003Ccode\u003EGrand.Web.Common\u003C/code\u003E (base controllers, tag helpers, themes, middleware) and \u003Ccode\u003EGrand.SharedUIResources\u003C/code\u003E (panel assets).\u003C/td\u003E\u003C/tr\u003E\n\u003Ctr\u003E\u003Ctd\u003E\u003Ccode\u003Esrc/Modules\u003C/code\u003E\u003C/td\u003E\u003Ctd\u003EApplication modules switched on in configuration: \u003Ccode\u003EGrand.Module.Installer\u003C/code\u003E, \u003Ccode\u003EGrand.Module.Migration\u003C/code\u003E, \u003Ccode\u003EGrand.Module.ScheduledTasks\u003C/code\u003E and \u003Ccode\u003EGrand.Module.Api\u003C/code\u003E.\u003C/td\u003E\u003C/tr\u003E\n\u003Ctr\u003E\u003Ctd\u003E\u003Ccode\u003Esrc/Plugins\u003C/code\u003E\u003C/td\u003E\u003Ctd\u003EInstallable providers and themes: \u003Ccode\u003EPayments.*\u003C/code\u003E, \u003Ccode\u003EShipping.*\u003C/code\u003E, \u003Ccode\u003ETax.*\u003C/code\u003E, \u003Ccode\u003EWidgets.*\u003C/code\u003E, \u003Ccode\u003EDiscountRules.*\u003C/code\u003E, \u003Ccode\u003EAuthentication.*\u003C/code\u003E, \u003Ccode\u003EExchangeRate.*\u003C/code\u003E, \u003Ccode\u003ETheme.*\u003C/code\u003E.\u003C/td\u003E\u003C/tr\u003E\n\u003Ctr\u003E\u003Ctd\u003E\u003Ccode\u003Esrc/Tests\u003C/code\u003E\u003C/td\u003E\u003Ctd\u003EMSTest projects, one per production project (\u003Ccode\u003EGrand.Business.Catalog.Tests\u003C/code\u003E, \u003Ccode\u003EGrand.Web.Admin.Tests\u003C/code\u003E and so on).\u003C/td\u003E\u003C/tr\u003E\n\u003Ctr\u003E\u003Ctd\u003E\u003Ccode\u003Esrc/Aspire\u003C/code\u003E\u003C/td\u003E\u003Ctd\u003EA .NET Aspire host that starts MongoDB, Redis and two replicas of the web app for local multi-instance testing.\u003C/td\u003E\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\n\u003Cp\u003EModules and plugins are not referenced by \u003Ccode\u003EGrand.Web\u003C/code\u003E. Each one builds itself into \u003Ccode\u003Esrc/Web/Grand.Web/Modules/\u0026lt;name\u0026gt;\u003C/code\u003E or \u003Ccode\u003Esrc/Web/Grand.Web/Plugins/\u0026lt;SystemName\u0026gt;\u003C/code\u003E, and the host discovers them at startup.\u003C/p\u003E\n\n\u003Ch2 id=\u0022layering\u0022\u003ELayering\u003C/h2\u003E\n\u003Cpre\u003E\u003Ccode\u003EGrand.Web / Grand.Module.Api     HTTP, controllers, view models\nGrand.Business.*                 use cases, services, validators\nGrand.Domain                     entities (no dependencies)\nGrand.Data / Grand.Infrastructure  data access, caching, startup\nGrand.SharedKernel               primitives shared by every layer\u003C/code\u003E\u003C/pre\u003E\n\u003Cul\u003E\n\u003Cli\u003EControllers hold no business logic. They read the input, send a mediator request and return a result.\u003C/li\u003E\n\u003Cli\u003EBusiness services depend on \u003Ccode\u003EIRepository\u0026lt;T\u0026gt;\u003C/code\u003E, never on a MongoDB collection.\u003C/li\u003E\n\u003Cli\u003EService registrations live in an \u003Ccode\u003EIStartupApplication\u003C/code\u003E class in the project that owns them, not in \u003Ccode\u003EProgram.cs\u003C/code\u003E.\u003C/li\u003E\n\u003Cli\u003EA core, business or web project never references a plugin. The core defines an interface; a plugin registers an implementation.\u003C/li\u003E\n\u003C/ul\u003E\n\n\u003Ch2 id=\u0022startup\u0022\u003EStartup and IStartupApplication\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003EProgram.cs\u003C/code\u003E in \u003Ccode\u003EGrand.Web\u003C/code\u003E is deliberately short. \u003Ccode\u003EStartupBase\u003C/code\u003E scans the loaded assemblies, including installed plugins and enabled modules, for classes implementing \u003Ccode\u003EIStartupApplication\u003C/code\u003E and runs them ordered by \u003Ccode\u003EPriority\u003C/code\u003E:\u003C/p\u003E\n\u003Cpre\u003E\u003Ccode\u003Epublic interface IStartupApplication\n{\n    void ConfigureServices(IServiceCollection services, IConfiguration configuration);\n    void Configure(WebApplication application, IWebHostEnvironment webHostEnvironment);\n    int Priority { get; }\n    bool BeforeConfigure { get; }\n}\u003C/code\u003E\u003C/pre\u003E\n\u003Cp\u003ELower priorities run first. The core uses negative values for URL rewriting and forwarded headers, \u003Ccode\u003E0\u003C/code\u003E for core services, \u003Ccode\u003E500\u003C/code\u003E for authentication and the context middleware and \u003Ccode\u003E1000\u003C/code\u003E for endpoint routing. Plugins conventionally use \u003Ccode\u003E10\u003C/code\u003E. \u003Ccode\u003EConfigure\u003C/code\u003E runs in two passes: every startup with \u003Ccode\u003EBeforeConfigure = true\u003C/code\u003E first, then the rest.\u003C/p\u003E\n\u003Cp\u003EOn each request the \u003Ccode\u003EContextMiddleware\u003C/code\u003E resolves the current store first and then the current customer, language and currency, and exposes them through \u003Ccode\u003EIContextAccessor\u003C/code\u003E (\u003Ccode\u003EStoreContext\u003C/code\u003E and \u003Ccode\u003EWorkContext\u003C/code\u003E). Code that runs outside a request, such as scheduled tasks and migrations, has no work context and must receive the store or customer explicitly.\u003C/p\u003E\n\n\u003Ch2 id=\u0022data-access\u0022\u003EData access: IRepository\u003C/h2\u003E\n\u003Cp\u003EAll persistence goes through \u003Ccode\u003EIRepository\u0026lt;T\u0026gt;\u003C/code\u003E from \u003Ccode\u003EGrand.Data\u003C/code\u003E. It exposes \u003Ccode\u003ETable\u003C/code\u003E as an \u003Ccode\u003EIQueryable\u0026lt;T\u0026gt;\u003C/code\u003E for LINQ queries and async writes such as \u003Ccode\u003EInsertAsync\u003C/code\u003E, \u003Ccode\u003EUpdateAsync\u003C/code\u003E, \u003Ccode\u003EDeleteAsync\u003C/code\u003E, plus partial updates (\u003Ccode\u003EUpdateField\u003C/code\u003E, \u003Ccode\u003EIncField\u003C/code\u003E, \u003Ccode\u003EAddToCollectionField\u003C/code\u003E) that change one field without rewriting the document. The MongoDB implementation is the default; a LiteDB implementation exists for embedded scenarios.\u003C/p\u003E\n\u003Cpre\u003E\u003Ccode\u003Evar query = _productRepository.Table\n    .Where(p =\u0026gt; p.Published \u0026amp;\u0026amp; p.VendorId == vendorId)\n    .OrderBy(p =\u0026gt; p.DisplayOrder);\n\nreturn await _productRepository.PagedAsync(query, pageIndex, pageSize);\u003C/code\u003E\u003C/pre\u003E\n\u003Cp\u003EThe repository also offers async helpers that work for both providers: \u003Ccode\u003EToListAsync\u003C/code\u003E, \u003Ccode\u003EFirstOrDefaultAsync\u003C/code\u003E, \u003Ccode\u003ECountAsync\u003C/code\u003E, \u003Ccode\u003EAnyAsync\u003C/code\u003E and \u003Ccode\u003EPagedAsync\u003C/code\u003E.\u003C/p\u003E\n\u003Cp\u003EMongoDB documents use PascalCase field names (\u003Ccode\u003EName\u003C/code\u003E, not \u003Ccode\u003Ename\u003C/code\u003E), which matters when you query the database directly in \u003Ccode\u003Emongosh\u003C/code\u003E.\u003C/p\u003E\n\n\u003Ch2 id=\u0022mediator\u0022\u003EMediator: commands, queries and events\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003EGrand.Mediator\u003C/code\u003E is an in-house mediator with a MediatR-compatible API (\u003Ccode\u003EIRequest\u003C/code\u003E, \u003Ccode\u003EIRequestHandler\u003C/code\u003E, \u003Ccode\u003EINotification\u003C/code\u003E, \u003Ccode\u003EINotificationHandler\u003C/code\u003E, \u003Ccode\u003EIMediator\u003C/code\u003E); the MediatR package itself is not used. Request types that cross projects are defined in \u003Ccode\u003EGrand.Business.Core\u003C/code\u003E; their handlers live in the business project that owns the feature. The storefront has its own view-model requests in \u003Ccode\u003EGrand.Web/Features/Handlers\u003C/code\u003E and \u003Ccode\u003EGrand.Web/Commands/Handler\u003C/code\u003E.\u003C/p\u003E\n\u003Cpre\u003E\u003Ccode\u003Epublic class GetSuggestedProductsQuery : IRequest\u0026lt;IList\u0026lt;Product\u0026gt;\u0026gt;\n{\n    public string[] CustomerTagIds { get; set; }\n    public int ProductsNumber { get; set; }\n}\n\n// in a controller or another handler\nvar products = await _mediator.Send(new GetSuggestedProductsQuery {\n    CustomerTagIds = customer.CustomerTags.ToArray(), ProductsNumber = 4 });\u003C/code\u003E\u003C/pre\u003E\n\u003Cp\u003EAfter every write, services publish an entity event so other code can react without the service knowing about it:\u003C/p\u003E\n\u003Cpre\u003E\u003Ccode\u003Eawait _repository.UpdateAsync(entity);\nawait _cache.RemoveByPrefix(CacheKey.TAXCATEGORIES_PATTERN_KEY);\nawait _mediator.EntityUpdated(entity);\u003C/code\u003E\u003C/pre\u003E\n\u003Cp\u003EHandlers implement \u003Ccode\u003EINotificationHandler\u0026lt;EntityUpdated\u0026lt;T\u0026gt;\u0026gt;\u003C/code\u003E (or \u003Ccode\u003EEntityInserted\u003C/code\u003E, \u003Ccode\u003EEntityDeleted\u003C/code\u003E). Plugins use the same mechanism to hook into the platform, for example to react when an order is placed.\u003C/p\u003E\n\n\u003Ch2 id=\u0022caching\u0022\u003ECaching\u003C/h2\u003E\n\u003Cp\u003E\u003Ccode\u003EICacheBase\u003C/code\u003E is the only caching abstraction. It is an in-memory cache, optionally synchronised across instances through Redis publish/subscribe (\u003Ccode\u003ERedis:RedisPubSubEnabled\u003C/code\u003E in \u003Ccode\u003Eappsettings.json\u003C/code\u003E). Caching belongs in business services, around the repository call, never in controllers or handlers. Keys are constants on the \u003Ccode\u003ECacheKey\u003C/code\u003E class and must include every variable that changes the result, above all the store id and language id.\u003C/p\u003E\n\u003Cpre\u003E\u003Ccode\u003Epublic async Task\u0026lt;IList\u0026lt;TaxCategory\u0026gt;\u0026gt; GetAllTaxCategories(string storeId = \u0022\u0022)\n{\n    var key = string.Format(CacheKey.TAXCATEGORIES_ALL_KEY, storeId);\n    return await _cacheBase.GetAsync(key, async () =\u0026gt;\n    {\n        var query = _taxCategoryRepository.Table.AsQueryable();\n        if (!string.IsNullOrEmpty(storeId))\n            query = query.Where(tc =\u0026gt; tc.StoreId == storeId || string.IsNullOrEmpty(tc.StoreId));\n        return await _taxCategoryRepository.ToListAsync(query.OrderBy(tc =\u0026gt; tc.DisplayOrder));\n    });\n}\u003C/code\u003E\u003C/pre\u003E\n\u003Cp\u003EEvery insert, update and delete clears the family prefix with \u003Ccode\u003ERemoveByPrefix\u003C/code\u003E, otherwise the storefront keeps serving stale data.\u003C/p\u003E\n\n\u003Ch2 id=\u0022request-path\u0022\u003EFrom request to page\u003C/h2\u003E\n\u003Cpre\u003E\u003Ccode\u003EController action\n  -\u0026gt; IMediator.Send(query)            Grand.Web/Features/Handlers\n     -\u0026gt; business service               Grand.Business.*\n        -\u0026gt; ICacheBase.GetAsync          Grand.Infrastructure/Caching\n           -\u0026gt; IRepository\u0026lt;T\u0026gt; -\u0026gt; MongoDB   Grand.Data\n  -\u0026gt; View(model)\n     -\u0026gt; theme view locations (IThemeView), widget zones\u003C/code\u003E\u003C/pre\u003E\n\n\u003Ch2 id=\u0022related\u0022\u003ERelated\u003C/h2\u003E\n\u003Cul\u003E\n\u003Cli\u003E\u003Ca href=\u0022/developers-development-setup\u0022\u003EDevelopment setup\u003C/a\u003E\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/developers-writing-a-plugin\u0022\u003EWriting a plugin\u003C/a\u003E\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/developers-creating-a-theme\u0022\u003ECreating a theme\u003C/a\u003E\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/developers-ai-agent-kit\u0022\u003EThe AI Agent Kit\u003C/a\u003E \u2014 the repository\u0027s \u003Ccode\u003E.ai/knowledge\u003C/code\u003E folder goes deeper on every section above.\u003C/li\u003E\n\u003C/ul\u003E","ParentCategoryId":"6abdec6d83d2816248229f65","SeName":"developers-architecture-overview","MetaKeywords":null,"MetaDescription":"How the GrandNode solution is organised: Core, Business, Web, Modules and Plugins, the MongoDB repository, the mediator, domain events and caching.","MetaTitle":null,"AllowComments":false,"Captcha":{"ReCaptchaChallengeField":null,"ReCaptchaResponseField":null,"ReCaptchaResponseValue":null,"ReCaptchaResponse":null},"RelatedArticles":[],"CategoryBreadcrumb":[{"Name":"For developers","Description":null,"IsCurrent":false,"Children":null,"Parent":null,"SeName":"docs-developers","Id":"6abdec6d83d2816248229f65","UserFields":[]}],"AddNewComment":{"CommentText":null,"DisplayCaptcha":false,"Id":null,"UserFields":[]},"Comments":[],"Id":"6abdec6d83d2816248229f67","UserFields":[]}