-
Notifications
You must be signed in to change notification settings - Fork 0
Description
Overview:
Aster is a modular ASP.NET Core application platform inspired by Orchard Core, designed with a more decoupled and flexible architecture. This specification outlines the key architectural components and requirements of Aster, including multitenancy, modularity, resource composition, data persistence, and extensibility. Each section below defines the expected behaviors and design considerations, ensuring a clear separation of concerns and guiding the responsible engineers in implementation.
- Multitenancy Architecture
Aster must support a robust multi-tenant architecture, enabling multiple tenant applications to run in a single deployment with strict isolation and flexible configuration. The design introduces the concept of shells (groupings of tenants) to balance isolation with resource sharing.
• Tenant Isolation Modes: The platform must provide three isolation strategies for tenant data. The choice is configurable per tenant, and the system should enforce data separation in each mode:
• Tenant ID Discriminator (Shared Tables): All tenants can share a set of tables, with each row tagged by a Tenant ID. Every query and data operation must include the tenant’s identifier to filter data, ensuring no cross-tenant data leakage. (This approach addresses a limitation in Orchard Core, which lacked single-table multi-tenant support .) Engineers must implement safeguards so that all data access layers automatically apply the current Tenant ID filter when this mode is used.
• Per-Database Isolation: Each tenant can be assigned a separate database. In this mode, connection strings are tenant-specific. The framework will route each tenant’s data access to its own database, achieving complete physical isolation. This allows independent administration of each tenant’s data store (backups, security, etc.), at the cost of additional management overhead . The configuration for each tenant must include its database connection details.
• Per-Schema Isolation (Shared Database): Tenants can share a single database instance but use distinct schemas. The platform will create and use a unique schema (namespace) for each tenant’s tables. This provides isolation (each tenant’s tables live in a different schema) while using one database. The system must ensure all ORM mappings or SQL queries target the proper schema for the active tenant. (For example, a tenant’s connection may be bound to a specific default schema at runtime to simplify queries.) This mode offers a security and manageability compromise between separate DBs and shared tables .
• Shells as Multi-Tenant Containers: Aster introduces shells as groupings of tenants that share a common service collection and baseline configuration. In Orchard Core, each tenant is backed by a one-to-one shell (a dedicated DI container per tenant) . In Aster, a single shell can host multiple tenants, allowing controlled sharing of module code and services among those tenants. Key requirements for the shell system include:
• A shell is essentially an isolated dependency injection container (service collection) that is initialized with a specific set of modules and features. All tenants under that shell have access to the services (code) defined in the shell’s container.
• Shells help reduce duplication of resources. For example, if ten tenants use the same set of modules, they can be grouped in one shell, avoiding ten separate loads of the same assemblies. This improves startup time and memory usage.
• Tenant–Shell Assignment: Each tenant’s configuration designates which shell it belongs to. Tenants in the same shell share the loaded features, but can still differ in which of those features are enabled for them (see below). The system must prevent tenants from accessing services or modules not loaded in their shell – if a tenant requires a module that the shell doesn’t have, that tenant should either trigger the shell to load it (if dynamic loading is allowed for that shell) or be assigned to a different shell that includes it.
• Shell Isolation: Even though multiple tenants share a shell’s service collection, their runtime contexts (scopes) must enforce isolation. Any singleton or scoped service that holds state should be made tenant-aware. This can be achieved by registering such services in a special tenant scope within the shell, or by having the service keyed by Tenant ID. Engineers must design the dependency injection lifetimes such that services can either be shared safely across tenants or are unique per tenant. For instance, a caching service might keep separate data per tenant, and an interface to retrieve the current tenant’s context will be available to all services.
• Shell Host and Management: The application’s host will manage shells and tenants. On startup, Aster should load all configured shells, create the DI container for each shell (loading the appropriate modules), and then instantiate tenants within those shells. A shell management service should handle routing incoming requests to the correct shell/tenant based on the request (e.g., hostname or URL prefix mapping to tenant). It should also support lifecycle operations like starting/stopping shells or adding new tenants (and possibly creating or assigning shells on the fly).
• Per-Tenant Feature Configuration: Within a shell, each tenant can have its own configuration specifying which features are enabled or disabled. The framework must allow per-tenant module/feature management so that tenants can customize functionality:
• Each tenant will have a Tenant Configuration (likely stored as a configuration file or database document) listing the enabled features (and their versions, if applicable) for that tenant. This can be managed through an admin UI or configuration APIs.
• When a tenant is activated (at application start or when enabled), the shell’s DI container will already contain all services for all possible features loaded in that shell. The tenant activation process must enable or disable services and functionality at runtime according to the tenant’s config. For example, if Module A is present in the shell but Tenant X has all features of Module A disabled, the system should ensure that Tenant X does not execute any scheduled tasks, controllers, or background services from Module A. This could be implemented by the framework checking a feature toggle before certain actions, or by not wiring up certain feature-specific services for that tenant. Design-wise, it may involve feature guards or tenant-specific service filters.
• Enabling a feature for a tenant: When an admin enables a new feature in a tenant’s settings, the system should invoke that feature’s activation logic within the context of that tenant. This may include running database migrations for that feature on the tenant’s database, seeding default data, and starting any services (if needed) for that tenant. It should not require restarting the application or shell. For example, if a tenant enables a “Blog” feature, the Blog module’s services are already loaded in the shell, but now the tenant’s site should start showing blog menus, and the Blog feature’s migrations (creating Blog-specific indices or content types) should run for that tenant.
• Disabling a feature for a tenant: Similarly, when a feature is disabled on a specific tenant, the system should unload or deactivate that feature’s functionality for that tenant. This could mean disposing any tenant-specific resources (like scheduled tasks or caches) related to that feature. The feature’s services might still exist in the shell’s container, but they should effectively become no-ops or be ignored for that tenant’s requests.
• Configuration Isolation: Tenant-specific settings (for example, connection strings, enabled features, theme selection, etc.) must be isolated per tenant. The framework will provide an API (and likely UI) for updating tenant settings, which should be stored in a secure, isolated manner (such as a JSON configuration per tenant or a Tenants collection in the database). Only authorized personnel or the default host admin should be able to modify tenant configurations.
• Security: The multi-tenant system must ensure that requests and background tasks are always executed in the context of a known tenant. Under no circumstances should a service resolve or bleed data from another tenant’s context. This requires a tenant resolution middleware that identifies the tenant (and thus the shell) for each incoming request (using host name, URL prefix, or another discriminator) and scopes the execution to that tenant. The framework will throw an error or reject operations if a tenant context is missing or if cross-tenant access is attempted (except through special, controlled APIs for host-level management).
Note: The shells concept should remain flexible. Initially, shells could be configured manually (e.g., grouping tenants by intended module sets), but the design should allow future enhancements such as automatically scaling tenants across shells or even processes, if needed. The multitenancy implementation team is responsible for providing the configuration schema for tenants (including database info, shell assignment, and enabled features) and for implementing the tenant resolution and isolation mechanisms described above.
- Modularity System
Aster’s modularity system is designed to allow extensible features and dynamic composition of the application at runtime, much like Orchard Core, but with greater flexibility. Modules and features are the primary building blocks. The system must support both statically linked modules (known at compile-time) and dynamically loaded modules (added at runtime), and allow enabling or disabling features per tenant on the fly.
• Module and Feature Concepts:
• A Module in Aster is a package or assembly that can contain one or more functional Features. It is analogous to an Orchard Core module: essentially a unit of deployment containing code (services, controllers, views, etc.) and a manifest. Modules are typically located in a Modules directory or referenced via NuGet packages.
• Features: Features are subdivisions of a module, representing specific functionality that can be toggled. Each feature has a unique name (often prefixed by the module name) and can declare dependencies on other features. The module’s manifest (or equivalent configuration) lists its features and metadata. This allows a single module to deliver multiple optional capabilities. For example, a “BlogModule” might contain features like Blog–Core, Blog–Widgets, etc., which the tenant can enable independently. According to Orchard’s design, a module may be comprised of a default feature or a group of individually defined features . Aster will follow this approach to maximize flexibility: module developers can split functionality into features that admins enable per need.
• Manifest & Metadata: Each module must include a manifest file or attribute (e.g., Module.json or assembly attributes) declaring the module’s identity (name, version, author, etc.) and its features. For each feature, the manifest lists a description and any Dependencies (other features that must be enabled first). The system will use this metadata to manage feature resolution and avoid dependency conflicts. Example: A feature “Comments” might depend on feature “Content Items”, meaning the latter is auto-enabled if not already when Comments is enabled . The platform should automatically enforce these dependencies: enabling a feature will enable its required dependencies if they are not already active (for that tenant), and disabling a feature should either prevent disabling a required dependency or also disable features that depend on it.
• Static Module Loading:
Modules can be compiled into the application or deployed alongside it and loaded at application startup. The framework will scan predefined locations (e.g., a Modules folder or specified assemblies) during application initialization to discover modules. All discovered modules’ manifests are read and their services and features are registered before the application starts serving requests.
• Assembly Discovery: The system will use reflection or an assembly scanning service to load modules. Only valid modules (those containing the expected manifest and entry point) should be considered. Any module incompatibilities (e.g., built for a different version of Aster) should be logged and ignored, or cause a startup error depending on severity.
• Service Registration: For each module, the features that are globally enabled (if any are enabled by default or for the host) will have their services added to the DI container of the appropriate shell(s). Typically, all features of a module will be loaded into a shell’s container, but only activated per tenant as configured. The module can provide a Startup class or similar hook to register its services into the IServiceCollection. Aster’s module loader must support conditional registration: e.g., services can be tagged with which feature they belong to, so they can be toggled per tenant.
• Ordering and Dependencies: Static loading should respect module ordering if specified (for example, a module manifest might indicate it should load after another module to override behavior). Also, base framework modules (like a Core module providing fundamental services) should load first. Circular dependencies should be detected at startup and either resolved or reported as errors.
• Dynamic Module Loading:
Aster must support adding or removing modules at runtime without requiring an application restart, a feature not available in Orchard Core’s default setup . This requires careful design:
• The framework should allow an administrator or automated process to install new modules (for example, by dropping a module package into a watched directory, uploading via an admin UI, or via a package feed). When a new module is introduced, the system will load its assembly into an isolated context, verify its manifest and dependencies, then integrate it into the running application. If the module has features that should be immediately active (e.g., a default feature), the system should register its services into the relevant shell containers and run any initialization (like migrations) for each tenant (or at least for tenants designated to auto-enable it).
• Assembly Loading and Isolation: To avoid conflicts, dynamically loaded modules might be loaded using a separate AssemblyLoadContext. Engineers must ensure that if a module depends on certain library versions, it does not conflict with already loaded libraries. If conflicts arise, the platform should prefer the host’s assemblies or employ load contexts to isolate the module’s dependencies.
• Precompiled Resources: Note that some components (MVC views, Razor Pages, etc.) expect compile-time presence. To support dynamic loading of such components, Aster could require modules to ship precompiled views or use Razor Class Libraries. The dynamic loader must hook into ASP.NET’s MVC engine to add the module’s views and controllers to the application. (For example, registering a new ApplicationPart for MVC when a module with controllers is loaded, so that routes and controllers become available.)
• Activation & Deactivation: When a dynamic module is loaded, its features should default to “disabled” for all tenants (unless a tenant configuration or a recipe explicitly enables them). Admins can then enable the desired features per tenant. Conversely, if a module is removed or unloaded at runtime, the system must safely detach its features: this means disposing services, removing routes, and possibly reversing its migrations (if supported) or at least ensuring no further calls access it. Unloading assemblies at runtime is non-trivial in .NET (you typically unload an entire AssemblyLoadContext), so Aster might treat module removal as a special operation that may require a shell (or app) restart to fully reclaim resources.
• Hot Reload vs. Warm Swap: Ideally, small updates to modules (like a patch) could be applied by unloading the old version’s context and loading the new one. The specification requires supporting dynamic addition; supporting dynamic update is a bonus if feasible. In any case, the platform should log these operations clearly and provide hooks for modules (via interfaces like IFeatureEventHandler) to respond to being enabled or disabled at runtime (so modules can e.g. start a background task or stop it when toggled).
• Tenant-Specific Service Injection:
A critical aspect of modularity in a multi-tenant environment is loading services on a per-tenant basis. Aster’s design must ensure that the DI container for a shell knows which services belong to which feature, so it can scope them to tenants that have the feature enabled. Possible implementation specifics:
• The container could register all module services but keyed or labeled by feature. A Feature Manager service then controls resolution: for example, a tenant’s scope might only resolve services for enabled features. This might be achieved via runtime checks in factory methods or using a custom IServiceProvider that filters by tenant context.
• Another approach (used by Orchard Core) is to actually build separate service providers per tenant. Aster’s twist with shells implies that if two tenants share a shell, they share the same provider instance. Therefore, to get tenant-specific variation, Aster might implement an OnDemand Activation pattern: all services live in the shell, but if a tenant doesn’t have a feature, calls to that feature’s services should result in no-ops or exceptions. For example, an MVC controller for a disabled feature might be present but should return 404 or be inert for that tenant.
• The framework should provide extension points like events or interfaces (e.g., IFeatureEventHandler) to allow module developers to react to feature enable/disable events per tenant. When a feature is enabled for a tenant, the event handler can perform actions such as seeding data or scheduling tasks in that tenant context. This is similar to Orchard’s mechanism where enabling a feature triggers code in that module to set up defaults. If a feature has dependencies, those should be enabled first automatically , and the handlers for dependencies run before the dependent feature’s handler.
• Dependency Constraints: If a tenant admin tries to enable a feature whose dependency is not present in the shell (perhaps because that dependency is part of another module not loaded in this shell), the system should either load the required module dynamically (if available) or prevent the action with a clear error. It is the responsibility of the module loader to ensure that all declared dependencies in manifests are either satisfied or produce a meaningful diagnostic.
• Module Isolation and Conflicts:
• All modules should run under the same application by default, but to avoid name collisions, they should be namespaced (e.g., different modules should not use the same assembly or feature names). The manifest system will enforce unique module IDs and feature names.
• If two versions of a module are present (which ideally should not happen in a single application instance), the loader must detect the conflict and choose a resolution strategy (likely refusing the second module or requiring an update of the first).
• Modules should be decoupled: they communicate only through well-defined extension points (like services or events) rather than hard references if possible. For instance, rather than module A directly calling module B’s methods (which creates tight coupling), module B could expose an interface that module A can consume if B’s feature is enabled. This fosters a more plug-and-play architecture.
In summary, the modular system requires implementing a Module Manager component responsible for discovering modules, loading them (statically at startup, and dynamically on demand), and coordinating feature enablement per tenant. The engineering team must ensure that module dependencies are respected, and that dynamic scenarios (adding/removing modules) do not destabilize the running application. All of this should be done in a tenant-aware manner so that features can be isolated per tenant as configured.
- Composable Resource Type Definitions
Aster will provide a flexible content modeling system, allowing developers and administrators to define Resource Types (analogous to Orchard Core’s content types) that are composed of reusable parts. This system uses a Resource–Aspect–Facet model to represent content structure in a decoupled way. Content items (instances of resource types) should support versioning, and rich behaviors like access control are achieved by attaching aspects.
• Resource Types: A Resource Type is a definition (schema) for a unit of content or data in the application – for example, “Blog Post”, “Product”, “UserProfile”. It is comparable to a content type in Orchard . Resource types are named and can be defined dynamically at runtime or via code. They serve as a container for aspects and facets:
• A resource type may include zero or more Aspects (parts), which themselves can host Facets (fields).
• Each resource type can also have certain settings (e.g. whether items of this type are draftable, versionable, securable, etc.). By default, content items are versionable (they maintain history of changes) and draftable (they can exist in unpublished draft states), unless configured otherwise for the type. For instance, a resource type might be marked as non-versionable if version history is not needed.
• Resource types can be created and altered at runtime via an administrative interface or migrations (see below). The system should maintain these definitions in a store (likely the database, similar to Orchard Core’s ContentDefinition records) so that all tenants (or each tenant, if definitions differ per tenant) have their own set of resource type definitions. By default, definitions are tenant-specific, meaning tenants can customize content models without affecting others. (However, Aster could allow sharing definitions across tenants if they share a shell and choose to use common types, but this would be an advanced scenario.)
• Aspects (Parts): An Aspect represents a reusable, self-contained piece of a resource’s behavior or data. Aspects in Aster are analogous to Orchard Core content parts, providing a way to modularize content functionality . Characteristics of aspects include:
• An aspect typically encapsulates a certain feature or concern. For example, “Commentable” could be an aspect that, when attached to a resource type, enables that content item to have comments. Another example is a “SeoMetadata” aspect that adds SEO fields to any content item it’s attached to.
• Aspects can carry their own data (usually in the form of facets/fields) and logic. They may also provide behavior through code: e.g., methods, event handlers, or interface implementations that get invoked in the content lifecycle.
• Reusability: Aspects are designed to be reused across multiple resource types. This encourages a composition approach: instead of creating monolithic content classes, you mix and match aspects. For instance, a Blog Post type and a Product type could both include a “Tags” aspect to enable tagging, rather than duplicating that functionality in each.
• Attachment and Configuration: An aspect can be attached to a resource type via the content definition manager. Each aspect might have settings when attached (for example, a “TextBody” aspect could have a setting for which text format to allow, or a position/order in the content item’s rendering). The specification should define that there can be at most one instance of a particular aspect type on a given resource type, to avoid ambiguity (similar to Orchard’s rule: one part type per content type ).
• Lifecycle and Access: Aspects can also be used to implement cross-cutting concerns. For example, an AccessControlAspect could be attached to a resource type to enable item-level permissions; this aspect would include fields like who can view or edit the item, and logic to enforce those permissions. Other examples: a LocalizationAspect to handle localized content variations, an AuditAspect that tracks creation and modification timestamps and users.
• Aspect Development: Module developers can create new aspect types by defining a class (or schema) for the aspect, plus any supporting code (drivers, handlers, templates). These aspect types must be registered with the system (likely in the module’s manifest or via a service) so that they can be attached to resource types. Each aspect should define how it’s stored (whether its data is just part of the main resource document or in a separate structure) and how it’s displayed or interacted with (e.g., its UI editor component, if any).
• Facets (Fields): A Facet is the smallest unit of content definition – essentially a field, analogous to Orchard Core content fields . Facets represent individual data elements (e.g., a string, number, boolean, datetime, media reference) that can appear on content. Key points about facets:
• Association: Facets are not attached directly to resource types in Aster; they are attached to aspects. (This is similar to Orchard Core’s model where fields attach to parts, and parts attach to types .) This indirection allows fields to be grouped logically by aspect. For example, a “Product Details” aspect might contain facets like “Price” (NumericField), “SKU” (TextField), and “Weight” (NumericField). Keeping them under one aspect means they can be enabled/disabled or reused as a set.
• Facet Types: The framework will provide a library of common facet types (text, number, Boolean, date/time, image, reference, etc.), and modules can introduce new facet types as needed (for instance, a GeolocationField facet type). Each facet type defines the kind of data it holds and may include validation rules or specialized behavior. For example, a Numeric facet might have properties for minimum/maximum allowed values and formatting options.
• Multiplicity: Unlike aspects, multiple facets of the same type can be attached to a single aspect (or part) if needed, as long as they have unique names. For example, one could have two text facets on a part, like “FirstName” and “LastName”, both of type Text. Each facet on a given aspect is identified by a unique name (or alias).
• Storage and Indexing: Facet values are stored as part of the content item’s data (likely in the JSON document representing the content item). The system should make facet values queryable by allowing index creation (discussed in the YesSql section). For instance, a NumericField might also create a numeric index for ranged queries.
• UI and Editors: Each facet type should come with a default editor template and display template (or use a generic mechanism) so that when building an admin UI, the system knows how to render input fields for that facet. The spec for the admin UI would detail how facets can be arranged on a content editor, but that is beyond core architecture – suffice it to say the content definition manager and the admin interface should work together to allow adding facets to aspects visually and editing them on content items.
• Resource Item Versioning: All resource items (the actual content instances) must support versioning by default. This means the system keeps track of multiple versions (revisions) of each item, with the ability to have a published version and draft versions. Specifics of versioning:
• The content storage should include a version identifier and a publication status for each resource item record. For example, a resource item could have fields like ContentItemId (persistent ID of the content), VersionId (auto-incrementing or GUID for each version), Published (bool), Latest (bool for the most recent version). This is similar to Orchard Core’s content item versioning approach.
• Only one version of an item is the current published version (per culture, if localization is considered). When an item is edited, a new draft version is created. The system should allow retrieving historical versions and possibly rolling back to a previous version.
• Merging and concurrency: if two processes attempt to edit an item simultaneously, the framework should handle this by either locking the content item or merging changes in a new version (to avoid silent overrides). This might involve optimistic concurrency checks (each version having an ordinal or timestamp to check).
• The versioning also ties into aspects: some aspects might need special versioning logic. For instance, an aspect representing a workflow or state machine might not want to duplicate state across versions, depending on design. However, by default, aspects and facets are versioned with the content item (meaning each version of the item has its own copy of aspect data).
• The specification should note that versioning can be turned off for certain resource types if not needed (for example, a log entry content type might be always immutable and not versioned). This could be controlled by settings on the resource type (like Orchard’s .Draftable() or .Versionable() settings ). The content definition manager API should expose these as options when creating or editing a type.
• Access Control via Aspects: The architecture does not bake in a specific access control mechanism into every content item, but it allows aspects to implement access control and other behaviors. For example:
• A SecurableAspect (or simply using a common aspect provided by the framework) can be attached to a resource type to make it “securable”. Orchard Core has a concept where a content type can be marked Securable and then each content item can have permissions set . In Aster, this could be realized by an aspect that contains, say, a list of user/role permissions or an owner field. The presence of this aspect on an item would trigger the security checking logic in the application to consult those permissions. The Aster framework should include hooks in the authorization system that, when a content item is accessed, it checks if the item’s type has a Securable aspect and if so, enforce the rules defined in it.
• Other Behaviors: Similarly, aspects can provide additional functionalities. An AuditAspect can automatically timestamp and record the editor of an item whenever it’s modified, by hooking into the content saving pipeline. A SchedulingAspect might allow content items to go live or expire at certain times (controlling visibility based on dates). Because aspects can contain logic (not just data), the system should have a way to invoke aspect logic at appropriate times. This could be via an event system (e.g., events for OnPublished, OnSaving, etc., that aspects can subscribe to) or by having aspects implement certain interfaces (like IContentItemValidation or IContentItemUpdated) that the core will call.
• Composition and Extensibility: The resource type definitions are stored in a way that modules can contribute. For example, a module might, during its migration or setup, add a new aspect to existing resource types (if the admin opts in). The specification should ensure that the content definition store and APIs allow dynamic composition: you can add or remove aspects/facets from types at runtime and the changes propagate (e.g., new fields appear on the editors, new data is stored). There should also be validation – for instance, you shouldn’t remove an aspect from a type if content items of that type exist and the aspect holds critical data, unless you also handle migrating or dropping that data. These operations might be restricted to administrators and possibly require data migration steps.
The Content Management Subsystem team is responsible for implementing the above in a Content Definition Manager (for managing types, aspects, facets) and a Content Manager (for creating, querying, and versioning content items). They must ensure that the design allows modules to define new aspect and facet types easily, and that the storage model (likely JSON documents in YesSql or a similar store) can accommodate dynamic schemas. Moreover, performance considerations (like indexing fields for querying) should be planned as described in the next section.
- YesSql Integration
Aster will use YesSql, a document database layer, for persisting application data such as content items, configuration, and indexes. Each module can extend the data storage by introducing its own document types and mapping logic (indices). The integration with YesSql must be structured so that data from different modules and features remains organized and queryable, and each tenant’s data is isolated according to the chosen multitenancy strategy.
• Document Database Overview: YesSql treats data as Documents stored in a relational database, optionally mapped to Index tables for querying. In Aster, many core concepts (like resource items, tenant settings, user accounts, etc.) will be stored as documents. The advantage is flexibility in schema and easy multi-tenant support via separate tables or collections. The team should configure YesSql to align with the multitenancy modes:
• In a Tenant ID Discriminator scenario (shared tables), a TenantId field should be part of each document record or implicitly part of the query (YesSql supports a notion of separate stores or table prefixes; if using a single store without prefixes, then adding a TenantId column to documents and indices is necessary). The system will ensure all YesSql queries are filtered by the current tenant’s ID.
• For Per-Database or Per-Schema modes, typically each tenant gets its own YesSql Store instance or at least a separate set of tables, so a TenantId column might not be needed. Instead, the store itself is isolated. The framework likely will instantiate one YesSql Store per tenant (or per shell) depending on configuration. Each store can have its own connection string or schema configuration. The specification should define a consistent approach: e.g., in per-database mode, one store per tenant; in per-schema mode, one store per tenant but pointing to same DB with a table prefix or schema differentiation.
• Collections: YesSql supports collections, which logically group documents so that their tables (Document, Index tables) have a distinct prefix. Aster should leverage collections to separate data domains. By default, content items might go into a “Content” collection, while internal settings might go into a “Configuration” collection, etc. Each module could potentially use a dedicated collection for its documents to avoid name collisions and to allow modular export/import of data. The specification demands that modules must declare which collection their documents belong to when registering their mappings.
• Module-defined Document Types: Modules are free to define new document types to represent data specific to their features. For example, a Blog module might define a BlogPost document (which could correspond to the BlogPost content item type), or a Workflow module might define a WorkflowInstance document for storing state. Each such document type is typically a POCO class that will be serialized to JSON for storage (unless stored via mapped indices alone). Requirements for modules defining documents:
• They should register these document types with YesSql’s mapping configuration. In practice, this may involve using a naming convention or a fluent mapping API. The YesSql Integration layer in Aster will provide an API for modules to call during startup (or in a migration) like Store.RegisterDocumentType() so that YesSql knows how to store and retrieve that type. If using JSON storage, the content of the document will be in a common Documents table (with a Type discriminator or separate table per collection).
• Each document type should ideally have a stable identity key (YesSql by default uses an Id property for internal identity). If a module wants to retrieve documents by some key other than Id, they should create an index for it.
• Document classes can also implement interfaces or base classes if needed (for instance, all content item documents might derive from a base class that includes common properties like Owner, CreatedUtc, etc.).
• Indices and Querying: YesSql’s indexing system allows creating RDBMS index tables for faster querying of documents by certain fields. Aster must provide a clear structure for modules to define and register Index Providers for their document types or aspects. Key points:
• A module can define an index class (or multiple) for any of its document types (or even for core document types if extension is needed). For example, an PublishedBlogPostIndex could have fields like BlogPostId, PublishedUtc, AuthorId, allowing efficient querying of blog posts by author or date without scanning all JSON.
• Modules register these indices by implementing YesSql’s IIndexProvider for the relevant document type T. The Aster framework will scan for all IIndexProvider implementations provided by enabled modules and register them with the YesSql Store. There should be a convention or a registration call in the module’s startup to add the index. For clarity, the spec could require modules to expose an AddIndexes(IStore store) method or use an assembly attribute that the core will reflect on.
• Collections for Indices: If a module’s documents are in a custom collection, its index provider must specify that collection when registering indices so that YesSql creates the index table in the correct context. The framework should facilitate this by maybe allowing a module to declare a default collection for all its content, or by requiring each IIndexProvider to indicate the collection name.
• Index Table Management: The system must ensure that index tables and document tables are created and updated as needed. YesSql typically can create tables on the fly when the session is initialized with new indices, but Aster should proactively create them during module installation to avoid runtime delays. This likely falls under migrations (see below). Each index corresponds to a table (or more, for map-reduce style indices). The naming of these tables typically includes the index name and collection. The spec should enforce a naming convention to avoid collisions (perhaps combining module and index name).
• Indices should be kept up-to-date automatically by YesSql on data changes (YesSql’s unit of work will update indices whenever a document is stored). However, the developers should ensure that certain large indices or cross-document indices are optimized. (For instance, if an index needs to combine data from multiple documents, a map-reduce pattern may be needed, which YesSql can support but might require careful design.)
• Database Migrations: Each module may need to set up initial data or schema when it is first enabled, and possibly update them in later versions. Aster must provide a structured migrations system (similar to Orchard Core’s Data Migration classes ) for modules to define these changes. Requirements for migrations:
• Migration Class: A module can include a class, typically named Migrations (or something implementing an IMigration interface), which contains methods to create or update schema/definitions. The initial migration method (often Create()) will run when the module’s feature is first enabled for a tenant. It can create things like new resource types, default content entries, or custom SQL tables if absolutely needed. Subsequent updates (e.g., UpdateFrom1(), UpdateFrom2(), etc.) run in order when the module’s version advances . Aster should adopt a similar convention, as it has proven effective: the migration method returns an integer “version” which is stored per tenant to know what the last applied migration is. The next time the module starts (or is updated), the framework calls the next update method(s) until up-to-date. The numbers must remain constant across versions of the module to avoid confusion.
• Schema Changes: While most data will reside in documents and indices, modules might occasionally require custom schema objects (for example, a spatial index might need a SQL function or a text search module might need a table for full-text indexing). The migrations should use YesSql’s schema builder abstractions (so that it’s database-agnostic). The spec should list out common operations: creating document collections, creating index tables (though these are often auto-generated, sometimes you might pre-create for complex indices), altering tables if needed for new fields, etc.
• Content Definitions in Migrations: Many modules will use migrations to set up content definitions (resource types, aspects, facets). For example, a Blog module on Create() might call into the Content Definition Manager to create a new resource type “Blog Post” with Title and Body aspects. These should run only once per tenant. Aster’s migration system must ensure that if two tenants enable the same feature, each tenant runs its own migration in isolation (so Tenant A enabling Blog won’t affect Tenant B until Tenant B also enables it). The migration record (which version a tenant is on for that feature) should be stored per tenant (perhaps in a Tenants_Configuration collection or similar).
• Execution Context: Migrations should run in the context of the tenant and shell they apply to. The system should open a YesSql session for that tenant’s store, and possibly a scope that includes the module’s services, then execute the migration methods. Proper error handling must be in place: if a migration fails (throws an exception), it should mark the feature as failed to enable and provide feedback to the admin. The migration could potentially be retried after fixing issues, but the system should not half-enable a feature without its migration completing.
• Sample Migration Steps: As an illustration, consider a module ForumModule with a feature Forums. On Create(), it might create a new resource type “Forum” and “ForumPost”, attach aspects like CommonPart or TitlePart to them, and maybe define a ForumPostIndex for quick lookup of posts by forum. It will call the ContentDefinition API to do this. Then it returns 1. Later, if the module is upgraded, an UpdateFrom1() might add a new facet (e.g., “IsSticky” boolean) to ForumPost and perhaps create a new index for sticky posts. The versioning ensures each tenant that had version 1 will get the update applied.
• Collections and Module Data Isolation: Aster should define a default set of YesSql collections and how modules use them:
• The Default Collection (perhaps named “Default” or “Content”) could hold most content-related documents (content items of various types). Alternatively, each tenant or each module could have its own collection for content items. But having a unified content collection per tenant simplifies queries across types.
• A Configuration Collection might store things like tenant settings, content type definitions, and other configuration documents, separate from content. This way, one can backup content vs config separately or query them without overlap.
• Modules that store non-content data (like a Logging module storing log entries) might use their own collection (“Logging” collection) to avoid bloating the main content tables. The specification should encourage module authors to use the default collections unless isolation is needed, to keep schema proliferation manageable.
• Under the hood, if using table prefix or schema per tenant, the collection names might be combined with that. For example, tenant1 + collection “Default” might result in tables named Tenant1_Default_Document, Tenant1_Default_Index, etc., or in schema Tenant1 with tables Document, Index. The Data access implementation team should devise a naming scheme that ensures uniqueness and clarity.
• Performance and Indexing Strategy: The YesSql integration must also consider performance for queries:
• Frequently queried fields should have indices. The framework might provide some common indices out-of-the-box (like an index on ContentItemId and ContentType for content items, to quickly fetch all items of a type).
• Modules should declare indices for any property that will be used in queries or filters. For example, if a Commerce module has an Order document with OrderDate, and the UI needs to list orders by date, an index on OrderDate is warranted.
• Lazy vs Eager Indexing: YesSql updates indices on document changes within a session. This is generally fine, but if some indexing is expensive, the design could allow disabling certain indices or updating them in background. However, initial implementation should focus on correctness – ensuring all necessary indices are created – and then consider optimization flags as needed.
• Engineers must ensure that all index creations and queries respect the tenant boundaries. It might be helpful to encapsulate the YesSql ISession per tenant such that any query automatically scopes to the tenant’s store/prefix. This can be done by providing a tenant-specific ISession from a scoped service (like Orchard’s ISession is typically tenant-scoped in Orchard Core).
In summary, the YesSql integration provides the persistence backbone for Aster’s modular data. The Data Access engineering team must implement the registration of document mappings and indices in a way that is straightforward for module developers (perhaps via a fluent configuration or by scanning assembly attributes). Additionally, they must handle the initialization of the database for each tenant (creating necessary tables, schemas, or databases based on the multitenancy config) and coordinate with the migrations system so that all modules’ data needs are met when features go live. Thorough testing is required to verify that enabling a feature correctly creates the data structures and that disabling (or upgrading a module) doesn’t break data integrity.
- API Exposure
By default, the Aster framework’s core does not mandate any specific web API (such as REST or GraphQL) for interacting with content or services. However, it is designed such that modules can optionally expose their own APIs in a consistent and isolated manner. This extensibility ensures that if a feature needs to provide external access (e.g., a REST endpoint for a mobile app, or a GraphQL schema for queries), it can do so without special-case changes to the core.
• Optional API Modules: Modules may include API controllers, endpoints, or middleware as part of their features. If a module chooses to expose an API:
• It can provide one or more ASP.NET Core MVC controllers or Minimal API endpoints. When the module is loaded, these endpoints should be integrated into the application’s routing map only for tenants where the feature is enabled. This implies that the routing system needs to be tenant-aware. The framework can achieve this by registering routes/endpoints inside the tenant scope during shell activation. Concretely, if a module has a controller with route /api/xyz, that route becomes active in the application pipeline, but the controller internally should resolve the current tenant context and operate on that tenant’s data. If a tenant does not have the module enabled, calls to that route under that tenant should return a not found (or the route not be registered at all for that tenant).
• Modules can define API endpoints under a specific URL prefix or area to avoid collisions. For example, a module “Blog” might expose endpoints under /api/blog/*. The specification should encourage module developers to use an Area (in MVC terms) or similar grouping for their endpoints. The Aster framework can assist by automatically assigning module endpoints to an area corresponding to the module name. This way, routing can easily enable/disable entire areas per tenant.
• If a module requires additional pipeline components (like authentication schemes or specialized middleware), it should be able to register them when the module is enabled. The design might allow modules to contribute to the global middleware pipeline during startup. However, since that pipeline is typically application-wide, any middleware should internally check for tenant/feature enablement if it affects request processing. Alternatively, the shell could maintain a tenant-specific middleware pipeline (this is complex; likely we stick to global pipeline and inside it call tenant-specific logic).
• GraphQL and Other APIs: Although not in the base spec, the architecture should not preclude GraphQL or other API styles. If a GraphQL module is added, it should be able to plug into Aster to expose a GraphQL endpoint. For instance, Orchard Core has an optional GraphQL module that, when enabled, provides a /api/graphql endpoint and allows querying content items . Aster should allow a similar approach:
• A GraphQL module could add a single endpoint (e.g., /api/graphql) and use the content definition metadata to dynamically build a schema for the types and fields. It would likely depend on other modules being present to supply types (for example, if the Blog feature is enabled, the GraphQL module should detect a BlogPost content type and add it to the GraphQL schema). This implies that modules can communicate: the GraphQL module might offer an interface or convention where other modules register GraphQL field mappings. The core should support such inter-module communication without coupling (perhaps via a common service or attribute that the GraphQL module looks for).
• Security for APIs: Any exposed API must respect the multitenancy and security boundaries. For REST controllers, standard ASP.NET Core authorization filters can ensure only the tenant’s users access their data. For cross-tenant admin APIs (if any), the design should avoid exposing one tenant’s data to another. Essentially, an API call is always processed in the context of a single tenant. If an admin tool needs to manage multiple tenants, that would likely be a host-level API not covered by normal modules (and out of scope for base spec).
• Rate limiting or other cross-cutting concerns could be implemented as modules or middleware, which should also be considered if, for example, an API module wants to throttle requests.
• Isolation of Module APIs: The framework must ensure that if a module is disabled or not present, its API endpoints are not reachable. This dynamic enabling/disabling of endpoints can be handled by building the endpoint mappings at tenant startup time. For example, when a tenant is started, after determining enabled features, the system goes through each enabled feature’s contributions (like routes, GraphQL types, etc.) and activates them. Conversely, if a feature is disabled on a running tenant, the system should ideally remove or deactivate its endpoints. While ASP.NET Core does not natively support removing controller routes at runtime, Aster could maintain a route constraint or a custom router that checks feature flags. A simpler approach is that feature enable/disable requires a tenant restart to fully apply (which in Aster’s context could mean reloading that tenant’s shell or forcing reinitialization). This is an acceptable trade-off for complexity; the specification can state that changing feature configuration might necessitate a lightweight tenant reload to update available APIs.
• Documentation and Usage: It is expected that modules exposing APIs will also expose documentation (e.g., OpenAPI/Swagger support if REST, or schema if GraphQL). Aster’s core might include an API explorer page that aggregates module APIs. However, since this is a specification for the framework, it’s sufficient to ensure the structure supports it. For instance, if using Swagger, modules could include XML comments for their controllers and, if the Swagger module (if one exists) is enabled, it can pick up all areas including those modules. The module should tag its endpoints with the area or group name so they appear logically separated in API docs.
• Example Use Cases:
• A Commerce module might expose a REST API to fetch products or submit orders, enabling headless commerce scenarios. It would have controllers like ProductsController with [HttpGet] /api/commerce/products etc. When this feature is enabled for a tenant, customers can call those endpoints to get product data from that tenant’s catalog. If the feature is disabled, those endpoints are not active.
• A Content Management API module (perhaps developed later) could expose generic CRUD endpoints for any resource type (similar to Orchard Core’s content APIs or GraphQL). This would not be in the core, but the core must allow that module to iterate content definitions and provide endpoints accordingly.
• A Realtime module could even integrate SignalR hubs or gRPC services. The spec doesn’t detail those, but it should be possible to plug them in. For example, if a module wants to open a SignalR hub for live notifications, it can register that in the services and map it in the endpoint routing when the feature is enabled.
In conclusion, API exposure is an opt-in capability: Aster’s baseline remains a modular web application framework, and APIs are treated as just another type of feature a module can offer. The responsibility lies with module developers to use the extension points (controllers, endpoints, GraphQL integrations) provided by ASP.NET Core and Aster’s modular system. The core team must ensure that the modular system is compatible with these ASP.NET Core features — meaning that all needed services like MVC, Razor, etc., are properly configured to discover controllers or endpoints in modules. The framework should include testing scenarios where a module is added/removed and its API appears/disappears accordingly. By keeping API concerns modular, Aster stays flexible for a variety of application types (CMS, headless backend, SaaS platform APIs) without imposing a one-size-fits-all API in the core.
⸻
Responsibility Summary: This specification defines a clear separation of concerns. The Multitenancy features will be implemented by the platform core (shell management, tenant resolution, isolation) by engineers focusing on hosting and infrastructure. The Modularity System requires contributions from framework developers to build the module loader and from module developers to correctly declare their features and services. The Resource Type (Content) system is the domain of the content management team, providing runtime composition of content structures. YesSql integration will be handled by the data persistence team, ensuring a robust multi-tenant document store and index management. Finally, API exposure is largely left to module developers, with the core team ensuring the underlying ASP.NET Core pipeline is multi-tenant aware. All teams must collaborate to ensure these pieces fit together seamlessly: for instance, enabling a feature triggers the appropriate migrations (data team) and makes new endpoints available (API team) while respecting tenant isolation (multitenancy team). By following this specification, engineers will deliver a platform that is modular, extensible, and suitable for complex multi-tenant applications.