Compare commits

...

76 Commits

Author SHA1 Message Date
Loretta c95ec68c09 EKÁER integration, product search, and schema updates
- Added EKÁER (NAV) service registrations and config to PluginNopStartup
- Extended MgProductDto/IMgProductDto with Sku property
- Raised min search term length to 3 in autocomplete endpoints
- Increased ProductSearchAutoComplete maxResults to 300
- Refactored product search to filter by AvailableQuantity > 0
- Closed GTIN/VTSZ issue in EKAER_ISSUES.md, moved to data model topic
- Added SCHEMA.md with Toon domain model, including Currency property for partners
2026-06-03 16:59:23 +02:00
Loretta 1b11ca2579 Add XML doc conventions and update references
Added a section on XML documentation standards to CONVENTIONS.md, clarified references to core and Mango.Nop framework convention docs, and improved formatting. Enabled EmitCompilerGeneratedFiles in Mango.Nop.Core.csproj.
2026-05-23 09:27:34 +02:00
Loretta 65bf004808 Update AcBinarySerializable attribute signature
Updated all affected entities to use the new AcBinarySerializable attribute signature, adding an extra boolean parameter (false) as the last argument. No other logic or code was changed.
2026-05-15 08:41:59 +02:00
Loretta 474cb99754 [LOADED_DOCS: 3 files, no new loads]
Update AcBinarySerializable attribute to 5 params

Updated AcBinarySerializable to use a five-parameter signature across all relevant entities and DTOs. Adjusted CONVENTIONS.md to document the new attribute usage. Refactored MiscSignalRApiPlugin constructor to a single-line parameter list for clarity.
2026-05-11 20:25:42 +02:00
Loretta cb9a381400 [LOADED_DOCS: 2 files, no new loads]
Update DB connection, protocol flush policy, and docs

- Switch appsettings.json to production DB connection string
- Update copilot-instructions.md to reference Rules #1-6
- Set FlushPolicy.DoubleBuffered for AcBinary protocol in PluginNopStartup.cs
2026-05-05 15:06:06 +02:00
Loretta 1a0bd01500 [LOADED_DOCS: 1 files, no new loads]
Add repo prefixes and clarify workspace dependencies

Added prefix, type, and layer properties to each @repo block in copilot-instructions.md. Updated own-dep-repos arrays for clearer dependency paths. Adjusted [LOADED_DOCS: ...] prefix instructions to reflect correct file counts. These changes enhance multi-repo management and documentation protocol clarity.
2026-04-26 19:13:04 +02:00
Loretta 7fb87283ce [LOADED_DOCS: NONE]
Refactor skill loading protocol in copilot-instructions

Clarified session setup: only two reactive skills are pre-loaded, with three user-gated skills now lazy-loaded on demand. Updated documentation to add "AUTHORITY, RULE SCOPE, AND SKILL INVOCATION" section, revised skill trigger descriptions, and adjusted `[LOADED_DOCS: ...]` prefix logic. Documented docs-archive as a lazy-loaded skill. Added settings.local.json for local file removal permissions. No code logic changes—documentation and protocol only.
2026-04-26 13:44:27 +02:00
Loretta 599f8a6787 Refactor docs: topic folders, navigation, protocol sync
- Restructured documentation: added `docs/README.md` to each sub-project, moved LOGGING and SIGNALR docs into dedicated subfolders with their own `README.md`.
- Updated all cross-references to use new topic folder paths and canonical AyCode.Core doc locations.
- Updated `.csproj` files to auto-include all Markdown docs and project-level `README.md` files.
- Removed obsolete single-file docs, replaced with structured content in topic folders.
- Enforced AI Agent Protocol: session setup, output prefix, no-re-read, and mandatory `docs-check` skill after code changes.
- Added domain-critical reminders and navigation guidance to relevant `README.md` files.
2026-04-25 07:24:39 +02:00
Loretta 25e5ded777 Update AI Agent protocol docs and config/debug logic
- Switched to new `[LOADED_DOCS: N files (+K this turn: ...)]` prefix format in all protocol docs, with examples and enforcement details.
- Documented `docs-discovery` skill, protocol history, and documentation-first coding requirements.
- Added protocol documentation for `Nop.Plugin.Misc.AIPlugin`.
- Changed SQL connection string to use `FruitBank_DEV` in `appsettings.json`.
- Wrapped `IHubProtocol` debug output in `#if DEBUG` in `PluginNopStartup.cs`.
2026-04-24 08:20:19 +02:00
Loretta 6e3ca4a1e3 Add Mango.Nop.Core docs: architecture, conventions, rules
Added Mango.Nop.Core section to copilot-instructions.md detailing workspace dependencies, framework-first design, and separation from plugin logic. Introduced ARCHITECTURE.md to define framework vs. consumer boundaries, entity mirroring, and DTO parameterization. Added CONVENTIONS.md to formalize placement rules, naming conventions, and anti-patterns, referencing AyCode.Core for shared doctrine.
2026-04-23 16:11:49 +02:00
Loretta 8d3c2a8462 [LOADED_DOCS: .github\copilot-instructions.md]
Update DB config, docs, and SignalR binary protocol setup

- Switched appsettings.json connection string to production DB.
- Added "Shared Agent Skills" section to copilot-instructions.md in both main and Mango.Nop Libraries repos.
- Refactored PluginNopStartup.cs: improved using statements, updated SignalR to use AddAcBinaryProtocol with explicit options, removed manual IHubProtocol registration, and added protocol diagnostics.
- No business logic changes; focused on configuration, documentation, and infrastructure.
2026-04-22 22:45:05 +02:00
Loretta b494018e9c Strengthen doc-first protocol and clarify Claude mapping
Updated copilot-instructions.md to enforce stricter documentation-first and multi-repo rules, including cross-repo doc loading, per-question doc checks, and improved context recovery triggers. Broadened explicit consent to all file types. Updated CLAUDE.md with sequential execution override and tool mapping, ensuring Claude follows the same protocol and rule set.
2026-04-04 20:52:57 +02:00
Loretta 3725b4c2fd Enforce doc-first protocol, add SGen, modular plugin docs
- Enforced strict documentation-first AI agent protocol in `.github/copilot-instructions.md` (multi-repo, no auto doc edits, explicit consent, [LOADED_DOCS] prefix, [DOCUMENTATION CHECK] on code changes)
- Updated solution structure: added `docs` folder to solution items for LLM/AI context
- Integrated AyCode SGen (source-generated binary serialization) with forced runtime registration; documented SGen usage and exclusions
- Overhauled plugin `README.md` and added modular docs: `SCHEMA.md`, `DOMAIN_MODEL.md`, `MEASUREMENT.md`, `DATA_LAYER.md`, `AI_SERVICES.md`, `SIGNALR_ENDPOINTS.md`
- Updated `CLAUDE.md` to require reading copilot instructions first
- Switched appsettings connection string to production DB
- Minor doc clarifications, corrects, and project file updates
2026-04-02 22:19:29 +02:00
Loretta bb08c7ae61 Update LLM instruction files for token efficiency and cross-repo navigation
- CLAUDE.md: reduced to single-line pointer to copilot-instructions.md (eliminates redundant auto-loaded content)
- copilot-instructions.md: added @repo name field, relative paths in own-dep-repos, "do not re-read .md files" rule, and explicit permission to navigate external repos

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-31 16:15:02 +02:00
Loretta 5535011df9 Comprehensive documentation overhaul and expansion
- Added detailed .md docs for Core, Data, and Services projects: DTOs, NopDependencies, repositories, transactions, services, and logging bridge
- Updated all project README.md files with metadata, documentation tables, inheritance diagrams, and clearer dependency/reference explanations
- Root README.md now includes a documentation map and improved project/type tables
- Expanded and clarified `.github/copilot-instructions.md` as the single source of truth for domain rules, patterns, and conventions
- Updated ARCHITECTURE.md, CONVENTIONS.md, and GLOSSARY.md to document DTO strategies, transaction and logging patterns, project boundaries, and AyCode integration
- Added CLAUDE.md for Claude-specific code authoring guidance
- All .csproj files now include new docs as non-compilable content
- Ensured clear separation of concerns and improved AI/tooling discoverability across the codebase
2026-03-30 10:26:35 +02:00
Loretta 2303e99f95 Clarify docs: conventions, layering, timestamp interface
Updated documentation to clarify .md file update scope and add rules for documentation layering. Replaced inline dependency graph in README.md with a reference to ARCHITECTURE.md. Corrected timestamp interface references from ITimeStampUpdated to ITimeStampModified for consistency. Clarified nopCommerce dependency boundaries.
2026-03-29 18:30:05 +02:00
Loretta f47701af59 Add architecture docs for Mango.Nop & update dev DB config
Added detailed documentation: ARCHITECTURE.md, CONVENTIONS.md, GLOSSARY.md, and copilot-instructions.md for Mango.Nop.Core, Data, and Services. Updated/added README.md files for all Mango.Nop libraries and the FruitBank nopCommerce plugin, clarifying structure, key types, and usage. Switched appsettings.json connection string from production to development database. These changes improve developer onboarding and enforce architectural consistency.
2026-03-29 10:44:03 +02:00
Loretta 1a9e0dcf0b Merge branch '4.80' into FruitBank_v0.0.8.0 2026-03-28 16:22:34 +01:00
Loretta 1898f4d517 Update DLL HintPaths to use $(Configuration); prod DB switch
Replaced hardcoded Debug paths in all .csproj files with $(Configuration) for AyCode and FruitBank DLL references, enabling correct DLL resolution for any build configuration. Switched appsettings.json connection string to use the FruitBank_PROD database. Added a null check for StockTaking in StockSignalREndpointServer.cs to prevent possible null reference exceptions. Noted some encoding issues in .csproj comments and copyright fields. No other functional changes.
2026-03-24 18:30:43 +01:00
Loretta ced4b98908 Add serialization attributes and update to PROD config
Annotated core entities with AcBinarySerializable and ToonDescription for improved serialization and documentation. Updated appsettings.json to use the production database. Changed solution file to target Visual Studio 17. Added necessary using statements and metadata for entity classes.
2026-03-20 17:06:49 +01:00
Loretta e47c7c8f74 Remove IDE-specific workspace and layout files
Deleted .wsuo, DocumentLayout.json, and VSWorkspaceState.json files that store user-specific IDE state and metadata. This cleanup prevents sharing of personal or machine-specific settings in version control.
2026-02-03 18:24:17 +01:00
Loretta ca1ac2a2d1 Restore Product class and enums; add Toon metadata
Re-enable Product class and related enums in Nop.Core.Domain.Catalog, moving them from commented to active code. Add [ToonDescription] attributes to Product properties for enhanced documentation and serialization. Mark sensitive Customer fields with [ToonIgnore] to exclude them from Toon processing.
2026-01-17 09:19:46 +01:00
Loretta c0073815ae Add ToonDescription attributes to core entities and DTOs
Introduced ToonDescription annotations across DTO and entity classes to provide metadata, business rules, and documentation for classes and properties. Updated usings to include AyCode.Core.Serializers.Toons where needed. These changes improve code clarity and support tooling for documentation and code generation.
2026-01-15 11:34:06 +01:00
Loretta 954b99fcbc Add OrderStatus enum property to Order class
Introduced the OrderStatus property as an enum wrapper for OrderStatusId in the Order class. This enhances type safety and code readability by allowing direct use of the OrderStatus enum. The property is annotated with ToonDescription to clarify its purpose and business rule.
2026-01-15 11:33:51 +01:00
Loretta 7bcd601035 Add ToonDescription attributes for enhanced metadata support
Introduced Toon serialization/description attributes to CustomerDto, GenericAttribute, and OrderNote classes. Added table mapping and foreign key metadata, as well as necessary using statements, to improve serialization, documentation, and tooling capabilities.
2026-01-14 21:05:20 +01:00
Loretta 0eeefedee3 Add AyCode.Core.Helpers and Serializers usings to files
Added using statements for AyCode.Core.Helpers across several files and AyCode.Core.Serializers.Binaries in DevAdminSignalRHubSandbox.cs to enable new helper and serializer functionalities. No other code changes were made.
2025-12-19 07:14:58 +01:00
Loretta 99bfaf960b Minimal SignalR test hub and endpoint for isolated testing
Refactored Mango.Sandbox.EndPoints to enable minimal, dependency-light SignalR endpoint testing. Introduced DevAdminSignalRHubSandbox and TestSignalREndpoint for protocol/contract tests without full NopCommerce/FruitBank infra. Added SignalRClientSandbox and comprehensive MSTest coverage for all parameter types. Simplified Program.cs startup, updated project references, and added minimal logger. Original SignalREndpointTests replaced with focused, low-level and high-level tests. CORS and DTOs updated for compatibility.
2025-12-11 23:46:36 +01:00
Loretta 3c479e1ad5 Add stock-taking enhancements and validation updates
Enhanced stock-taking functionality by introducing the `CloseStockTaking` method in `StockTakingDbContext` to manage session closures with validation. Added the `IsReadyForClose` method to `IMgStockTaking` and its implementation in `MgStockTaking`. Integrated `FruitBankDbContext` for stock updates.

Improved logging in `StockSignalREndpointServer` and added a new SignalR endpoint for closing stock-taking sessions. Updated `AddStockTaking` to initialize properties for new entries.

Simplified loading logic in `OrderItemDtoDbTable` by commenting out unnecessary `.ThenLoad` chains. Enhanced validation in `RefreshStockTakingItemMeasuredValuesFromPallets` and adjusted return statements in `AddOrUpdateMeasuredStockTakingItemPallet` and `UpdateStockTakingItemPallet` to fetch updated entities.

These changes improve maintainability, robustness, and functionality across the codebase.
2025-12-08 15:49:50 +01:00
Loretta 120ed09738 .Net10, VS2026; StockTaking in progress... 2025-12-01 16:17:57 +01:00
Loretta b21197ca67 improvements, fixes 2025-11-26 09:42:07 +01:00
Loretta 6b09ba3d78 improvements, fixes, etc... 2025-11-24 08:27:02 +01:00
Loretta 4c5d5d55c4 improvements 2025-11-21 07:20:42 +01:00
Loretta 82c3b5a7cd StockQuantityHistoryExt, StockQuantityHistoryDto; improvements, fixes; 2025-11-13 19:58:39 +01:00
Loretta 7946a52219 improvements, fixes, etc.. 2025-11-11 20:51:28 +01:00
Loretta 5277a090fb Add MessagePack packages 2025-11-07 19:21:51 +01:00
Loretta ed7a2e8cd6 BaseEntity.ToString() 2025-11-07 05:56:47 +01:00
Loretta bef91e0131 ... 2025-11-06 15:11:59 +01:00
Loretta 2bd97c88ca Mango.Nop.Core cleanup; 2025-11-05 14:56:40 +01:00
Loretta 0da0565134 Add Mango.Nop.Data project to solution 2025-11-04 15:57:09 +01:00
Loretta ec71f81d4c SignalR improvements; etc... 2025-10-30 14:54:33 +01:00
Loretta df9157c4ab Price calculation improvements, fixes, etc... 2025-10-24 15:47:45 +02:00
Loretta 08069dda58 fixes 2025-10-24 12:01:48 +02:00
Loretta 7fb047fd28 Implement CheckAndUpdateProductManageInventoryMethodToManageStock to MgEventConsumer; improvements; 2025-10-24 08:12:35 +02:00
Loretta 56927f50ea GeneratePackageOnBuild false 2025-10-23 11:27:00 +02:00
Loretta 408fa0f87e refactoring, cleanup v1.1... 2025-10-22 16:20:36 +02:00
Loretta d52deeb2b4 refactoring, cleanup, etc... 2025-10-22 15:16:20 +02:00
Loretta 38e036553e noplogger improvements, fixes, etc... 2025-10-21 15:30:43 +02:00
Loretta fd39b0700a Add MeasurementOwnerId; RevisorId generic attributes to Order; improvements, fixes, etc.. 2025-10-20 16:46:47 +02:00
Loretta a0bb6117ae ShippingDocumentToFiles fix; CustomOrder create improvements; 2025-10-19 15:08:38 +02:00
Loretta 55f626c71d nuget packages update; fixes 2025-10-18 18:45:49 +02:00
Loretta c4742f0d86 improvements, fixes, etc... 2025-10-18 08:43:36 +02:00
Loretta f7f2d30915 improvements, fixes, etc... 2025-10-16 11:43:51 +02:00
Loretta 28de927bd9 ... 2 2025-10-15 08:08:12 +02:00
Loretta c397c68601 improvements, fixes, etc... 2025-10-13 14:18:14 +02:00
Loretta a5e3702616 fixes 2025-10-12 18:07:59 +02:00
Loretta a2ed202276 improvements, fixes, etc... 2025-10-12 07:47:55 +02:00
Loretta cefb19584d product event fixex; etc... 2025-10-11 17:51:30 +02:00
Loretta 28d9122818 improvements, etc 2025-10-11 12:52:55 +02:00
Loretta 6fecdd2317 ShippingItemPallets improvements 2025-10-05 14:55:36 +02:00
Loretta daa1421011 improvements, fixes, etc... 2025-10-03 07:19:24 +02:00
Loretta 96eb509ec5 improvements, fixes, etc... 2025-09-30 18:18:22 +02:00
Loretta 19cc506fc2 improvements, fixes, etc... 2025-09-30 07:24:36 +02:00
Loretta 08f91408c3 improvements, fixes, etc... 2025-09-29 13:33:39 +02:00
Loretta 32c37cf22c improvements, fixes 2025-09-23 12:17:23 +02:00
Loretta 871078c4ab improvements, fixes, etc... 2025-09-23 10:41:38 +02:00
Loretta dc8d41c4e8 nuget packages update 2025-09-19 14:44:25 +02:00
Loretta b0a99e139b Add FullName to CustomerDto 2025-09-17 05:31:49 +02:00
Loretta 5e2c30a4ec improvement, fixes, etc... 2025-09-12 13:34:54 +02:00
Loretta ef16260ebe improvements, fixes, etc... 2025-09-11 12:51:58 +02:00
Loretta 60f0071ade NopLogWriter fixes; SignalRLogger fixes; etc... 2025-09-05 06:18:01 +02:00
Loretta a1f7c8af2d Add appsettings.json to NopCommerce project! install reference packages; Implement endpoints; Fix CORS; etc... 2025-09-03 07:07:22 +02:00
Loretta 19662fd213 temp 2025-09-02 15:21:57 +02:00
Loretta 1c7980e209 remove Nop.Core, Nop.Data, Nop.Services from common library 2025-08-28 15:24:40 +02:00
Loretta eefb0392aa outputpath set to "bin\FruitBank" 2025-08-28 15:20:36 +02:00
Loretta 761001665f references fixed2 2025-08-28 14:53:58 +02:00
Loretta 9c363e360c references fixed 2025-08-28 14:51:35 +02:00
3113 changed files with 3114 additions and 204993 deletions

178
.github/copilot-instructions.md vendored Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@ -1,47 +0,0 @@
{
"Version": 1,
"WorkspaceRootPath": "H:\\Applications\\Mango\\Source\\NopCommerce.Common\\4.70\\Libraries\\",
"Documents": [],
"DocumentGroupContainers": [
{
"Orientation": 0,
"VerticalTabListWidth": 256,
"DocumentGroups": [
{
"DockedWidth": 200,
"SelectedChildIndex": -1,
"Children": [
{
"$type": "Bookmark",
"Name": "ST:129:0:{1fc202d4-d401-403c-9834-5b218574bb67}"
},
{
"$type": "Bookmark",
"Name": "ST:129:0:{116d2292-e37d-41cd-a077-ebacac4c8cc4}"
},
{
"$type": "Bookmark",
"Name": "ST:128:0:{1fc202d4-d401-403c-9834-5b218574bb67}"
},
{
"$type": "Bookmark",
"Name": "ST:130:0:{116d2292-e37d-41cd-a077-ebacac4c8cc4}"
},
{
"$type": "Bookmark",
"Name": "ST:131:0:{116d2292-e37d-41cd-a077-ebacac4c8cc4}"
},
{
"$type": "Bookmark",
"Name": "ST:132:0:{116d2292-e37d-41cd-a077-ebacac4c8cc4}"
},
{
"$type": "Bookmark",
"Name": "ST:128:0:{116d2292-e37d-41cd-a077-ebacac4c8cc4}"
}
]
}
]
}
]
}

View File

@ -1,7 +0,0 @@
{
"ExpandedNodes": [
""
],
"SelectedNode": "\\H:\\Applications\\Mango\\Source\\NopCommerce.Common\\4.70\\Libraries",
"PreviewInSolutionExplorer": false
}

17
CLAUDE.md Normal file
View File

@ -0,0 +1,17 @@
CRITICAL: Your FIRST action in every session MUST be reading `.github/copilot-instructions.md`. Execute ALL session-start instructions found there before responding to any user query. It is the single source of truth for this repo.
## SEQUENTIAL EXECUTION OVERRIDE
The AI AGENT CORE PROTOCOL in copilot-instructions.md requires STRICT SEQUENTIAL execution. This OVERRIDES your default parallelization behavior. Do NOT parallelize doc reads with code searches. The sequence is:
1. Read copilot-instructions.md → process its rules FULLY
2. Read ALL docs/ .md files listed in the protocol → wait for completion
3. Output [LOADED_DOCS: ...] prefix
4. ONLY THEN respond to the user's query or search code
## Tool mapping for AI AGENT CORE PROTOCOL
The copilot-instructions.md references Copilot tool names. Map them to Claude Code tools:
- `get_file` / `file_search``Read`, `Glob`, `Grep`
- `code_search` / `get_symbols_by_name` / `find_symbol``Grep`, `Glob`
- `replace_string_in_file` / `edit_file``Edit`
- `create_file``Write`
Follow the protocol using YOUR tools. The rules (LOADED_DOCS prefix, hard-gate, no-re-read, context recovery, explicit consent) apply equally to Claude Code.

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
namespace Mango.Nop.Core { public static class AcBinaryForcedInit { public static void ForceRegister() { System.Console.WriteLine("[SGEN TESTING] AcBinaryForcedInit called."); AyCode.Core.Serializers.Generated.AcBinaryGeneratedWritersInit.Register(); } } }

View File

@ -0,0 +1,60 @@
using AyCode.Core.Serializers.Attributes;
using AyCode.Core.Serializers.Toons;
using Nop.Core.Domain.Catalog;
using Nop.Core.Domain.Common;
using Nop.Core.Domain.Customers;
namespace Mango.Nop.Core.Dtos;
[AcBinarySerializable(false, true, false, true, false, false)]
[LinqToDB.Mapping.Table(Name = nameof(Customer))]
[System.ComponentModel.DataAnnotations.Schema.Table(nameof(Customer))]
[ToonDescription($"Data transfer object for {nameof(Customer)}", TypeRelation = ToonTypeRelation.DtoOf, RelatedTypes = [typeof(Customer)])]
public class CustomerDto : ModelDtoBase<Customer>, ISoftDeletedEntity
{
public string Username { get; set; }
public string Email { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
[ToonDescription(BusinessRule = "get => $\"{LastName} {FirstName}\"")]
public string FullName => $"{LastName} {FirstName}";
public int RegisteredInStoreId { get; set; }
public bool Deleted { get; set; }
public CustomerDto() :base()
{ }
public CustomerDto(int customerId) : base(customerId)
{ }
public CustomerDto(Customer customer) : base(customer)
{
}
public override void CopyDtoValuesToEntity(Customer entity)
{
base.CopyDtoValuesToEntity(entity);
entity.Username = Username;
entity.FirstName = FirstName;
entity.LastName = LastName;
entity.Email = Email;
entity.RegisteredInStoreId = RegisteredInStoreId;
entity.Deleted = Deleted;
}
public override void CopyEntityValuesToDto(Customer entity)
{
base.CopyEntityValuesToDto(entity);
Username = entity.Username;
FirstName = entity.FirstName;
LastName = entity.LastName;
Email = entity.Email;
RegisteredInStoreId = entity.RegisteredInStoreId;
Deleted = entity.Deleted;
}
}

View File

@ -0,0 +1,22 @@
using AyCode.Interfaces;
using AyCode.Interfaces.Entities;
using Nop.Core;
namespace Mango.Nop.Core.Dtos;
public interface IModelDtoBaseEmpty : IAcModelDtoBaseEmpty
{
}
public interface IModelDtoBase : IEntityInt, IModelDtoBaseEmpty
{
}
public interface IModelDtoBase<TMainEntity> : IModelDtoBase where TMainEntity : BaseEntity
{
TMainEntity CreateMainEntity();
void CopyDtoValuesToEntity(TMainEntity entity);
void CopyEntityValuesToDto(TMainEntity entity);
}

View File

@ -0,0 +1,54 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using AyCode.Interfaces.Entities;
using Nop.Core.Domain.Common;
namespace Mango.Nop.Core.Dtos
{
public interface IGenericAttributeDto : IModelDtoBase<GenericAttribute>
{
public int EntityId { get; set; }
public string KeyGroup { get; set; }
public string Key { get; set; }
public string Value { get; set; }
public int StoreId { get; set; }
public DateTime? CreatedOrUpdatedDateUTC { get; set; }
}
public abstract class MgGenericAttributeDto : GenericAttribute, IGenericAttributeDto
{
public GenericAttribute CreateMainEntity()
{
var mainEntity = Activator.CreateInstance<GenericAttribute>();
CopyDtoValuesToEntity(mainEntity);
mainEntity.CreatedOrUpdatedDateUTC = DateTime.UtcNow;
return mainEntity;
}
public void CopyDtoValuesToEntity(GenericAttribute entity)
{
entity.Id = Id;
entity.Key = Key;
entity.Value = Value;
entity.EntityId = EntityId;
entity.KeyGroup = KeyGroup;
entity.StoreId = StoreId;
entity.CreatedOrUpdatedDateUTC = CreatedOrUpdatedDateUTC;
}
public void CopyEntityValuesToDto(GenericAttribute entity)
{
Id = entity.Id;
Key = entity.Key;
Value = entity.Value;
EntityId = entity.EntityId;
KeyGroup = entity.KeyGroup;
StoreId = entity.StoreId;
CreatedOrUpdatedDateUTC = entity.CreatedOrUpdatedDateUTC;
}
}
}

View File

@ -0,0 +1,116 @@
using AyCode.Core.Extensions;
using AyCode.Core.Serializers.Toons;
using AyCode.Core.Helpers;
using LinqToDB.Mapping;
using Mango.Nop.Core.Entities;
using Mango.Nop.Core.Interfaces;
using Newtonsoft.Json;
using Nop.Core;
using Nop.Core.Domain.Common;
using Nop.Core.Domain.Customers;
using Nop.Core.Domain.Orders;
using Nop.Core.Domain.Payments;
using Nop.Core.Domain.Shipping;
using System.Linq.Expressions;
namespace Mango.Nop.Core.Dtos;
[ToonDescription("Base DTO for orders with items, customer and status tracking")]
public abstract class MgOrderDto<TOrderItemDto, TProductDto> : MgEntityBase, IModelDtoBase<Order>, IMgOrderDto<TOrderItemDto, TProductDto> where TOrderItemDto : IMgOrderItemDto<TProductDto> where TProductDto : IMgProductDto
{
public Guid OrderGuid { get; set; }
public int StoreId { get; set; }
public int CustomerId { get; set; }
public int OrderStatusId { get; set; }
public int ShippingStatusId { get; set; }
public decimal OrderDiscount { get; set; }
public decimal OrderTotal { get; set; }
public DateTime CreatedOnUtc { get; set; }
public DateTime? PaidDateUtc { get; set; }
public int PaymentStatusId { get; set; }
public string ShippingMethod { get; set; }
public string CustomOrderNumber { get; set; }
public string CustomValuesXml { get; set; }
public bool Deleted { get; set; }
[Association(ThisKey = nameof(CustomerId), OtherKey = nameof(Customer.Id), CanBeNull = false)]
public Customer Customer { get; set; } //TODO: CustomerDto!!! - J.
[Association(ThisKey = nameof(Id), OtherKey = nameof(OrderItem.OrderId), CanBeNull = true)]
public List<TOrderItemDto> OrderItemDtos { get; set; }
[Association(ThisKey = nameof(Id), OtherKey = nameof(OrderNote.OrderId), CanBeNull = true)]
public List<OrderNote> OrderNotes { get; set; }
[ToonDescription(Purpose = "Enum wrapper", BusinessRule = "get, set => OrderStatusId")]
public OrderStatus OrderStatus
{
get => (OrderStatus)OrderStatusId;
set => OrderStatusId = (int)value;
}
[ToonDescription(Purpose = "Enum wrapper", BusinessRule = "get, set => ShippingStatusId")]
public ShippingStatus ShippingStatus
{
get => (ShippingStatus)ShippingStatusId;
set => ShippingStatusId = (int)value;
}
[ToonDescription(Purpose = "Enum wrapper", BusinessRule = "get, set => PaymentStatusId")]
public PaymentStatus PaymentStatus
{
get => (PaymentStatus)PaymentStatusId;
set => PaymentStatusId = (int)value;
}
protected MgOrderDto() :base()
{ }
protected MgOrderDto(int orderId)
{
Id = orderId;
}
protected MgOrderDto(Order order)
{
CopyEntityValuesToDto(order);
}
public virtual void CopyDtoValuesToEntity(Order entity)
{
//var config = new MapperConfiguration(cfg =>
//{
// cfg.CreateMap<MgOrderDto<Order, Order>();
// //cfg.CreateMap<Person, Person>()
// // .ForMember(dest => dest.Address, opt => opt.MapFrom(src => _mapper.Map<Address>(src.Address)));
//});
//var _mapper = config.CreateMapper();
//_mapper.Map<>
PropertyHelper.CopyPublicValueTypeProperties(this, entity);
}
public virtual void CopyEntityValuesToDto(Order entity)
{
PropertyHelper.CopyPublicValueTypeProperties(entity, this);
}
public virtual void CopyEntityValuesToDto(Order entity, List<TOrderItemDto> orderItemDtos)
{
CopyEntityValuesToDto(entity);
InitializeOrderItemDtos(orderItemDtos);
}
public virtual void InitializeOrderItemDtos(List<TOrderItemDto> orderItemDtos)
{
OrderItemDtos = orderItemDtos;
}
public virtual Order CreateMainEntity()
{
//base.CreateMainEntity();
var order = new Order();
CopyDtoValuesToEntity(order);
return order;
}
}

View File

@ -0,0 +1,79 @@
using AyCode.Core.Extensions;
using AyCode.Core.Serializers.Toons;
using AyCode.Core.Helpers;
using LinqToDB.Mapping;
using Mango.Nop.Core.Entities;
using Mango.Nop.Core.Interfaces;
using Nop.Core;
using Nop.Core.Domain.Orders;
namespace Mango.Nop.Core.Dtos;
[ToonDescription("Base DTO for order items with product association")]
public abstract class MgOrderItemDto<TProductDto> : MgEntityBase, IModelDtoBase<OrderItem>, IMgOrderItemDto<TProductDto> where TProductDto : IMgProductDto
{
public Guid OrderItemGuid { get; set; }
public int OrderId { get; set; }
public int ProductId { get; set; }
public int Quantity { get; set; }
public decimal UnitPriceInclTax { get; set; }
public decimal UnitPriceExclTax { get; set; }
public decimal PriceInclTax { get; set; }
public decimal PriceExclTax { get; set; }
public string AttributesXml { get; set; }
public decimal? ItemWeight { get; set; }
[ToonDescription(BusinessRule = "get => ProductDto?.Name ?? 'ProductDto is null!!!'")]
public string ProductName => ProductDto?.Name ?? "ProductDto is null!!!";
[Association(ThisKey = nameof(ProductId), OtherKey = nameof(BaseEntity.Id), CanBeNull = true)]
public TProductDto? ProductDto { get; set; }
protected MgOrderItemDto() :base()
{ }
protected MgOrderItemDto(int orderItemId)
{
Id = orderItemId;
}
protected MgOrderItemDto(OrderItem orderItem)
{
CopyEntityValuesToDto(orderItem);
}
public virtual void CopyDtoValuesToEntity(OrderItem entity)
{
PropertyHelper.CopyPublicValueTypeProperties(this, entity);
}
public virtual void CopyEntityValuesToDto(OrderItem entity)
{
PropertyHelper.CopyPublicValueTypeProperties(entity, this);
}
public virtual void CopyEntityValuesToDto(OrderItem entity, TProductDto productDto)
{
CopyEntityValuesToDto(entity);
InitializeProductDto(productDto);
}
public virtual void InitializeProductDto(TProductDto productDto)
{
ProductDto = productDto;
}
public virtual OrderItem CreateMainEntity()
{
//base.CreateMainEntity();
var orderItem = new OrderItem();
CopyDtoValuesToEntity(orderItem);
return orderItem;
}
}

View File

@ -0,0 +1,102 @@
using AyCode.Interfaces.Entities;
using AyCode.Core.Serializers.Toons;
using Mango.Nop.Core.Entities;
using Mango.Nop.Core.Interfaces;
//using Nop.Core.Domain.Catalog;
namespace Mango.Nop.Core.Dtos;
[ToonDescription("Base DTO for products with warehouse and pricing")]
public abstract class MgProductDto : MgEntityBase, /*Product,*/ IMgProductDto//IModelDtoBase<Product>//, IDiscountSupported<DiscountProductMapping>
{
//public int Id { get; set; }
public int ProductTypeId { get; set; }
public int ParentGroupedProductId { get; set; }
public string Name { get; set; }
public string ShortDescription { get; set; }
public string FullDescription { get; set; }
public string Sku { get; set; }
public int WarehouseId { get; set; }
public decimal Price { get; set; }
public int StockQuantity { get; set; }
public decimal ProductCost { get; set; }
public decimal Weight { get; set; }
public decimal Length { get; set; }
public decimal Width { get; set; }
public decimal Height { get; set; }
public bool Deleted { get; set; }
public bool SubjectToAcl { get; set; }
public bool LimitedToStores { get; set; }
protected MgProductDto() :base()
{ }
protected MgProductDto(int productId)
{
Id = productId;
}
//protected MgProductDto(Product product)
//{
// CopyEntityValuesToDto(product);
//}
//public virtual void CopyDtoValuesToEntity(Product entity)
//{
// entity.Id = Id;
// entity.ProductTypeId = ProductTypeId;
// entity.ParentGroupedProductId = ParentGroupedProductId;
// entity.Name = Name;
// entity.ShortDescription = ShortDescription;
// entity.FullDescription = FullDescription;
// entity.WarehouseId = WarehouseId;
// entity.StockQuantity = StockQuantity;
// entity.Weight = Weight;
// entity.Length = Length;
// entity.Width = Width;
// entity.Height = Height;
// entity.Deleted = Deleted;
//}
//public virtual void CopyEntityValuesToDto(Product entity)
//{
// Id = entity.Id;
// ProductTypeId = entity.ProductTypeId;
// ParentGroupedProductId = entity.ParentGroupedProductId;
// Name = entity.Name;
// ShortDescription = entity.ShortDescription;
// FullDescription = entity.FullDescription;
// WarehouseId = entity.WarehouseId;
// StockQuantity = entity.StockQuantity;
// Weight = entity.Weight;
// Length = entity.Length;
// Width = entity.Width;
// Height = entity.Height;
// Deleted = entity.Deleted;
//}
//public virtual Product CreateMainEntity()
//{
// //base.CreateMainEntity();
// var product = new Product();
// CopyDtoValuesToEntity(product);
// return product;
//}
}

View File

@ -0,0 +1,72 @@
using AyCode.Interfaces.Entities;
using AyCode.Core.Serializers.Toons;
using LinqToDB.Mapping;
using Mango.Nop.Core.Entities;
using Mango.Nop.Core.Interfaces;
using Nop.Core.Domain.Catalog;
using Nop.Core.Domain.Common;
using Nop.Core.Domain.Orders;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Mango.Nop.Core.Dtos
{
public interface IMgTStockQuantityHistoryDto<TProductDto> : IEntityInt, IMgStockQuantityHistory where TProductDto : IMgProductDto
{
public TProductDto ProductDto { get; set; }
}
[ToonDescription("Base DTO for stock quantity history with product")]
public abstract class MgStockQuantityHistoryDto<TProductDto> : MgEntityBase, IModelDtoBase<StockQuantityHistory>,
IMgTStockQuantityHistoryDto<TProductDto> where TProductDto : IMgProductDto
{
public int ProductId { get; set; }
public int QuantityAdjustment { get; set; }
public int StockQuantity { get; set; }
public string Message { get; set; }
public int? CombinationId { get; set; }
public int? WarehouseId { get; set; }
public DateTime CreatedOnUtc { get; set; }
[Association(ThisKey = nameof(ProductId), OtherKey = nameof(IMgProductDto.Id), CanBeNull = false)]
public TProductDto ProductDto { get; set; }
public void CopyDtoValuesToEntity(Order entity)
{
throw new NotImplementedException();
}
public void CopyDtoValuesToEntity(StockQuantityHistory entity)
{
throw new NotImplementedException();
}
public void CopyEntityValuesToDto(Order entity)
{
throw new NotImplementedException();
}
public void CopyEntityValuesToDto(StockQuantityHistory entity)
{
throw new NotImplementedException();
}
public Order CreateMainEntity()
{
throw new NotImplementedException();
}
StockQuantityHistory IModelDtoBase<StockQuantityHistory>.CreateMainEntity()
{
throw new NotImplementedException();
}
}
}

View File

@ -0,0 +1,46 @@
using AyCode.Core.Interfaces;
using Nop.Core;
namespace Mango.Nop.Core.Dtos;
public abstract class ModelDtoBase : IModelDtoBase
{
public int Id { get; set; }
protected ModelDtoBase(){}
protected ModelDtoBase(int id) => Id = id;
}
public abstract class ModelDtoBase<TMainEntity> : ModelDtoBase, IModelDtoBase<TMainEntity> where TMainEntity : BaseEntity
{
protected ModelDtoBase() : base()
{
}
protected ModelDtoBase(int id) : base(id)
{
}
protected ModelDtoBase(TMainEntity mainEntity)
{
CopyEntityValuesToDto(mainEntity);
}
public virtual TMainEntity CreateMainEntity()
{
var mainEntity = Activator.CreateInstance<TMainEntity>();
CopyDtoValuesToEntity(mainEntity);
return mainEntity;
}
public virtual void CopyDtoValuesToEntity(TMainEntity entity)
{
entity.Id = Id;
}
public virtual void CopyEntityValuesToDto(TMainEntity entity)
{
Id = entity.Id;
}
}

View File

@ -1,8 +1,14 @@
using AyCode.Interfaces.Entities;
using AyCode.Core.Serializers.Toons;
using Nop.Core;
namespace Mango.Nop.Core.Entities;
public class MgEntityBase : BaseEntity, IEntityInt
[ToonDescription("Base entity class with Id property and ToString override")]
public abstract class MgEntityBase : BaseEntity, IEntityInt
{
public override string ToString()
{
return $"{GetType().Name}; Id: {Id}";
}
}

View File

@ -0,0 +1,31 @@
using System.ComponentModel.Design;
using AyCode.Core.Serializers.Toons;
using AyCode.Interfaces.Entities;
using AyCode.Interfaces.TimeStampInfo;
using LinqToDB.Mapping;
namespace Mango.Nop.Core.Entities;
public interface IMgStockTaking : IEntityInt, ITimeStampInfo
{
public DateTime StartDateTime { get; set; }
public bool IsClosed { get; set; }
public bool IsReadyForClose();
}
[ToonDescription("Base entity for stock taking sessions with items")]
public abstract class MgStockTaking<TStockTakingItem> : MgEntityBase, IMgStockTaking where TStockTakingItem : class, IMgStockTakingItem
{
public DateTime StartDateTime { get; set; }
public bool IsClosed { get; set; }
public abstract bool IsReadyForClose();
[Association(ThisKey = nameof(Id), OtherKey = nameof(IMgStockTakingItem.StockTakingId), CanBeNull = true)]
public List<TStockTakingItem>? StockTakingItems { get; set; }
public int Creator { get; set; }
public DateTime Created { get; set; }
public DateTime Modified { get; set; }
}

View File

@ -0,0 +1,43 @@
using AyCode.Interfaces.Entities;
using AyCode.Core.Serializers.Toons;
using AyCode.Interfaces.TimeStampInfo;
using LinqToDB;
using LinqToDB.Mapping;
using Mango.Nop.Core.Dtos;
using Mango.Nop.Core.Interfaces;
namespace Mango.Nop.Core.Entities;
public interface IMgStockTakingItem : IEntityInt, ITimeStampInfo
{
public int StockTakingId { get; set; }
public int ProductId { get; set; }
public bool IsMeasured { get; set; }
public int OriginalStockQuantity { get; set; }
public int MeasuredStockQuantity { get; set; }
}
[ToonDescription("Base entity for stock taking items with product association")]
public abstract class MgStockTakingItem<TStockTaking, TProduct> : MgEntityBase, IMgStockTakingItem
where TStockTaking : class, IMgStockTaking where TProduct : class, IMgProductDto
{
public int StockTakingId { get; set; }
public int ProductId { get; set; }
public bool IsMeasured { get; set; }
public int OriginalStockQuantity { get; set; }
public int MeasuredStockQuantity { get; set; }
[Association(ThisKey = nameof(StockTakingId), OtherKey = nameof(IMgStockTaking.Id), CanBeNull = true)]
public TStockTaking? StockTaking{ get; set; }
[Association(ThisKey = nameof(ProductId), OtherKey = nameof(IMgProductDto.Id), CanBeNull = true)]
public TProduct? Product { get; set; }
public DateTime Created { get; set; }
public DateTime Modified { get; set; }
}

View File

@ -0,0 +1,27 @@
using AyCode.Core.Enums;
using AyCode.Core.Extensions;
using Nop.Core;
namespace Mango.Nop.Core.Extensions;
public static class CollectionExtensionsNopBaseEntity
{
public static void UpdateBaseEntityCollection<TDataItem>(this IList<TDataItem> source, IList<TDataItem> dataItems, bool isRemove) where TDataItem : BaseEntity
{
if (source == null) throw new ArgumentNullException(nameof(source), $"source == null");
if (dataItems == null) throw new ArgumentNullException(nameof(dataItems), $"dataItems == null");
foreach (var dataItem in dataItems)
{
source.UpdateBaseEntityCollection(dataItem, isRemove);
}
}
public static TrackingState UpdateBaseEntityCollection<TDataItem>(this IList<TDataItem> source, TDataItem dataItem, bool isRemove) where TDataItem : BaseEntity
{
if (source == null) throw new ArgumentNullException(nameof(source), $"source == null");
var index = source.FindIndex(x => x.Id == dataItem.Id);
return source.UpdateCollectionByIndex(index, dataItem, isRemove);
}
}

View File

@ -0,0 +1,53 @@
using System;
using System.Collections.Generic;
using AyCode.Utils.Extensions;
using Mango.Nop.Core.Utils;
using Nop.Core.Domain.Common;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
namespace Mango.Nop.Core.Extensions;
public static class GenericAttributeExtensions
{
public static TValue? GetValueOrNull<TValue>(this IEnumerable<GenericAttribute> src, string key) where TValue : struct
{
var ga = src.SingleOrDefault(x => x.Key == key);
if (ga == null || ga.Value.IsNullOrWhiteSpace()) return null;
return CommonHelper2.To<TValue>(ga.Value);
}
public static TValue GetValueOrDefault<TValue>(this IEnumerable<GenericAttribute> src, string key, TValue defaultValue = default) where TValue : struct
{
var gaValue = GetValueOrNull<TValue>(src, key);
return gaValue == null ? defaultValue : CommonHelper2.To<TValue>(gaValue);
}
public static bool TryGetValue<TValue>(this IEnumerable<GenericAttribute> src, string key, [NotNullWhen(true)] out TValue? value) where TValue : struct
{
value = null;
var gaValue = GetValueOrNull<TValue>(src, key);
if (gaValue == null) return false;
value = CommonHelper2.To<TValue>(gaValue);
return true;
}
public static GenericAttribute AddNewGenericAttribute(this ICollection<GenericAttribute> src, string keyGroup, string key, string value, int entityId, int storeId)
{
var ga = new GenericAttribute
{
KeyGroup = keyGroup,
Key = key,
Value = value,
EntityId = entityId,
StoreId = storeId,
CreatedOrUpdatedDateUTC = DateTime.UtcNow
};
src.Add(ga);
return ga;
}
}

View File

@ -0,0 +1,9 @@
using AyCode.Core.Interfaces;
using AyCode.Interfaces;
namespace Mango.Nop.Core.Interfaces.ForeignKeys;
public interface ICustomerForeignKey : IForeignKey
{
int CustomerId { get; set; }
}

View File

@ -0,0 +1,14 @@
using System.Collections.Generic;
using AyCode.Core.Interfaces;
using Nop.Core.Domain.Common;
namespace Mango.Nop.Core.Interfaces.ForeignKeys;
public interface IGenericAttributeForeignList : IGenericAttributeForeignCollection<List<GenericAttribute>>
{
}
public interface IGenericAttributeForeignCollection<T> : IForeignCollection<T> where T : IEnumerable<GenericAttribute>
{
T GenericAttributes { get; }
}

View File

@ -1,6 +0,0 @@
namespace Mango.Nop.Core.Interfaces;
public interface IMgDbContextBase //: IAcDbContextBase
{
}

View File

@ -0,0 +1,41 @@
using AyCode.Interfaces.Entities;
using Mango.Nop.Core.Dtos;
using Nop.Core.Domain.Common;
using Nop.Core.Domain.Customers;
using Nop.Core.Domain.Orders;
using Nop.Core.Domain.Shipping;
namespace Mango.Nop.Core.Interfaces;
public interface IMgOrderDto<TOrderItemDto, TProductDto> : IEntityInt, ISoftDeletedEntity where TOrderItemDto : IMgOrderItemDto<TProductDto> where TProductDto : IMgProductDto
{
public Guid OrderGuid { get; set; }
public int StoreId { get; set; }
public int CustomerId { get; set; }
public int OrderStatusId { get; set; }
public OrderStatus OrderStatus { get; set; }
public int ShippingStatusId { get; set; }
public ShippingStatus ShippingStatus { get; set; }
public decimal OrderDiscount { get; set; }
public decimal OrderTotal { get; set; }
public DateTime CreatedOnUtc { get; set; }
public DateTime? PaidDateUtc { get; set; }
public string ShippingMethod { get; set; }
public string CustomOrderNumber { get; set; }
public string CustomValuesXml { get; set; }
public Customer Customer { get; } //TODO: CustomerDto!!! - J.
public List<TOrderItemDto> OrderItemDtos { get; }
public List<OrderNote> OrderNotes { get; }
void InitializeOrderItemDtos(List<TOrderItemDto> orderItemDtos);
void CopyEntityValuesToDto(Order entity, List<TOrderItemDto> orderItemDtos);
}

View File

@ -0,0 +1,25 @@
using AyCode.Interfaces.Entities;
using Nop.Core.Domain.Orders;
namespace Mango.Nop.Core.Interfaces;
public interface IMgOrderItemDto<TProductDto> : IEntityInt where TProductDto : IMgProductDto
{
public Guid OrderItemGuid { get; set; }
public int OrderId { get; set; }
public int ProductId { get; set; }
public int Quantity { get; set; }
public string AttributesXml { get; set; }
public decimal? ItemWeight { get; set; }
public string ProductName { get; }
public TProductDto? ProductDto { get; set; }
void InitializeProductDto(TProductDto productDto);
void CopyEntityValuesToDto(OrderItem entity, TProductDto productDto);
}

View File

@ -0,0 +1,29 @@
using AyCode.Interfaces.Entities;
using Nop.Core.Domain.Common;
using Nop.Core.Domain.Localization;
using Nop.Core.Domain.Security;
using Nop.Core.Domain.Seo;
using Nop.Core.Domain.Stores;
namespace Mango.Nop.Core.Interfaces;
public interface IMgProductDto : IEntityInt, ILocalizedEntity, ISlugSupported, IAclSupported, IStoreMappingSupported/*, IDiscountSupported<DiscountProductMapping>*/, ISoftDeletedEntity
{
int ProductTypeId { get; set; }
int ParentGroupedProductId { get; set; }
string Name { get; set; }
string ShortDescription { get; set; }
string FullDescription { get; set; }
string Sku { get; set; }
int WarehouseId { get; set; }
decimal Price { get; set; }
int StockQuantity { get; set; }
decimal ProductCost { get; set; }
decimal Weight { get; set; }
decimal Length { get; set; }
decimal Width { get; set; }
decimal Height { get; set; }
}

View File

@ -0,0 +1,12 @@
using AyCode.Core.Loggers;
namespace Mango.Nop.Core.Loggers;
public interface ILogger<TCategory> : ILogger
{
}
public interface ILogger : IAcLoggerBase
{
}

View File

@ -0,0 +1,31 @@
using AyCode.Core.Enums;
using AyCode.Core.Loggers;
namespace Mango.Nop.Core.Loggers;
public class Logger<TCategory> : Logger, ILogger<TCategory>
{
//public Logger() : base(typeof(TCategory).Name)
//{ }
public Logger(params IAcLogWriterBase[] logWriters) : base(typeof(TCategory).Name, logWriters)
{ }
public Logger(AppType appType, LogLevel logLevel, params IAcLogWriterBase[] logWriters) : base(appType, logLevel, typeof(TCategory).Name, logWriters)
{ }
}
public class Logger : AcLoggerBase, ILogger
{
public Logger(params IAcLogWriterBase[] logWriters) : this(null, logWriters)
{ }
public Logger(string? categoryName) : base(categoryName)
{ }
public Logger(string? categoryName, params IAcLogWriterBase[] logWriters) : base(categoryName, logWriters)
{ }
public Logger(AppType appType, LogLevel logLevel, string? categoryName, params IAcLogWriterBase[] logWriters) : base(appType, logLevel, categoryName, logWriters)
{ }
}

View File

@ -1,44 +1,60 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<BaseOutputPath>bin\FruitBank</BaseOutputPath>
<GeneratePackageOnBuild>False</GeneratePackageOnBuild>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<CopyRefAssembliesToPublishDirectory>true</CopyRefAssembliesToPublishDirectory>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MessagePack.Annotations" Version="2.5.192" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.10" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="linq2db" Version="5.4.1" />
<PackageReference Include="MessagePack" Version="3.1.4" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="9.0.11" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\NopCommerce\Libraries\Nop.Core\Nop.Core.csproj" />
<ProjectReference Include="..\..\..\..\NopCommerce\Libraries\Nop.Data\Nop.Data.csproj" />
<ProjectReference Include="..\..\..\..\NopCommerce\Libraries\Nop.Services\Nop.Services.csproj" />
</ItemGroup>
<ItemGroup>
<Reference Include="AyCode.Core">
<HintPath>..\..\..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Core.dll</HintPath>
</Reference>
<Reference Include="AyCode.Core.Server">
<HintPath>..\..\..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Core.Server.dll</HintPath>
</Reference>
<Reference Include="AyCode.Entities">
<HintPath>..\..\..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Entities.dll</HintPath>
</Reference>
<Reference Include="AyCode.Entities.Server">
<HintPath>..\..\..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Entities.Server.dll</HintPath>
</Reference>
<Reference Include="AyCode.Interfaces">
<HintPath>..\..\..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Interfaces.dll</HintPath>
</Reference>
<Reference Include="AyCode.Interfaces.Server">
<HintPath>..\..\..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Interfaces.Server.dll</HintPath>
</Reference>
<Reference Include="AyCode.Utils">
<HintPath>..\..\..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Utils.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<Reference Include="AyCode.Core">
<HintPath>..\..\..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\Debug\net8.0\AyCode.Core.dll</HintPath>
</Reference>
<Reference Include="AyCode.Core.Server">
<HintPath>..\..\..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\Debug\net8.0\AyCode.Core.Server.dll</HintPath>
</Reference>
<Reference Include="AyCode.Entities">
<HintPath>..\..\..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\Debug\net8.0\AyCode.Entities.dll</HintPath>
</Reference>
<Reference Include="AyCode.Entities.Server">
<HintPath>..\..\..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\Debug\net8.0\AyCode.Entities.Server.dll</HintPath>
</Reference>
<Reference Include="AyCode.Interfaces">
<HintPath>..\..\..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\Debug\net8.0\AyCode.Interfaces.dll</HintPath>
</Reference>
<Reference Include="AyCode.Interfaces.Server">
<HintPath>..\..\..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\Debug\net8.0\AyCode.Interfaces.Server.dll</HintPath>
</Reference>
<Reference Include="AyCode.Utils">
<HintPath>..\..\..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\Debug\net8.0\AyCode.Utils.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Core.Serializers.SourceGenerator\AyCode.Core.Serializers.SourceGenerator.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
</ItemGroup>
<ItemGroup>
<Folder Include="ExtendedFactories\" />
<Folder Include="ExtendedModels\" />
</ItemGroup>
<ItemGroup>
<None Include="docs\**\*.md" />
<None Include="**\README.md" Exclude="$(DefaultItemExcludes);docs\**" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,17 @@
using AyCode.Interfaces;
using Mango.Nop.Core.Interfaces;
namespace Mango.Nop.Core.Models;
public class MgLoginModelRequest : IAcModelDtoBaseEmpty
{
public MgLoginModelRequest(){}
public MgLoginModelRequest(string email, string password)
{
Email = email;
Password = password;
}
public string Email { get; set; }
public string Password { get; set; }
}

View File

@ -0,0 +1,24 @@
using AyCode.Interfaces;
using AyCode.Utils.Extensions;
using Mango.Nop.Core.Dtos;
using Nop.Core.Domain.Customers;
namespace Mango.Nop.Core.Models;
public class MgLoginModelResponse : IAcModelDtoBaseEmpty
{
public CustomerDto? CustomerDto { get; set; }
public string ErrorMessage { get; set; }
public MgLoginModelResponse()
{ }
public MgLoginModelResponse(Customer customer, string errorMessage) : this(new CustomerDto(customer), errorMessage)
{ }
public MgLoginModelResponse(CustomerDto customerDto, string errorMessage)
{
CustomerDto = customerDto;
ErrorMessage = errorMessage;
}
public bool IsSuccesLogin => CustomerDto != null && ErrorMessage.IsNullOrWhiteSpace();
}

View File

@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using AyCode.Core.Consts;
namespace Mango.Nop.Core
{
public abstract class NopCommonConst: AcConst
{
//public static string DefaultLocale = "en-US";
//public static string BaseUrl = "https://localhost:7144";
//public static string LoggerHubName = "loggerHub";
}
}

View File

@ -1,4 +1,4 @@
namespace Nop.Core.Domain.Catalog;
namespace Nop.Core.Domain.Catalog;
/// <summary>
/// Represents a backorder mode

View File

@ -0,0 +1,28 @@
//namespace Nop.Core.Domain.Common;
using AyCode.Core.Serializers.Toons;
using AyCode.Interfaces.Entities;
namespace Nop.Core;
/// <summary>
/// Represents the base class for entities
/// </summary>
public abstract partial class BaseEntity : IBaseEntity
{
/// <summary>
/// Gets or sets the entity identifier
/// </summary>
public int Id { get; set; }
public override string ToString()
{
return $"{GetType().Name} [Id: {Id}]";
}
}
public interface IBaseEntity //: IEntityInt
{
public int Id { get; set; }
}

View File

@ -1,4 +1,6 @@
using Nop.Core.Domain.Common;
using AyCode.Core.Serializers.Attributes;
using AyCode.Core.Serializers.Toons;
using Nop.Core.Domain.Common;
using Nop.Core.Domain.Tax;
namespace Nop.Core.Domain.Customers;
@ -6,6 +8,8 @@ namespace Nop.Core.Domain.Customers;
/// <summary>
/// Represents a customer
/// </summary>
[AcBinarySerializable(false, true, false, true, false, false)]
[ToonDescription("NopCommerce customer entity")]
public partial class Customer : BaseEntity, ISoftDeletedEntity
{
public Customer()
@ -21,21 +25,25 @@ public partial class Customer : BaseEntity, ISoftDeletedEntity
/// <summary>
/// Gets or sets the username
/// </summary>
//[ToonIgnore]
public string Username { get; set; }
/// <summary>
/// Gets or sets the email
/// </summary>
//[ToonIgnore]
public string Email { get; set; }
/// <summary>
/// Gets or sets the first name
/// </summary>
//[ToonIgnore]
public string FirstName { get; set; }
/// <summary>
/// Gets or sets the last name
/// </summary>
//[ToonIgnore]
public string LastName { get; set; }
/// <summary>
@ -233,6 +241,11 @@ public partial class Customer : BaseEntity, ISoftDeletedEntity
/// </summary>
public int? BillingAddressId { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the customer is required to change password
/// </summary>
public bool MustChangePassword { get; set; }
/// <summary>
/// Gets or sets the shipping address identifier
/// </summary>

View File

@ -1,8 +1,11 @@
namespace Nop.Core.Domain.Customers;
using AyCode.Core.Serializers.Attributes;
namespace Nop.Core.Domain.Customers;
/// <summary>
/// Represents a customer role
/// </summary>
[AcBinarySerializable(false, true, false, true, false, false)]
public partial class CustomerRole : BaseEntity
{
/// <summary>

View File

@ -1,8 +1,12 @@
namespace Nop.Core.Domain.Discounts;
using AyCode.Core.Serializers.Attributes;
namespace Nop.Core.Domain.Discounts;
/// <summary>
/// Represents a discount-product mapping class
/// </summary>
// SGen incompatible: DiscountMapping.Id is readonly (new int Id { get; }) — generated reader cannot set it (CS0200)
//[AcBinarySerializable(false, true, false, true, false)]
public partial class DiscountProductMapping : DiscountMapping
{
/// <summary>

View File

@ -1,13 +1,20 @@
namespace Nop.Core.Domain.Common;
using AyCode.Core.Serializers.Attributes;
using AyCode.Core.Serializers.Toons;
namespace Nop.Core.Domain.Common;
/// <summary>
/// Represents a generic attribute
/// </summary>
[AcBinarySerializable(false, true, false, true, false, false)]
[ToonDescription("NopCommerce generic attribute for key-value storage", Purpose = "A flexible key-value store used to extend entities with custom business logic data without changing the database schema")]
public partial class GenericAttribute : BaseEntity
{
/// <summary>
/// Gets or sets the entity identifier
/// </summary>
///
[ToonDescription(Constraints = "polymorphic-fk(KeyGroup)")]
public int EntityId { get; set; }
/// <summary>
@ -23,6 +30,7 @@ public partial class GenericAttribute : BaseEntity
/// <summary>
/// Gets or sets the value
/// </summary>
[ToonDescription(Purpose = "Raw string representation of the Key's value")]
public string Value { get; set; }
/// <summary>

View File

@ -1,4 +1,6 @@
using Nop.Core.Domain.Common;
using AyCode.Core.Serializers.Attributes;
using AyCode.Core.Serializers.Toons;
using Nop.Core.Domain.Common;
using Nop.Core.Domain.Payments;
using Nop.Core.Domain.Shipping;
using Nop.Core.Domain.Tax;
@ -8,6 +10,8 @@ namespace Nop.Core.Domain.Orders;
/// <summary>
/// Represents an order
/// </summary>
[AcBinarySerializable(false, true, false, true, false, false)]
[ToonDescription("NopCommerce order entity with payment and shipping")]
public partial class Order : BaseEntity, ISoftDeletedEntity
{
#region Properties
@ -299,6 +303,7 @@ public partial class Order : BaseEntity, ISoftDeletedEntity
/// <summary>
/// Gets or sets the order status
/// </summary>
[ToonDescription(Purpose = "Enum wrapper", BusinessRule = "get, set => OrderStatusId")]
public OrderStatus OrderStatus
{
get => (OrderStatus)OrderStatusId;
@ -308,6 +313,7 @@ public partial class Order : BaseEntity, ISoftDeletedEntity
/// <summary>
/// Gets or sets the payment status
/// </summary>
[ToonDescription(Purpose = "Enum wrapper", BusinessRule = "get, set => PaymentStatusId")]
public PaymentStatus PaymentStatus
{
get => (PaymentStatus)PaymentStatusId;
@ -317,6 +323,7 @@ public partial class Order : BaseEntity, ISoftDeletedEntity
/// <summary>
/// Gets or sets the shipping status
/// </summary>
[ToonDescription(Purpose = "Enum wrapper", BusinessRule = "get, set => ShippingStatusId")]
public ShippingStatus ShippingStatus
{
get => (ShippingStatus)ShippingStatusId;
@ -326,6 +333,7 @@ public partial class Order : BaseEntity, ISoftDeletedEntity
/// <summary>
/// Gets or sets the customer tax display type
/// </summary>
[ToonDescription(Purpose = "Enum wrapper", BusinessRule = "get, set => CustomerTaxDisplayTypeId")]
public TaxDisplayType CustomerTaxDisplayType
{
get => (TaxDisplayType)CustomerTaxDisplayTypeId;

View File

@ -1,8 +1,14 @@
namespace Nop.Core.Domain.Orders;
using AyCode.Core.Serializers.Attributes;
using AyCode.Core.Serializers.Toons;
/// <summary>
/// Represents an order item
/// </summary>
[AcBinarySerializable(false, true, false, true, false, false)]
[ToonDescription("NopCommerce order item entity")]
public partial class OrderItem : BaseEntity
{
/// <summary>

View File

@ -1,13 +1,19 @@
namespace Nop.Core.Domain.Orders;
using AyCode.Core.Serializers.Attributes;
using AyCode.Core.Serializers.Toons;
namespace Nop.Core.Domain.Orders;
/// <summary>
/// Represents an order note
/// </summary>
[AcBinarySerializable(false, true, false, true, false, false)]
[ToonDescription("NopCommerce order note entity")]
public partial class OrderNote : BaseEntity
{
/// <summary>
/// Gets or sets the order identifier
/// </summary>
[ToonDescription($"Foreign key to parent {nameof(Order)}", ForeignKey = nameof(Order))]
public int OrderId { get; set; }
/// <summary>

View File

@ -1,4 +1,6 @@
using Nop.Core.Domain.Common;
using AyCode.Core.Serializers.Attributes;
using AyCode.Core.Serializers.Toons;
using Nop.Core.Domain.Common;
using Nop.Core.Domain.Discounts;
using Nop.Core.Domain.Localization;
using Nop.Core.Domain.Security;
@ -10,23 +12,26 @@ namespace Nop.Core.Domain.Catalog;
/// <summary>
/// Represents a product
/// </summary>
[AcBinarySerializable(false, true, false, true, false, false)]
[ToonDescription("Core nopCommerce product entity with catalog, pricing, and inventory management")]
public partial class Product : BaseEntity, ILocalizedEntity, ISlugSupported, IAclSupported, IStoreMappingSupported, IDiscountSupported<DiscountProductMapping>, ISoftDeletedEntity
{
/// <summary>
/// Gets or sets the product type identifier
/// </summary>
[ToonDescription(Purpose = "Defines if the product is Simple or Grouped", BusinessRule = "Enum: ProductType")]
public int ProductTypeId { get; set; }
/// <summary>
/// Gets or sets the parent product identifier. It's used to identify associated products (only with "grouped" products)
/// </summary>
[ToonDescription(Purpose = "Hierarchical link for grouped product variants", BusinessRule = "Foreign Key to Product.Id")]
public int ParentGroupedProductId { get; set; }
/// <summary>
/// Gets or sets the values indicating whether this product is visible in catalog or search results.
/// It's used when this product is associated to some "grouped" one
/// This way associated products could be accessed/added/etc only from a grouped product details page
/// </summary>
[ToonDescription(Purpose = "Controls independent visibility in search/catalog", BusinessRule = "Used primarily for associated products")]
public bool VisibleIndividually { get; set; }
/// <summary>
@ -47,6 +52,7 @@ public partial class Product : BaseEntity, ILocalizedEntity, ISlugSupported, IAc
/// <summary>
/// Gets or sets the admin comment
/// </summary>
[ToonDescription(Purpose = "Internal administrative notes", BusinessRule = "Never exposed to customer")]
public string AdminComment { get; set; }
/// <summary>
@ -117,6 +123,7 @@ public partial class Product : BaseEntity, ILocalizedEntity, ISlugSupported, IAc
/// <summary>
/// Gets or sets the SKU
/// </summary>
[ToonDescription(Purpose = "Stock Keeping Unit", BusinessRule = "Unique alphanumeric identifier")]
public string Sku { get; set; }
/// <summary>
@ -125,7 +132,7 @@ public partial class Product : BaseEntity, ILocalizedEntity, ISlugSupported, IAc
public string ManufacturerPartNumber { get; set; }
/// <summary>
/// Gets or sets the Global Trade Item Number (GTIN). These identifiers include UPC (in North America), EAN (in Europe), JAN (in Japan), and ISBN (for books).
/// Gets or sets the Global Trade Item Number (GTIN).
/// </summary>
public string Gtin { get; set; }
@ -140,12 +147,12 @@ public partial class Product : BaseEntity, ILocalizedEntity, ISlugSupported, IAc
public int GiftCardTypeId { get; set; }
/// <summary>
/// Gets or sets gift card amount that can be used after purchase. If not specified, then product price will be used.
/// Gets or sets gift card amount that can be used after purchase.
/// </summary>
public decimal? OverriddenGiftCardAmount { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the product requires that other products are added to the cart (Product X requires Product Y)
/// Gets or sets a value indicating whether the product requires that other products are added to the cart
/// </summary>
public bool RequireOtherProducts { get; set; }
@ -162,6 +169,7 @@ public partial class Product : BaseEntity, ILocalizedEntity, ISlugSupported, IAc
/// <summary>
/// Gets or sets a value indicating whether the product is download
/// </summary>
[ToonDescription(Purpose = "Digital good flag")]
public bool IsDownload { get; set; }
/// <summary>
@ -212,6 +220,7 @@ public partial class Product : BaseEntity, ILocalizedEntity, ISlugSupported, IAc
/// <summary>
/// Gets or sets a value indicating whether the product is recurring
/// </summary>
[ToonDescription(Purpose = "Subscription flag", BusinessRule = "Triggers automated billing cycles")]
public bool IsRecurring { get; set; }
/// <summary>
@ -232,6 +241,7 @@ public partial class Product : BaseEntity, ILocalizedEntity, ISlugSupported, IAc
/// <summary>
/// Gets or sets a value indicating whether the product is rental
/// </summary>
[ToonDescription(Purpose = "Rental flag", BusinessRule = "Requires date selection in UI")]
public bool IsRental { get; set; }
/// <summary>
@ -247,6 +257,7 @@ public partial class Product : BaseEntity, ILocalizedEntity, ISlugSupported, IAc
/// <summary>
/// Gets or sets a value indicating whether the entity is ship enabled
/// </summary>
[ToonDescription(Purpose = "Physical shipping toggle")]
public bool IsShipEnabled { get; set; }
/// <summary>
@ -277,11 +288,12 @@ public partial class Product : BaseEntity, ILocalizedEntity, ISlugSupported, IAc
/// <summary>
/// Gets or sets the tax category identifier
/// </summary>
public int TaxCategoryId { get; set; }
public int TaxCategoryId { get; set; }
/// <summary>
/// Gets or sets a value indicating how to manage inventory
/// </summary>
[ToonDescription(Purpose = "Inventory logic selector", BusinessRule = "0:None, 1:Product, 2:Attributes")]
public int ManageInventoryMethodId { get; set; }
/// <summary>
@ -302,6 +314,7 @@ public partial class Product : BaseEntity, ILocalizedEntity, ISlugSupported, IAc
/// <summary>
/// Gets or sets the stock quantity
/// </summary>
[ToonDescription(Purpose = "Physical quantity in warehouse", BusinessRule = "Decremented on order placement or paid")]
public int StockQuantity { get; set; }
/// <summary>
@ -332,6 +345,7 @@ public partial class Product : BaseEntity, ILocalizedEntity, ISlugSupported, IAc
/// <summary>
/// Gets or sets a value backorder mode identifier
/// </summary>
[ToonDescription(Purpose = "Handling of out-of-stock purchases", BusinessRule = "Controls if StockQuantity can go negative")]
public int BackorderModeId { get; set; }
/// <summary>
@ -356,7 +370,6 @@ public partial class Product : BaseEntity, ILocalizedEntity, ISlugSupported, IAc
/// <summary>
/// Gets or sets a value indicating whether we allow adding to the cart/wishlist only attribute combinations that exist and have stock greater than zero.
/// This option is used only when we have "manage inventory" set to "track inventory by product attributes"
/// </summary>
public bool AllowAddingOnlyExistingAttributeCombinations { get; set; }
@ -366,7 +379,7 @@ public partial class Product : BaseEntity, ILocalizedEntity, ISlugSupported, IAc
public bool DisplayAttributeCombinationImagesOnly { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this product is returnable (a customer is allowed to submit return request with this product)
/// Gets or sets a value indicating whether this product is returnable
/// </summary>
public bool NotReturnable { get; set; }
@ -398,6 +411,7 @@ public partial class Product : BaseEntity, ILocalizedEntity, ISlugSupported, IAc
/// <summary>
/// Gets or sets the price
/// </summary>
[ToonDescription(Purpose = "Base selling price", BusinessRule = "Used as primary value before discounts")]
public decimal Price { get; set; }
/// <summary>
@ -408,6 +422,7 @@ public partial class Product : BaseEntity, ILocalizedEntity, ISlugSupported, IAc
/// <summary>
/// Gets or sets the product cost
/// </summary>
[ToonDescription(Purpose = "Purchase price from vendor", BusinessRule = "Used for profit/margin calculation")]
public decimal ProductCost { get; set; }
/// <summary>
@ -426,7 +441,7 @@ public partial class Product : BaseEntity, ILocalizedEntity, ISlugSupported, IAc
public decimal MaximumCustomerEnteredPrice { get; set; }
/// <summary>
/// Gets or sets a value indicating whether base price (PAngV) is enabled. Used by German users.
/// Gets or sets a value indicating whether base price (PAngV) is enabled.
/// </summary>
public bool BasepriceEnabled { get; set; }
@ -456,33 +471,15 @@ public partial class Product : BaseEntity, ILocalizedEntity, ISlugSupported, IAc
public bool MarkAsNew { get; set; }
/// <summary>
/// Gets or sets the start date and time of the new product (set product as "New" from date). Leave empty to ignore this property
/// Gets or sets the start date and time of the new product
/// </summary>
public DateTime? MarkAsNewStartDateTimeUtc { get; set; }
/// <summary>
/// Gets or sets the end date and time of the new product (set product as "New" to date). Leave empty to ignore this property
/// Gets or sets the end date and time of the new product
/// </summary>
public DateTime? MarkAsNewEndDateTimeUtc { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this product has tier prices configured
/// <remarks>The same as if we run TierPrices.Count > 0
/// We use this property for performance optimization:
/// if this property is set to false, then we do not need to load tier prices navigation property
/// </remarks>
/// </summary>
public bool HasTierPrices { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this product has discounts applied
/// <remarks>The same as if we run AppliedDiscounts.Count > 0
/// We use this property for performance optimization:
/// if this property is set to false, then we do not need to load Applied Discounts navigation property
/// </remarks>
/// </summary>
public bool HasDiscountsApplied { get; set; }
/// <summary>
/// Gets or sets the weight
/// </summary>
@ -515,8 +512,6 @@ public partial class Product : BaseEntity, ILocalizedEntity, ISlugSupported, IAc
/// <summary>
/// Gets or sets a display order.
/// This value is used when sorting associated products (used with "grouped" products)
/// This value is used when sorting home page products
/// </summary>
public int DisplayOrder { get; set; }
@ -528,6 +523,7 @@ public partial class Product : BaseEntity, ILocalizedEntity, ISlugSupported, IAc
/// <summary>
/// Gets or sets a value indicating whether the entity has been deleted
/// </summary>
[ToonDescription(Purpose = "Soft-delete flag", BusinessRule = "Records remain in DB but hidden from UI")]
public bool Deleted { get; set; }
/// <summary>
@ -543,6 +539,7 @@ public partial class Product : BaseEntity, ILocalizedEntity, ISlugSupported, IAc
/// <summary>
/// Gets or sets the product type
/// </summary>
[ToonDescription(Purpose = "Enum wrapper", BusinessRule = "get, set => ProductTypeId")]
public ProductType ProductType
{
get => (ProductType)ProductTypeId;
@ -552,6 +549,7 @@ public partial class Product : BaseEntity, ILocalizedEntity, ISlugSupported, IAc
/// <summary>
/// Gets or sets the backorder mode
/// </summary>
[ToonDescription(Purpose = "Enum wrapper", BusinessRule = "get, set => BackorderModeId")]
public BackorderMode BackorderMode
{
get => (BackorderMode)BackorderModeId;
@ -561,6 +559,7 @@ public partial class Product : BaseEntity, ILocalizedEntity, ISlugSupported, IAc
/// <summary>
/// Gets or sets the download activation type
/// </summary>
[ToonDescription(Purpose = "Enum wrapper", BusinessRule = "get, set => DownloadActivationTypeId")]
public DownloadActivationType DownloadActivationType
{
get => (DownloadActivationType)DownloadActivationTypeId;
@ -570,6 +569,7 @@ public partial class Product : BaseEntity, ILocalizedEntity, ISlugSupported, IAc
/// <summary>
/// Gets or sets the gift card type
/// </summary>
[ToonDescription(Purpose = "Enum wrapper", BusinessRule = "get, set => GiftCardTypeId")]
public GiftCardType GiftCardType
{
get => (GiftCardType)GiftCardTypeId;
@ -579,6 +579,7 @@ public partial class Product : BaseEntity, ILocalizedEntity, ISlugSupported, IAc
/// <summary>
/// Gets or sets the low stock activity
/// </summary>
[ToonDescription(Purpose = "Enum wrapper", BusinessRule = "get, set => LowStockActivityId")]
public LowStockActivity LowStockActivity
{
get => (LowStockActivity)LowStockActivityId;
@ -588,6 +589,7 @@ public partial class Product : BaseEntity, ILocalizedEntity, ISlugSupported, IAc
/// <summary>
/// Gets or sets the value indicating how to manage inventory
/// </summary>
[ToonDescription(Purpose = "Enum wrapper", BusinessRule = "get, set => ManageInventoryMethodId")]
public ManageInventoryMethod ManageInventoryMethod
{
get => (ManageInventoryMethod)ManageInventoryMethodId;
@ -597,6 +599,7 @@ public partial class Product : BaseEntity, ILocalizedEntity, ISlugSupported, IAc
/// <summary>
/// Gets or sets the cycle period for recurring products
/// </summary>
[ToonDescription(Purpose = "Enum wrapper", BusinessRule = "get, set => RecurringCyclePeriodId")]
public RecurringProductCyclePeriod RecurringCyclePeriod
{
get => (RecurringProductCyclePeriod)RecurringCyclePeriodId;
@ -606,6 +609,7 @@ public partial class Product : BaseEntity, ILocalizedEntity, ISlugSupported, IAc
/// <summary>
/// Gets or sets the period for rental products
/// </summary>
[ToonDescription(Purpose = "Enum wrapper", BusinessRule = "get, set => RentalPricePeriod")]
public RentalPricePeriod RentalPricePeriod
{
get => (RentalPricePeriod)RentalPricePeriodId;

View File

@ -1,9 +1,29 @@
namespace Nop.Core.Domain.Catalog;
using AyCode.Core.Serializers.Attributes;
using AyCode.Core.Serializers.Toons;
using AyCode.Interfaces.Entities;
using LinqToDB.Mapping;
using Mango.Nop.Core.Interfaces;
namespace Nop.Core.Domain.Catalog;
public interface IMgStockQuantityHistory
{
public int ProductId { get; set; }
public int QuantityAdjustment { get; set; }
public int StockQuantity { get; set; }
public string Message { get; set; }
public int? CombinationId { get; set; }
public int? WarehouseId { get; set; }
public DateTime CreatedOnUtc { get; set; }
}
/// <summary>
/// Represents a stock quantity change entry
/// </summary>
public partial class StockQuantityHistory : BaseEntity
[AcBinarySerializable(false, true, false, true, false, false)]
[Table(Name = nameof(StockQuantityHistory))]
[ToonDescription("NopCommerce stock movement log", Purpose = "Audit trail for physical and logical stock movements")]
public partial class StockQuantityHistory : BaseEntity, IMgStockQuantityHistory
{
/// <summary>
/// Gets or sets the stock quantity adjustment

View File

@ -1,4 +1,4 @@
namespace Nop.Core.Domain.Catalog;
namespace Nop.Core.Domain.Catalog;
/// <summary>
/// Represents a download activation type

View File

@ -1,4 +1,4 @@
namespace Nop.Core.Domain.Catalog;
namespace Nop.Core.Domain.Catalog;
/// <summary>
/// Represents a gift card type

View File

@ -1,5 +1,5 @@
namespace Nop.Core.Domain.Common;
//namespace Mango.Nop.Core.NopDependencies;
namespace Nop.Core.Domain.Common;
/// <summary>
/// Represents a soft-deleted (without actually deleting from storage) entity
/// </summary>

View File

@ -1,4 +1,4 @@
namespace Nop.Core.Domain.Catalog;
namespace Nop.Core.Domain.Catalog;
/// <summary>
/// Represents a low stock activity

View File

@ -1,4 +1,4 @@
namespace Nop.Core.Domain.Catalog;
namespace Nop.Core.Domain.Catalog;
/// <summary>
/// Represents a method of inventory management

View File

@ -1,4 +1,4 @@
namespace Nop.Core.Domain.Catalog;
namespace Nop.Core.Domain.Catalog;
/// <summary>
/// Represents a product type

View File

@ -1,4 +1,4 @@
namespace Nop.Core.Domain.Catalog;
namespace Nop.Core.Domain.Catalog;
/// <summary>
/// Represents a recurring product cycle period

View File

@ -1,4 +1,4 @@
namespace Nop.Core.Domain.Catalog;
namespace Nop.Core.Domain.Catalog;
/// <summary>
/// Represents a rental product period (for prices)

105
Mango.Nop.Core/README.md Normal file
View File

@ -0,0 +1,105 @@
# Mango.Nop.Core
@project {
type = "framework"
own-dep-projects = [
"AyCode.Core, AyCode.Core.Server, AyCode.Entities, AyCode.Entities.Server, AyCode.Interfaces, AyCode.Interfaces.Server, AyCode.Utils (in AyCode.Core repo)"
]
}
Shared domain library containing entities, DTOs, interfaces, and nopCommerce entity mirrors. **net9.0**. Zero nopCommerce runtime dependency.
## Documentation
| Document | Topic |
|---|---|
| `DTOS.md` | DTO system — two mapping strategies, all DTO types, GenericAttribute typed access |
| `NOP_DEPENDENCIES.md` | NopDependencies pattern — mirror copies, namespace rules, file list |
## Folder Structure
| Folder | Purpose |
|---|---|
| `Dtos/` | DTO classes shared across consumers |
| `Entities/` | Custom domain entities |
| `Extensions/` | Extension methods for `BaseEntity` collections and `GenericAttribute` |
| `Interfaces/` | DTO interfaces, soft-delete, foreign key markers |
| `Loggers/` | `ILogger` / `Logger` — logging abstraction wrapping AyCode logger |
| `Models/` | Login request/response models |
| `NopDependencies/` | Mirror copies of nopCommerce entity classes (same namespaces as originals) |
| `Services/` | `IMgLockService` interface |
| `Utils/` | `CommonHelper2` — email validation, type conversion utilities |
## Entity Hierarchy
```
Nop.Core.BaseEntity (NopDependencies/, implements IBaseEntity)
+-- MgEntityBase (Entities/, implements IEntityInt from AyCode)
+-- MgOrderDto<TOrderItemDto, TProductDto>
+-- MgOrderItemDto<TProductDto>
+-- MgProductDto
+-- MgStockQuantityHistoryDto<TProductDto>
+-- MgStockTaking<TStockTakingItem>
+-- MgStockTakingItem<TStockTaking, TProduct>
```
## Key Types (not in docs/)
### Entities
| Type | Key features |
|---|---|
| `MgEntityBase` | Inherits `BaseEntity`, implements `IEntityInt`. `ToString()` -> `"{TypeName}; Id: {Id}"` |
| `MgStockTaking<TStockTakingItem>` | `StartDateTime`, `IsClosed`, `List<TStockTakingItem>` navigation. Implements `ITimeStampInfo` |
| `MgStockTakingItem<TStockTaking, TProduct>` | `StockTakingId`, `ProductId`, `IsMeasured`, stock quantities. Navigations to parent and product |
### Extensions
| Method | Purpose |
|---|---|
| `UpdateBaseEntityCollection` | Add/update/remove entities in a list by Id match |
### Loggers
Extends AyCode logging infrastructure — for base types see `AyCode.Core/AyCode.Core/docs/LOGGING/README.md`.
| Type | Inherits | Purpose |
|---|---|---|
| `ILogger` | `IAcLoggerBase` | Mango-level logger interface |
| `ILogger<TCategory>` | `ILogger` | Generic category logger interface |
| `Logger` | `AcLoggerBase` | Logger implementation with `IAcLogWriterBase[]` writers |
| `Logger<TCategory>` | `Logger` | Typed logger — category name from `typeof(TCategory).Name` |
### Models
| Type | Purpose |
|---|---|
| `MgLoginModelRequest` | `Email` + `Password` for SignalR/API login |
| `MgLoginModelResponse` | `CustomerDto?` + `ErrorMessage`. Computed `IsSuccesLogin` |
### Other
| Type | Purpose |
|---|---|
| `NopCommonConst` | Abstract const class inheriting `AcConst` (placeholder) |
| `CommonHelper2` | Email validation, IP validation, type conversion (`To<T>`), string utilities |
## Dependencies
- `linq2db` — data access, `[Association]` / `[Table]` attributes
- `Microsoft.AspNetCore.Mvc.NewtonsoftJson` — JSON serialization
- Binary serialization: `AcBinarySerializer` (see `AyCode.Core/AyCode.Core/docs/BINARY/BINARY_FORMAT.md`). SignalR transport: `AcSignalR` (see `AyCode.Core/AyCode.Services/docs/SIGNALR/README.md`).
- `AyCode.Core.Serializers.SourceGenerator` (ProjectReference, `OutputItemType="Analyzer"`) — compile-time binary serializer. See [`AyCode.Core/docs/BINARY_SOURCE_GEN.md`](../../../../../../Aycode/Source/AyCode.Core/AyCode.Core/docs/BINARY_SOURCE_GEN.md)
- AyCode DLL references: `AyCode.Interfaces`, `AyCode.Core`, `AyCode.Utils`, `AyCode.Entities` (types only, no runtime nopCommerce dependency)
## AcBinary Source Generator (SGen)
9 types annotated with `[AcBinarySerializable]`. For general SGen docs see [`AyCode.Core/docs/BINARY_SOURCE_GEN.md`](../../../../../../Aycode/Source/AyCode.Core/AyCode.Core/docs/BINARY_SOURCE_GEN.md).
Requires `InternalsVisibleTo("Mango.Nop.Core")` in `AyCode.Core/Properties/AssemblyInfo.cs`.
### Excluded types
| Type | Reason |
|---|---|
| `DiscountProductMapping` | Base `DiscountMapping` hides `BaseEntity.Id` with readonly `new int Id { get; }` → CS0200. Uses compiled-expression fallback. |

View File

@ -1,29 +0,0 @@
using Mango.Nop.Core.Interfaces;
using Microsoft.AspNetCore.Http;
using Nop.Core.Caching;
using Nop.Core.Domain.Catalog;
using Nop.Core.Events;
using Nop.Data;
using Nop.Services.Events;
using Nop.Services.Logging;
namespace Mango.Nop.Core.Repositories;
public class MgDbContextBase : IMgDbContextBase
{
//TODO: ez itt nem ay igazi, kitalálni vmit! - J.
private readonly CacheKey _auctionAllKey = new("Nop.auction.all-{0}", AUCTION_PATTERN_KEY);
public const string AUCTION_PATTERN_KEY = "Nop.auction.";
protected ILogger Logger;
protected INopDataProvider DataProvider;
//protected IHttpContextAccessor HttpContextAccessor;
public MgDbContextBase(INopDataProvider dataProvider, ILogger logger)
{
Logger = logger;
DataProvider = dataProvider;
}
}

View File

@ -1,4 +1,4 @@
namespace Mango.Nop.Services;
namespace Mango.Nop.Core.Services;
public interface IMgLockService
{

View File

@ -2,14 +2,13 @@
using System.Globalization;
using System.Net;
using System.Text.RegularExpressions;
using Nop.Core.Infrastructure;
namespace Nop.Core;
namespace Mango.Nop.Core.Utils;
/// <summary>
/// Represents a common helper
/// </summary>
public partial class CommonHelper
public partial class CommonHelper2
{
#region Fields
@ -40,7 +39,7 @@ public partial class CommonHelper
if (!IsValidEmail(output))
{
throw new NopException("Email is not valid.");
throw new Exception("Email is not valid.");
}
return output;
@ -71,31 +70,31 @@ public partial class CommonHelper
return IPAddress.TryParse(ipAddress, out var _);
}
/// <summary>
/// Generate random digit code
/// </summary>
/// <param name="length">Length</param>
/// <returns>Result string</returns>
public static string GenerateRandomDigitCode(int length)
{
using var random = new SecureRandomNumberGenerator();
var str = string.Empty;
for (var i = 0; i < length; i++)
str = string.Concat(str, random.Next(10).ToString());
return str;
}
///// <summary>
///// Generate random digit code
///// </summary>
///// <param name="length">Length</param>
///// <returns>Result string</returns>
//public static string GenerateRandomDigitCode(int length)
//{
// using var random = new SecureRandomNumberGenerator();
// var str = string.Empty;
// for (var i = 0; i < length; i++)
// str = string.Concat(str, random.Next(10).ToString());
// return str;
//}
/// <summary>
/// Returns an random integer number within a specified rage
/// </summary>
/// <param name="min">Minimum number</param>
/// <param name="max">Maximum number</param>
/// <returns>Result</returns>
public static int GenerateRandomInteger(int min = 0, int max = int.MaxValue)
{
using var random = new SecureRandomNumberGenerator();
return random.Next(min, max);
}
///// <summary>
///// Returns an random integer number within a specified rage
///// </summary>
///// <param name="min">Minimum number</param>
///// <param name="max">Maximum number</param>
///// <returns>Result</returns>
//public static int GenerateRandomInteger(int min = 0, int max = int.MaxValue)
//{
// using var random = new SecureRandomNumberGenerator();
// return random.Next(min, max);
//}
/// <summary>
/// Ensure that a string doesn't exceed maximum allowed length
@ -189,10 +188,10 @@ public partial class CommonHelper
var instanceType = instance.GetType();
var pi = instanceType.GetProperty(propertyName)
?? throw new NopException("No property '{0}' found on the instance of type '{1}'.", propertyName, instanceType);
?? throw new Exception($"No property '{propertyName}' found on the instance of type '{instanceType}'.");
if (!pi.CanWrite)
throw new NopException("The property '{0}' on the instance of type '{1}' does not have a setter.", propertyName, instanceType);
throw new Exception($"No property '{propertyName}' found on the instance of type '{instanceType}'.");
if (value != null && !value.GetType().IsAssignableFrom(pi.PropertyType))
value = To(value, pi.PropertyType);
pi.SetValue(instance, value, Array.Empty<object>());
@ -253,23 +252,22 @@ public partial class CommonHelper
}
/// <summary>
/// Convert enum for front-end
/// Splits the camel-case word into separate one
/// </summary>
/// <param name="str">Input string</param>
/// <returns>Converted string</returns>
public static string ConvertEnum(string str)
/// <returns>Splitted string</returns>
public static string SplitCamelCaseWord(string str)
{
if (string.IsNullOrEmpty(str))
return string.Empty;
var result = string.Empty;
foreach (var c in str)
if (c.ToString() != c.ToString().ToLowerInvariant())
result += " " + c.ToString();
else
result += c.ToString();
var result = str.ToCharArray()
.Select(p => p.ToString())
.Aggregate(string.Empty, (current, c) => current + (c == c.ToUpperInvariant() ? $" {c}" : c));
//ensure no spaces (e.g. when the first letter is upper case)
result = result.TrimStart();
return result;
}
@ -311,13 +309,4 @@ public partial class CommonHelper
}
#endregion
#region Properties
/// <summary>
/// Gets or sets the default file provider
/// </summary>
public static INopFileProvider DefaultFileProvider { get; set; }
#endregion
}

View File

@ -0,0 +1,44 @@
# Architecture
## Framework vs. Consumer Boundary
This solution is **Layer 2 — Domain framework** (NopCommerce-plugin base). Built on Layer 0 (AyCode.Core), consumed by Layer 3 (specific plugin apps, plural and unknown). Full doctrine: AyCode.Core's `docs/ARCHITECTURE.md#framework-vs-consumer-boundary`.
### Layer position
```
Layer 0 — AyCode.Core universal primitives (serializers, logging, entities)
Layer 2 — Mango.Nop.Core this solution — NopCommerce-plugin domain framework
Layer 3 — Consumer plugin apps specific plugin projects with business logic
```
### Nop-plugin-framework-specific notes
- **`NopDependencies/`** mirrors NopCommerce entity classes under the original Nop namespaces — no runtime Nop dependency, compile-time type parity only
- **DTOs** use type parameters for consumer entity types (e.g. `MgOrderDto<TOrderItemDto, TProductDto>`) — concrete types are provided by plugin projects
- **Entity hierarchy**: `Nop.Core.BaseEntity` (mirrored) → `MgEntityBase` → generic DTOs/entities parameterized for consumers
- **`Logger` / `ILogger`** extend AyCode logging (`AcLoggerBase`) — plugin projects may subclass again if business-specific writers are needed
- **Zero runtime Nop dependency** — plugin apps bring their own Nop runtime
### What belongs here vs. in a plugin
**Yes, framework:**
- NopCommerce entity mirrors (for cross-plugin type sharing)
- Generic DTOs with type parameters
- Shared interfaces and extensions for NopCommerce domain entities
- Logger base with AyCode integration
**No, plugin only:**
- Plugin-specific business logic
- Plugin-named types (`XxxPlugin`, `XxxService` where Xxx is a specific product)
- Hardcoded plugin configuration, tenant, or product IDs
## Related Documents
| Topic | Document |
|---|---|
| Framework-first doctrine (full) | `../../../../../../../Aycode/Source/AyCode.Core/docs/ARCHITECTURE.md#framework-vs-consumer-boundary` |
| Placement rules, anti-patterns | `CONVENTIONS.md#framework-first-placement` |
| DTO system and GenericAttribute typed access | `DTOS.md` |
| NopDependencies mirror pattern | `NOP_DEPENDENCIES.md` |
| Folder structure and key types index | `../README.md` |

View File

@ -0,0 +1,28 @@
# Conventions
For core framework conventions (Ac prefix, naming patterns, Session/Transaction, etc.) see AyCode.Core's `docs/CONVENTIONS.md`.
## Framework-First Placement
Follow the doctrine in AyCode.Core's `docs/CONVENTIONS.md#framework-first-placement`. Same verdict table and hard rules apply.
**Nop-plugin-framework-specific additions:**
- `NopDependencies/` mirror classes stay in their original Nop namespaces — don't rename, don't add plugin-specific fields
- DTOs parameterize via type arguments, not concrete plugin types
- Any plugin-specific business logic belongs in the plugin project, not here
- Adding a new shared DTO: ensure it is useful for 2+ plugin projects (otherwise keep in the single plugin)
## Naming
- **`Mg` prefix**: for Mango-framework-level types (e.g. `MgEntityBase`, `MgOrderDto`, `MgProductDto`)
- **`IMg` prefix**: for Mango-framework-level interfaces (e.g. `IMgModelDtoBase`, `IMgSoftRemoveEntity`)
- **No prefix**: for NopCommerce entity mirrors in `NopDependencies/` — they stay in Nop namespace, same name as upstream
## XML Documentation
`<summary>` — brief, developer-facing, readable in VS IntelliSense tooltip. NO implementation details, NO wire-format / byte-level / perf specifics — those live in `docs/TOPIC/*.md`. Add `<example>` only when usage is non-obvious; otherwise omit.
## See Also
- Core framework naming (`Ac` / `IAc` prefixes), patterns, serialization conventions: AyCode.Core's `docs/CONVENTIONS.md`
- Framework-first verdict table (full): AyCode.Core's `docs/CONVENTIONS.md#framework-first-placement`

View File

@ -0,0 +1,74 @@
# DTO System
> Part of `Mango.Nop.Core`. See `Mango.Nop.Core/README.md` for project overview.
## Two Mapping Strategies
### Strategy 1: `ModelDtoBase<T>` (simple DTOs)
Used by: `CustomerDto`, `MgGenericAttributeDto`
```
ModelDtoBase → ModelDtoBase<Customer> → CustomerDto
```
- Manual `CopyEntityValuesToDto`/`CopyDtoValuesToEntity` overrides
- `CreateMainEntity()` uses `Activator.CreateInstance<T>()`
- No LinqToDB associations
### Strategy 2: `MgEntityBase` + `IModelDtoBase<T>` (complex DTOs)
Used by: `MgOrderDto`, `MgOrderItemDto`, `MgStockQuantityHistoryDto`
```
BaseEntity → MgEntityBase → MgOrderDto<TOrderItemDto, TProductDto> : IModelDtoBase<Order>
```
- Uses `PropertyHelper.CopyPublicValueTypeProperties()` for bulk value-type copy
- LinqToDB `[Association]` navigation properties
- Generic type parameters for child DTOs
### Strategy 3: Entity inheritance (MgProductDto)
```
BaseEntity → MgEntityBase → MgProductDto : IMgProductDto
```
- No `IModelDtoBase<Product>` (entity mapping methods are commented out)
- Direct property declarations mirroring `Product` fields
## DTO Types
| Type | Generic params | Maps to entity | Key features |
|---|---|---|---|
| `ModelDtoBase` | — | — | Abstract base, `int Id` only |
| `ModelDtoBase<TMainEntity>` | `TMainEntity : BaseEntity` | `TMainEntity` | `CreateMainEntity()`, `CopyEntityValuesToDto()`, `CopyDtoValuesToEntity()` |
| `MgOrderDto<TOrderItemDto, TProductDto>` | `TOrderItemDto : IMgOrderItemDto<TProductDto>`, `TProductDto : IMgProductDto` | `Order` | Has `Customer`, `List<TOrderItemDto>`, `List<OrderNote>` navigations. Enum wrappers: `OrderStatus`, `ShippingStatus`, `PaymentStatus` |
| `MgOrderItemDto<TProductDto>` | `TProductDto : IMgProductDto` | `OrderItem` | Has `TProductDto?` navigation. Computed `ProductName` property |
| `MgProductDto` | — | *(commented out)* | Base product DTO. Name, Price, StockQuantity, dimensions. No entity mapping methods (currently commented out) |
| `MgGenericAttributeDto` | — | `GenericAttribute` | Inherits `GenericAttribute` directly (not `ModelDtoBase`). Full bidirectional mapping |
| `MgStockQuantityHistoryDto<TProductDto>` | `TProductDto : IMgProductDto` | `StockQuantityHistory` | Has `TProductDto` navigation. Mapping methods `NotImplementedException` (stub) |
| `CustomerDto` | — | `Customer` | `Username`, `Email`, `FirstName`, `LastName`, `FullName` computed. Uses `[AcBinarySerializable]` and LinqToDB `[Table]` |
## DTO Interfaces
| Interface | Extends | Purpose |
|---|---|---|
| `IModelDtoBase` | `IEntityInt`, `IModelDtoBaseEmpty` | Marker for all DTOs with `int Id` |
| `IModelDtoBase<TMainEntity>` | `IModelDtoBase` | Bidirectional mapping contract: `CreateMainEntity()`, `CopyDtoValuesToEntity()`, `CopyEntityValuesToDto()` |
| `IMgOrderDto<TOrderItemDto, TProductDto>` | `IEntityInt`, `ISoftDeletedEntity` | Order DTO contract with navigation properties and initialization methods |
| `IMgOrderItemDto<TProductDto>` | `IEntityInt` | Order item DTO contract with product navigation |
| `IMgProductDto` | `IEntityInt`, `ILocalizedEntity`, `ISlugSupported`, `IAclSupported`, `IStoreMappingSupported`, `ISoftDeletedEntity` | Product DTO contract |
## GenericAttribute Typed Access
`GenericAttributeExtensions` provides typed access to nopCommerce's polymorphic key-value store:
| Method | Signature | Purpose |
|---|---|---|
| `GetValueOrNull<TValue>` | `(IEnumerable<GenericAttribute>, string key) -> TValue?` | Get typed value, returns null if not found |
| `GetValueOrDefault<TValue>` | `(IEnumerable<GenericAttribute>, string key, TValue default) -> TValue` | Same with default fallback |
| `TryGetValue<TValue>` | `(IEnumerable<GenericAttribute>, string key, out TValue?) -> bool` | Try-pattern for GA value |
| `AddNewGenericAttribute` | `(ICollection<GenericAttribute>, ...) -> GenericAttribute` | Create and add new GA with UTC timestamp |
**Rule:** Always use these extension methods — never parse `GenericAttribute.Value` strings manually.

View File

@ -0,0 +1,46 @@
# NopDependencies Pattern
> Part of `Mango.Nop.Core`. See `Mango.Nop.Core/README.md` for project overview.
## Why
`Mango.Nop.Core` has **zero nopCommerce runtime dependency**. This allows it to be referenced by projects that don't have the full nopCommerce stack (e.g. Blazor/MAUI clients that only need DTOs and interfaces).
To achieve this, `NopDependencies/` contains **mirror copies** of nopCommerce entity classes with the **same namespace** as the originals:
```csharp
// In NopDependencies/BaseEntity.cs — same namespace as nopCommerce
namespace Nop.Core;
public abstract partial class BaseEntity : IBaseEntity
{
public int Id { get; set; }
}
```
At compile time, projects referencing only `Mango.Nop.Core` get these mirror types. Projects with the full nopCommerce stack get the real types — they are type-compatible because they share the same namespace and shape.
## Rules
- **DO NOT modify** files in `NopDependencies/` unless the nopCommerce version changes
- **DO NOT change namespaces** — they must match the original nopCommerce types exactly
- All mirror classes are `partial` — they can be extended but should not be modified directly
## File List
| File | Namespace | Type |
|---|---|---|
| `BaseEntity.cs` | `Nop.Core` | `BaseEntity` (abstract, `IBaseEntity`) + `IBaseEntity` interface |
| `Catalogs/Customer.cs` | `Nop.Core.Domain.Customers` | `Customer` |
| `Catalogs/CustomerRole.cs` | `Nop.Core.Domain.Customers` | `CustomerRole` |
| `Catalogs/Order.cs` | `Nop.Core.Domain.Orders` | `Order` |
| `Catalogs/OrderItem.cs` | `Nop.Core.Domain.Orders` | `OrderItem` |
| `Catalogs/OrderNote.cs` | `Nop.Core.Domain.Orders` | `OrderNote` |
| `Catalogs/Product.cs` | `Nop.Core.Domain.Catalog` | `Product` |
| `Catalogs/GenericAttribute.cs` | `Nop.Core.Domain.Common` | `GenericAttribute` |
| `Catalogs/StockQuantityHistory.cs` | `Nop.Core.Domain.Catalog` | `StockQuantityHistory` |
| `Catalogs/DiscountMapping.cs` | `Nop.Core.Domain.Catalog` | `DiscountMapping` |
| `Catalogs/DiscountProductMapping.cs` | `Nop.Core.Domain.Catalog` | `DiscountProductMapping` |
**Enums:** `OrderStatus`, `PaymentStatus`, `ShippingStatus`, `ProductType`, `ManageInventoryMethod`, `BackorderMode`, `LowStockActivity`, `GiftCardType`, `RentalPricePeriod`, `RecurringProductCyclePeriod`, `DownloadActivationType`, `TaxDisplayType`, `VatNumberStatus`
**Interfaces:** `ISoftDeletedEntity`, `ILocalizedEntity`, `ISlugSupported`, `IAclSupported`, `IStoreMappingSupported`, `IDiscountSupported`

View File

@ -0,0 +1,19 @@
# Mango.Nop.Core documentation
Topic documentation for the `Mango.Nop.Core` project (Layer 2 framework — NopCommerce-adjacent base types).
## Reference docs (flat)
- [`ARCHITECTURE.md`](ARCHITECTURE.md) — Project architecture
- [`CONVENTIONS.md`](CONVENTIONS.md) — Project-specific conventions
- [`DTOS.md`](DTOS.md) — DTO system, mapping strategies
- [`NOP_DEPENDENCIES.md`](NOP_DEPENDENCIES.md) — NopCommerce entity mirror pattern
## Navigation
Per the AI Agent Core Protocol (folder navigation rule), start from this README when browsing `docs/`. All docs at this level are single-file reference; topic folders (e.g., LOGGING/) exist only at sub-projects that host a variant (see `../Mango.Nop.Services/docs/LOGGING/`).
## See also
- **Repo-level conventions**: `../../docs/CONVENTIONS.md`
- **Base framework**: `../../../../../Aycode/Source/AyCode.Core/` docs

View File

@ -1,4 +1,4 @@
namespace Mango.Nop.Core.Interfaces;
namespace Mango.Nop.Data.Interfaces;
public interface IMgDalBase //: IAcDalBase//: IDisposable
{

View File

@ -0,0 +1,22 @@
using Mango.Nop.Core.Loggers;
using Nop.Core.Domain.Catalog;
using Nop.Core.Domain.Orders;
using Nop.Data;
using System.Transactions;
namespace Mango.Nop.Data.Interfaces;
public interface IMgDbContextBase //: IAcDbContextBase
{
ILogger Logger { get; init; }
INopDataProvider DataProvider { get; init; }
IRepository<Order> Orders { get; set; }
IRepository<Product> Products { get; set; }
bool Transaction(Func<TransactionScope, bool> callbackTransactionBody, bool throwException = false);
bool TransactionSafe(Func<TransactionScope, bool> callbackTransactionBody, bool throwException = false);
Task<bool> TransactionAsync(Func<TransactionScope, Task<bool>> callbackTransactionBody, bool throwException = false);
Task<bool> TransactionSafeAsync(Func<TransactionScope, Task<bool>> callbackTransactionBody, bool throwException = false);
}

View File

@ -1,7 +1,7 @@
using Mango.Nop.Core.Entities;
using Nop.Data;
namespace Mango.Nop.Core.Interfaces;
namespace Mango.Nop.Data.Interfaces;
public interface IMgDbTableBase //: IAcDbTableBase
{

View File

@ -0,0 +1,53 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<BaseOutputPath>bin\FruitBank</BaseOutputPath>
<GeneratePackageOnBuild>False</GeneratePackageOnBuild>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<CopyRefAssembliesToPublishDirectory>true</CopyRefAssembliesToPublishDirectory>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MessagePack" Version="3.1.4" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="9.0.11" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\FruitBank\Libraries\Nop.Core\Nop.Core.csproj" />
<ProjectReference Include="..\..\..\..\FruitBank\Libraries\Nop.Data\Nop.Data.csproj" />
<ProjectReference Include="..\Mango.Nop.Core\Mango.Nop.Core.csproj" />
</ItemGroup>
<ItemGroup>
<Reference Include="AyCode.Core">
<HintPath>..\..\..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Core.dll</HintPath>
</Reference>
<Reference Include="AyCode.Core.Server">
<HintPath>..\..\..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Core.Server.dll</HintPath>
</Reference>
<Reference Include="AyCode.Entities">
<HintPath>..\..\..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Entities.dll</HintPath>
</Reference>
<Reference Include="AyCode.Entities.Server">
<HintPath>..\..\..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Entities.Server.dll</HintPath>
</Reference>
<Reference Include="AyCode.Interfaces">
<HintPath>..\..\..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Interfaces.dll</HintPath>
</Reference>
<Reference Include="AyCode.Interfaces.Server">
<HintPath>..\..\..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Interfaces.Server.dll</HintPath>
</Reference>
<Reference Include="AyCode.Utils">
<HintPath>..\..\..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Utils.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<None Include="docs\**\*.md" />
<None Include="**\README.md" Exclude="$(DefaultItemExcludes);docs\**" />
</ItemGroup>
</Project>

60
Mango.Nop.Data/README.md Normal file
View File

@ -0,0 +1,60 @@
# Mango.Nop.Data
@project {
type = "framework"
own-dep-projects = [
"AyCode.Core, AyCode.Core.Server, AyCode.Entities, AyCode.Entities.Server, AyCode.Interfaces, AyCode.Interfaces.Server, AyCode.Utils (in AyCode.Core repo)"
]
}
Data access layer with repository base classes and DB context abstractions. **net9.0**.
## Documentation
| Document | Topic |
|---|---|
| `REPOSITORIES.md` | MgDbTableBase, MgDtoDbTableBase — CRUD hooks, timestamps, delete rules, event bridging |
| `TRANSACTIONS.md` | MgDbContextBase — 4 transaction methods, lock strategy, callback contract |
## Folder Structure
| Folder | Purpose |
|---|---|
| `Interfaces/` | Repository interfaces: `IMgDalBase`, `IMgDbContextBase`, `IMgDbTableBase` |
| `Repositories/` | Repository base implementations: `MgDalBase`, `MgDbContextBase`, `MgDbTableBase`, `MgDtoDbTableBase` |
## Inheritance Chains
### Repository hierarchy
```
nopCommerce EntityRepository<TEntity> (Nop.Data)
+-- MgDbTableBase<TEntity>
+-- MgDtoDbTableBase<TDtoEntity, TMainEntity>
```
### Context hierarchy
```
MgDbContextBase (abstract, implements IMgDbContextBase)
+-- [Consumer DbContexts in plugins]
```
### DAL hierarchy
```
MgDalBase<TDbContext> (implements IMgDalBase<TDbContext>)
+-- [Consumer DALs in plugins]
```
## Interfaces
| Interface | Purpose |
|---|---|
| `IMgDbTableBase` | Marker interface for repository classes |
| `IMgDbContextBase` | Contract: `Logger`, `DataProvider`, `Orders`, `Products`, 4 transaction methods |
| `IMgDalBase` | `Name` property |
| `IMgDalBase<TDbContext>` | `Context`, `MutextLock` — typed access to DB context |
## Dependencies
- `Mango.Nop.Core` (ProjectReference)
- `Nop.Core`, `Nop.Data` (nopCommerce ProjectReferences)
- `Microsoft.AspNetCore.Mvc.NewtonsoftJson`

View File

@ -1,16 +1,14 @@
using System.Linq.Expressions;
using System.Transactions;
using Mango.Nop.Core.Interfaces;
using Mango.Nop.Data.Interfaces;
using Nop.Core;
using Nop.Core.Caching;
using Nop.Core.Configuration;
using Nop.Core.Events;
using Nop.Data;
using Nop.Data.DataProviders;
namespace Mango.Nop.Core.Repositories;
namespace Mango.Nop.Data.Repositories;
public class MgDalBase<TDbContext> : IMgDalBase<TDbContext> where TDbContext : IMgDbContextBase
public abstract class MgDalBase<TDbContext> : IMgDalBase<TDbContext> where TDbContext : IMgDbContextBase
{
public string Name { get; }
public TDbContext Context { get; }

View File

@ -0,0 +1,156 @@
using System.Transactions;
using AyCode.Core.Consts;
using AyCode.Core.Helpers;
using AyCode.Core.Loggers;
using AyCode.Utils.Extensions;
using Mango.Nop.Core.Loggers;
using Mango.Nop.Core.Services;
using Mango.Nop.Data.Interfaces;
using Nop.Core.Caching;
using Nop.Core.Domain.Catalog;
using Nop.Core.Domain.Orders;
using Nop.Data;
namespace Mango.Nop.Data.Repositories;
public abstract class MgDbContextBase : IMgDbContextBase
{
//TODO: ez itt nem ay igazi, kitalálni vmit! - J.
private readonly CacheKey _auctionAllKey = new("Nop.auction.all-{0}", AUCTION_PATTERN_KEY);
public const string AUCTION_PATTERN_KEY = "Nop.auction.";
protected readonly IMgLockService LockService;
public ILogger Logger { get; init; }
public INopDataProvider DataProvider { get; init; }
public IRepository<Order> Orders { get; set; }
public IRepository<OrderItem> OrderItems { get; set; }
public IRepository<Product> Products { get; set; }
//public IHttpContextAccessor HttpContextAccessor { get; init; }
public MgDbContextBase(IRepository<Product> productRepository, IRepository<Order> orderRepository, IRepository<OrderItem> orderItemRepository, INopDataProvider dataProvider, IMgLockService lockService, ILogger logger)
{
LockService = lockService;
Logger = logger;// new Logger<MgDbContextBase>(logWriters.ToArray());
DataProvider = dataProvider;
Products = productRepository;
Orders = orderRepository;
OrderItems = orderItemRepository;
}
private static TransactionScope CreateTransactionScope(TransactionScopeOption transactionScopeOption = TransactionScopeOption.Required)
{
//TransactionManager.ImplicitDistributedTransactions = true;
var transactionOptions = new TransactionOptions
{
IsolationLevel = IsolationLevel.ReadCommitted,
Timeout = TransactionManager.DefaultTimeout
};
return new TransactionScope(transactionScopeOption, transactionOptions, TransactionScopeAsyncFlowOption.Enabled);
//return new TransactionScope(TransactionScopeOption.Required, TransactionScopeAsyncFlowOption.Enabled);
}
public bool Transaction(Func<TransactionScope, bool> callbackTransactionBody, bool throwException = false)
=> TransactionInner(callbackTransactionBody, throwException);
/// <summary>
/// Using LoskService, global lock!
/// </summary>
/// <param name="callbackTransactionBody"></param>
/// <param name="throwException"></param>
/// <returns></returns>
public bool TransactionSafe(Func<TransactionScope, bool> callbackTransactionBody, bool throwException = false)
{
using (LockService.SemaphoreSlim.UseWait())
{
return TransactionInner(callbackTransactionBody, throwException);
}
}
public Task<bool> TransactionAsync(Func<TransactionScope, Task<bool>> callbackTransactionBody, bool throwException = false)
=> TaskHelper.ToThreadPoolTask(() => TransactionInnerAsync(callbackTransactionBody, throwException));
/// <summary>
/// Using LoskService, global lock!
/// </summary>
/// <param name="callbackTransactionBody"></param>
/// <param name="throwException"></param>
/// <returns></returns>
public Task<bool> TransactionSafeAsync(Func<TransactionScope, Task<bool>> callbackTransactionBody, bool throwException = false)
{
return TaskHelper.ToThreadPoolTask(async () =>
{
using (await LockService.SemaphoreSlim.UseWaitAsync())
{
return await TransactionInnerAsync(callbackTransactionBody, throwException);
}
});
}
private async Task<bool> TransactionInnerAsync(Func<TransactionScope, Task<bool>> callbackTransactionBody, bool throwException = false)
{
bool result;
try
{
using (var transaction = CreateTransactionScope())// new TransactionScope( /*TransactionScopeOption.RequiresNew, */TransactionScopeAsyncFlowOption.Enabled))
{
result = await callbackTransactionBody(transaction);
if (result) transaction.Complete();
}
if (!result) Logger.Warning($"TransactionInnerAsync({this}) transaction ROLLBACK!");
}
catch (Exception ex)
{
if (throwException) throw;
result = false;
Logger.Error($"TransactionInnerAsync({this}) transaction ROLLBACK! ex: {ex.Message}{AcEnv.NL}", ex);
}
return result;
}
private bool TransactionInner(Func<TransactionScope, bool> callbackTransactionBody, bool throwException = false)
{
bool result;
try
{
using(var transaction = CreateTransactionScope()) //new TransactionScope( /*TransactionScopeOption.RequiresNew, */TransactionScopeAsyncFlowOption.Enabled)
{
result = callbackTransactionBody(transaction);
if (result) transaction.Complete();
}
if (!result) Logger.Warning($"TransactionInner({this}) transaction ROLLBACK!");
}
catch (Exception ex)
{
if (throwException) throw;
result = false;
Logger.Error($"TransactionInnerAsync({this}) transaction error! ex: {ex.Message}{AcEnv.NL}", ex);
}
return result;
}
public override string ToString()
{
return GetType().Name;
}
}

View File

@ -1,31 +1,35 @@
using System.Linq.Expressions;
using AyCode.Interfaces.Entities;
using AyCode.Interfaces.Entities;
using AyCode.Interfaces.TimeStampInfo;
using Mango.Nop.Core.Interfaces;
using Mango.Nop.Core.Loggers;
using Mango.Nop.Data.Interfaces;
using Nop.Core;
using Nop.Core.Caching;
using Nop.Core.Configuration;
using Nop.Core.Domain.Catalog;
using Nop.Core.Domain.Common;
using Nop.Core.Events;
using Nop.Data;
using Nop.Services.Logging;
using System.Linq.Expressions;
namespace Mango.Nop.Core.Repositories;
namespace Mango.Nop.Data.Repositories;
public class MgDbTableBase<TEntity>(IEventPublisher eventPublisher, INopDataProvider dataProvider, IShortTermCacheManager shortTermCacheManager, IStaticCacheManager staticCacheManager, AppSettings appSettings, ILogger logger)
public abstract class MgDbTableBase<TEntity>(IEventPublisher eventPublisher, INopDataProvider dataProvider, IShortTermCacheManager shortTermCacheManager, IStaticCacheManager staticCacheManager, AppSettings appSettings)
: EntityRepository<TEntity>(eventPublisher, dataProvider, shortTermCacheManager, staticCacheManager, appSettings), IMgDbTableBase where TEntity : BaseEntity
{
protected ILogger Logger = logger;
//protected ILogger Logger = logger;
protected IEventPublisher EventPublisher = eventPublisher;
protected INopDataProvider DataProvider = dataProvider;
protected IShortTermCacheManager ShortTermCacheManager = shortTermCacheManager;
public virtual IQueryable<TEntity> GetAll() => Table;
#region SetTimeStampInfos
private static void SetTimeStampCreated(TEntity entity)
{
if (typeof(TEntity).GetInterface(nameof(ITimeStampCreated)) != null)
{
((entity as ITimeStampCreated)!).Created = DateTime.UtcNow;
(entity as ITimeStampCreated)!.Created = DateTime.UtcNow;
}
}
@ -33,12 +37,16 @@ public class MgDbTableBase<TEntity>(IEventPublisher eventPublisher, INopDataProv
{
if (typeof(TEntity).GetInterface(nameof(ITimeStampModified)) != null)
{
((entity as ITimeStampModified)!).Modified = DateTime.UtcNow;
(entity as ITimeStampModified)!.Modified = DateTime.UtcNow;
}
}
#endregion SetTimeStampInfos
//TODO: Transaction-be tenni az event-eket! - J.
#region OnCrudEvents
private void OnInsert(IList<TEntity> entities)
{
foreach (var entity in entities) OnInsert(entity);
@ -61,7 +69,7 @@ public class MgDbTableBase<TEntity>(IEventPublisher eventPublisher, INopDataProv
{
SetTimeStampModified(entity);
_staticCacheManager.ClearAsync().GetAwaiter().GetResult();
_staticCacheManager.ClearAsync().GetAwaiter().GetResult(); //TODO: EZ MI??? - J.
//_staticCacheManager.RemoveByPrefix(MgDbContextBase.AUCTION_PATTERN_KEY);
}
@ -79,6 +87,7 @@ public class MgDbTableBase<TEntity>(IEventPublisher eventPublisher, INopDataProv
//_staticCacheManager.RemoveByPrefix(MgDbContextBase.AUCTION_PATTERN_KEY);
}
#endregion OnCrudEvents
#region Insert
@ -106,9 +115,11 @@ public class MgDbTableBase<TEntity>(IEventPublisher eventPublisher, INopDataProv
OnInsert(entities);
return base.InsertAsync(entities, publishEvent);
}
#endregion Insert
#region Update
public override void Update(TEntity entity, bool publishEvent = true)
{
OnUpdate(entity);
@ -132,29 +143,25 @@ public class MgDbTableBase<TEntity>(IEventPublisher eventPublisher, INopDataProv
OnUpdate(entities);
return base.UpdateAsync(entities, publishEvent);
}
#endregion Update
#region Delete
protected override Task DeleteAsync(IList<TEntity> entities)
{
OnDelete(entities);
return base.DeleteAsync(entities);
}
protected override Task DeleteAsync<T>(IList<T> entities)
{
if(typeof(T) == typeof(TEntity)) OnDelete(entities.Cast<TEntity>().ToList());
return base.DeleteAsync(entities);
}
protected override IQueryable<TEntity> AddDeletedFilter(IQueryable<TEntity> query, in bool includeDeleted)
{
foreach (var entity in query) OnDelete(entity);
//EZ NEM DELETE METHOD! A GET-EKNÉL HASZNÁLJA A ISFOTDELETE FILTER-HEZ! - J.
//foreach (var entity in query) OnDelete(entity);
return base.AddDeletedFilter(query, in includeDeleted);
}
public virtual async Task DeleteAsync(int entityId, bool publishEvent = true)
{
var entity = await GetByIdAsync(entityId);
await DeleteAsync(entity, publishEvent);
}
public override Task DeleteAsync(TEntity entity, bool publishEvent = true)
{
OnDelete(entity);
@ -167,22 +174,46 @@ public class MgDbTableBase<TEntity>(IEventPublisher eventPublisher, INopDataProv
return base.DeleteAsync(entities, publishEvent);
}
/// <summary>
/// It does not call the PublishEvent() and OnDelete() functions!
/// </summary>
/// <param name="predicate"></param>
/// <returns></returns>
public override Task<int> DeleteAsync(Expression<Func<TEntity, bool>> predicate)
{
//TODO:ide mit kéne?! - J.
return base.DeleteAsync(predicate);
}
public virtual async Task DeleteAsync(Expression<Func<TEntity, bool>> predicate, bool publishEvent)
{
if (publishEvent)
{
var deletingEntities = await Table.Where(predicate).ToListAsync();
await base.DeleteAsync(deletingEntities, true);
return;
}
await DeleteAsync(predicate);
}
public override void Delete(TEntity entity, bool publishEvent = true)
{
OnDelete(entity);
base.Delete(entity, publishEvent);
}
public override void Delete(IList<TEntity> entities, bool publishEvent = true)
{
OnDelete(entities);
base.Delete(entities, publishEvent);
}
public override int Delete(Expression<Func<TEntity, bool>> predicate)
{
//TODO:ide mit kéne?! - J.
return base.Delete(predicate);
}
#endregion Delete
}

View File

@ -0,0 +1,77 @@
using System.Linq.Expressions;
using LinqToDB;
using Mango.Nop.Core.Loggers;
using Nop.Core;
using Nop.Core.Caching;
using Nop.Core.Configuration;
using Nop.Core.Domain.Orders;
using Nop.Core.Events;
using Nop.Data;
namespace Mango.Nop.Data.Repositories;
public abstract class MgDtoDbTableBase<TDtoEntity, TMainEntity> : MgDbTableBase<TDtoEntity> where TDtoEntity : BaseEntity/*, IMgModelDtoBase<TDtoEntity>*/ where TMainEntity : BaseEntity
{
public Type MainEntityType { get; } = typeof(Order);
public MgDtoDbTableBase(IEventPublisher eventPublisher, INopDataProvider dataProvider, IShortTermCacheManager shortTermCacheManager, IStaticCacheManager staticCacheManager, AppSettings appSettings)
: base(eventPublisher, dataProvider, shortTermCacheManager, staticCacheManager, appSettings)
{
}
public async Task<TMainEntity?> GetMainEntityById(int id) => await _dataProvider.GetTable<TMainEntity>().FirstOrDefaultAsync(x => x.Id == id);
public virtual async Task<int> DeleteMainEntityById(int id, bool publishEvent = true)
{
var affectedRows = 0;
var mainEntity = await GetMainEntityById(id);
if (mainEntity == null) return affectedRows;
affectedRows = await _dataProvider.GetTable<TMainEntity>().DeleteAsync(x => x.Id == id);
if (publishEvent) await _eventPublisher.PublishAsync(new EntityDeletedEvent<TMainEntity>(mainEntity));
return affectedRows;
}
public Task HandleEventAsync(EntityDeletedEvent<TDtoEntity> eventMessage)
=> throw new Exception($"To delete, you must use the DeleteMainEntityById<{MainEntityType.Name}> function instead of '{typeof(TDtoEntity).Name}'!");
public async Task HandleEventAsync(EntityInsertedEvent<TDtoEntity> eventMessage)
{
var mainEntity = await GetMainEntityById(eventMessage.Entity.Id);
if (mainEntity == null) throw new Exception($"MgDtoDbTableBase<{typeof(TDtoEntity).Name}, {MainEntityType.Name}>->EntityInsertedEvent<{typeof(TDtoEntity).Name}>(); (mainEntity == null); Id: {eventMessage.Entity.Id}");
await _eventPublisher.PublishAsync(new EntityInsertedEvent<TMainEntity>(mainEntity));
}
public async Task HandleEventAsync(EntityUpdatedEvent<TDtoEntity> eventMessage)
{
var mainEntity = await GetMainEntityById(eventMessage.Entity.Id);
if (mainEntity == null) throw new Exception($"MgDtoDbTableBase<{typeof(TDtoEntity).Name}, {MainEntityType.Name}>->EntityUpdatedEvent<{typeof(TDtoEntity).Name}>(); (mainEntity == null); Id: {eventMessage.Entity.Id}");
await _eventPublisher.PublishAsync(new EntityUpdatedEvent<TMainEntity>(mainEntity));
}
public override Task DeleteAsync(int entityId, bool publishEvent = true) => DeleteMainEntityById(entityId, publishEvent);
public override Task DeleteAsync(TDtoEntity entity, bool publishEvent = true) => DeleteMainEntityById(entity.Id, publishEvent);
public override Task DeleteAsync(IList<TDtoEntity> entities, bool publishEvent = true)
=> throw new Exception($"To delete, you must use the DeleteMainEntityById<{MainEntityType.Name}> function instead of '{typeof(TDtoEntity).Name}'!");
public override Task<int> DeleteAsync(Expression<Func<TDtoEntity, bool>> predicate)
=> throw new Exception($"To delete, you must use the DeleteMainEntityById<{MainEntityType.Name}> function instead of '{typeof(TDtoEntity).Name}'!");
public override Task DeleteAsync(Expression<Func<TDtoEntity, bool>> predicate, bool publishEvent)
=> throw new Exception($"To delete, you must use the DeleteMainEntityById<{MainEntityType.Name}> function instead of '{typeof(TDtoEntity).Name}'!");
public override void Delete(TDtoEntity entity, bool publishEvent = true)
=> throw new Exception($"To delete, you must use the DeleteMainEntityById<{MainEntityType.Name}> function instead of '{typeof(TDtoEntity).Name}'!");
public override void Delete(IList<TDtoEntity> entities, bool publishEvent = true)
=> throw new Exception($"To delete, you must use the DeleteMainEntityById<{MainEntityType.Name}> function instead of '{typeof(TDtoEntity).Name}'!");
public override int Delete(Expression<Func<TDtoEntity, bool>> predicate)
=> throw new Exception($"To delete, you must use the DeleteMainEntityById<{MainEntityType.Name}> function instead of '{typeof(TDtoEntity).Name}'!");
}

View File

@ -0,0 +1,17 @@
# Mango.Nop.Data documentation
Topic documentation for the `Mango.Nop.Data` project (Layer 2, data access layer).
## Reference docs (flat)
- [`REPOSITORIES.md`](REPOSITORIES.md) — Repository pattern usage
- [`TRANSACTIONS.md`](TRANSACTIONS.md) — Transaction pattern usage
## Navigation
Per the AI Agent Core Protocol (folder navigation rule), start from this README when browsing `docs/`. All docs at this level are single-file reference.
## See also
- **Repo-level conventions**: `../../docs/CONVENTIONS.md`
- **Core DTOs**: `../Mango.Nop.Core/docs/DTOS.md`

View File

@ -0,0 +1,42 @@
# Repository Pattern
> Part of `Mango.Nop.Data`. See `Mango.Nop.Data/README.md` for project overview.
> For transaction patterns see `docs/TRANSACTIONS.md`.
## MgDbTableBase\<TEntity\>
Repository base wrapping nopCommerce `EntityRepository<TEntity>`.
**Constructor:**
```csharp
MgDbTableBase(IEventPublisher, INopDataProvider, IShortTermCacheManager, IStaticCacheManager, AppSettings)
```
### Features
| Feature | Detail |
|---|---|
| `GetAll()` | Returns `IQueryable<TEntity>` from `Table` property |
| Automatic timestamps | `ITimeStampCreated.Created` set on insert; `ITimeStampModified.Modified` set on insert/update |
| CRUD hooks | `OnInsert(entity)`, `OnUpdate(entity)`, `OnDelete(entity)` — virtual, overridable |
| Cache clear | `OnUpdate` clears `IStaticCacheManager` (currently clears all — TODO noted in code) |
| All CRUD overrides | Overrides all sync/async `Insert`, `Update`, `Delete` methods to call hooks before `base.*` |
| `DeleteAsync(int entityId)` | Convenience: load by id then delete |
| `DeleteAsync(predicate, bool publishEvent)` | When `publishEvent=true`, loads entities first then deletes with events |
## MgDtoDbTableBase\<TDtoEntity, TMainEntity\>
DTO-aware repository for when the DTO entity (`TDtoEntity`) maps to a different main nopCommerce entity (`TMainEntity`). This is needed because LinqToDB tables are registered for DTOs, but nopCommerce events must fire on the main entity type.
### Features
| Feature | Detail |
|---|---|
| `GetMainEntityById(id)` | Loads `TMainEntity` from `INopDataProvider.GetTable<TMainEntity>()` |
| `DeleteMainEntityById(id)` | Deletes the **main entity** (not the DTO) and publishes `EntityDeletedEvent<TMainEntity>` |
| Delete overrides | **All Delete methods throw** — forces callers to use `DeleteMainEntityById()` instead |
| Event bridging | `EntityInsertedEvent<TDtoEntity>` -> loads main entity -> publishes `EntityInsertedEvent<TMainEntity>` |
| Event bridging | `EntityUpdatedEvent<TDtoEntity>` -> loads main entity -> publishes `EntityUpdatedEvent<TMainEntity>` |
| Event bridging | `EntityDeletedEvent<TDtoEntity>` -> **throws** (must use `DeleteMainEntityById`) |
**Critical rule:** Never call `Delete` on a `MgDtoDbTableBase` repository directly. Always use `DeleteMainEntityById(int id)`.

View File

@ -0,0 +1,53 @@
# Transaction Pattern
> Part of `Mango.Nop.Data`. See `Mango.Nop.Data/README.md` for project overview.
> For repository base classes see `docs/REPOSITORIES.md`.
## MgDbContextBase
Abstract database context base. NOT an EF Core DbContext — wraps `INopDataProvider` and `IRepository<T>` nopCommerce repos.
**Constructor:**
```csharp
MgDbContextBase(IRepository<Product>, IRepository<Order>, IRepository<OrderItem>, INopDataProvider, IMgLockService, ILogger)
```
### Standard Repositories
| Property | Type |
|---|---|
| `Orders` | `IRepository<Order>` |
| `OrderItems` | `IRepository<OrderItem>` |
| `Products` | `IRepository<Product>` |
| `DataProvider` | `INopDataProvider` (LinqToDB raw queries) |
| `Logger` | Mango `ILogger` |
| `LockService` | `IMgLockService` (global `SemaphoreSlim`) |
### 4 Transaction Methods
| Method | Lock | Async |
|---|---|---|
| `Transaction(callback)` | No | No |
| `TransactionSafe(callback)` | `SemaphoreSlim` | No |
| `TransactionAsync(callback)` | No | Yes (thread pool) |
| `TransactionSafeAsync(callback)` | `SemaphoreSlim` | Yes (thread pool) |
**Callback contract:** `Func<TransactionScope, (Task<)bool(>)>` — return `true` to commit (`Complete()`), `false` to rollback.
**Isolation level:** `ReadCommitted`
**Error handling:** Catches exceptions, logs, returns `false` (unless `throwException = true`).
**TransactionSafe variants:** Use `LockService.SemaphoreSlim` for global serialization. Use for order creation, stock adjustment — any operation where concurrent modifications would corrupt data.
**Async variants:** Run on thread pool via `TaskHelper.ToThreadPoolTask()`.
## MgDalBase\<TDbContext\>
Data Access Layer orchestrator. Thin wrapper exposing:
| Property | Type | Purpose |
|---|---|---|
| `Name` | `string` | DAL instance name |
| `Context` | `TDbContext` | The DB context (typed) |
| `MutextLock` | `Mutex` | Cross-process locking |

View File

@ -1,7 +0,0 @@
namespace Mango.Nop.Services
{
public class Class1
{
}
}

View File

@ -0,0 +1,157 @@
using AyCode.Core.Helpers;
using AyCode.Entities;
using AyCode.Entities.Server.LogItems;
using AyCode.Utils.Extensions;
using LinqToDB;
using LinqToDB.Data;
using LinqToDB.DataProvider;
using LinqToDB.DataProvider.SqlServer;
using Mango.Nop.Data.Repositories;
using Nop.Core;
using Nop.Core.Domain.Common;
using Nop.Core.Domain.Customers;
using Nop.Core.Domain.Logging;
using Nop.Data;
using Nop.Data.DataProviders;
using System.Data.Common;
using System.Transactions;
using LogLevel = AyCode.Core.Loggers.LogLevel;
using LogLevelNop = Nop.Core.Domain.Logging.LogLevel;
namespace Mango.Nop.Services.Loggers
{
public interface INopLoggerMsSqlNopDataProvider
{
}
public class NopLoggerMsSqlNopDataProvider : MsSqlNopDataProvider, INopLoggerMsSqlNopDataProvider
{
//protected override IDataProvider LinqToDbDataProvider => SqlServerTools.GetDataProvider(SqlServerVersion.v2012, SqlServerProvider.MicrosoftDataSqlClient);
//protected override DbConnection CreateDbConnection(string connectionString = null)
//{
// connectionString = "Data Source=195.26.231.218;Initial Catalog=MangoManagement;Integrated Security=False;Persist Security Info=False;User ID=sa;Password=v6f_?xNfg9N1;Trust Server Certificate=True";
// return base.GetInternalDbConnection(connectionString);
//}
public void InsertLogItem<TEntity>(TEntity entity) where TEntity : notnull
{
using var transaction = CreateTransactionScope(TransactionScopeAsyncFlowOption.Suppress);
using var dataContext = CreateDataConnection();
dataContext.Insert(entity);
transaction.Complete();
}
public async Task InsertLogItemAsync<TEntity>(TEntity entity) where TEntity : notnull
{
using var transaction = CreateTransactionScope(TransactionScopeAsyncFlowOption.Enabled);
await using var dataContext = CreateDataConnection();
await dataContext.InsertAsync(entity);
transaction.Complete();
}
private static TransactionScope CreateTransactionScope(TransactionScopeAsyncFlowOption transactionScopeAsyncFlowOption)
{
var transactionOptions = new TransactionOptions
{
IsolationLevel = IsolationLevel.ReadUncommitted,
Timeout = TransactionManager.DefaultTimeout
};
return new TransactionScope(TransactionScopeOption.Suppress, transactionOptions, transactionScopeAsyncFlowOption);
}
}
public class NopLogWriter : AcLogItemWriterBase<AcLogItem>//AcTextLogWriterBase
{
private readonly SemaphoreSlim _lockSlim = new(0);
private NopLoggerMsSqlNopDataProvider _dataProvider;
private readonly global::Nop.Services.Logging.ILogger _nopLogger;
protected readonly CommonSettings _commonSettings;
protected readonly CustomerSettings _customerSettings;
protected readonly IWebHelper _webHelper;
public NopLogWriter(INopLoggerMsSqlNopDataProvider nopLoggerMsSqlNopDataProvider, CommonSettings commonSettings, CustomerSettings customerSettings, IWebHelper webHelper,
global::Nop.Services.Logging.ILogger nopLogger) : this(nopLoggerMsSqlNopDataProvider, commonSettings, customerSettings, webHelper,nopLogger, null)
{ }
public NopLogWriter(INopLoggerMsSqlNopDataProvider nopLoggerMsSqlNopDataProvider,
CommonSettings commonSettings, CustomerSettings customerSettings, IWebHelper webHelper, global::Nop.Services.Logging.ILogger nopLogger, string? categoryName = null) : base(categoryName)
{
_nopLogger = nopLogger;
_dataProvider = (NopLoggerMsSqlNopDataProvider)nopLoggerMsSqlNopDataProvider;
_commonSettings = commonSettings;
_customerSettings = customerSettings;
_webHelper = webHelper;
}
//public NopLogWriter(ILogger nopLogger, AppType appType, LogLevel logLevel, string? categoryName = null) : base(appType, logLevel, categoryName)
//{
// _nopLogger=nopLogger;
//}
protected override void WriteLogItemCallback(AcLogItem logItem)
{
//using (_lockSlim.UseWait())
{
switch (logItem.LogLevel)
{
case LogLevel.Detail:
case LogLevel.Trace:
case LogLevel.Debug:
//if (_nopLogger.IsEnabled(LogLevelNop.Debug)) InsertLog(LogLevelNop.Debug, logItem.Text, logItem.Exception, null);
//break;
case LogLevel.Info:
if (_nopLogger.IsEnabled(LogLevelNop.Information)) InsertLog(LogLevelNop.Information, logItem.Text, logItem.Exception, null);
//_nopLogger.Information(logItem.Text); //.Forget();
break;
case LogLevel.Suggest:
case LogLevel.Warning:
if (_nopLogger.IsEnabled(LogLevelNop.Warning)) InsertLog(LogLevelNop.Warning, logItem.Text, logItem.Exception, null);
//_nopLogger.Warning(logItem.Text); //.Forget();
break;
case LogLevel.Error:
if (_nopLogger.IsEnabled(LogLevelNop.Error)) InsertLog(LogLevelNop.Error, logItem.Text, logItem.Exception, null);
//_nopLogger.Error(logItem.Text); //.Forget();//, logItem.Exception);
break;
case LogLevel.Disabled:
break;
default:
throw new ArgumentOutOfRangeException();
}
}
}
private void InsertLog(LogLevelNop logLevel, string shortMessage, string fullMessage = "", Customer customer = null)
{
//check ignore word/phrase list?
//if (IgnoreLog(shortMessage) || IgnoreLog(fullMessage))
// return;
_dataProvider.InsertLogItem(PrepareLog(logLevel, shortMessage, fullMessage, customer));
//_dataProvider.InsertLogItemAsync(PrepareLog(logLevel, shortMessage, fullMessage, customer)).Forget();
}
private Log PrepareLog(LogLevelNop logLevel, string shortMessage, string fullMessage = "", Customer customer = null)
{
return new Log
{
LogLevel = logLevel,
ShortMessage = shortMessage,
FullMessage = fullMessage,
IpAddress = _customerSettings.StoreIpAddresses ? _webHelper.GetCurrentIpAddress() : string.Empty,
CustomerId = customer?.Id,
PageUrl = _webHelper.GetThisPageUrl(true),
ReferrerUrl = _webHelper.GetUrlReferrer(),
CreatedOnUtc = DateTime.UtcNow
};
}
}
}

View File

@ -1,46 +1,56 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<BaseOutputPath>bin\FruitBank</BaseOutputPath>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<CopyRefAssembliesToPublishDirectory>true</CopyRefAssembliesToPublishDirectory>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MessagePack.Annotations" Version="2.5.192" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.10" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="MessagePack" Version="3.1.4" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="9.0.11" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\NopCommerce\Libraries\Nop.Core\Nop.Core.csproj" />
<ProjectReference Include="..\..\..\..\NopCommerce\Libraries\Nop.Data\Nop.Data.csproj" />
<ProjectReference Include="..\..\..\..\NopCommerce\Libraries\Nop.Services\Nop.Services.csproj" />
<ProjectReference Include="..\..\..\..\NopCommerce\Presentation\Nop.Web.Framework\Nop.Web.Framework.csproj" />
<ProjectReference Include="..\Mango.Nop.Core\Mango.Nop.Core.csproj" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\FruitBank\Libraries\Nop.Core\Nop.Core.csproj" />
<ProjectReference Include="..\..\..\..\FruitBank\Libraries\Nop.Data\Nop.Data.csproj" />
<ProjectReference Include="..\..\..\..\FruitBank\Libraries\Nop.Services\Nop.Services.csproj" />
<ProjectReference Include="..\..\..\..\FruitBank\Presentation\Nop.Web.Framework\Nop.Web.Framework.csproj" />
<ProjectReference Include="..\Mango.Nop.Core\Mango.Nop.Core.csproj" />
<ProjectReference Include="..\Mango.Nop.Data\Mango.Nop.Data.csproj" />
</ItemGroup>
<ItemGroup>
<Reference Include="AyCode.Core">
<HintPath>..\..\..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\Debug\net8.0\AyCode.Core.dll</HintPath>
</Reference>
<Reference Include="AyCode.Core.Server">
<HintPath>..\..\..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\Debug\net8.0\AyCode.Core.Server.dll</HintPath>
</Reference>
<Reference Include="AyCode.Entities">
<HintPath>..\..\..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\Debug\net8.0\AyCode.Entities.dll</HintPath>
</Reference>
<Reference Include="AyCode.Entities.Server">
<HintPath>..\..\..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\Debug\net8.0\AyCode.Entities.Server.dll</HintPath>
</Reference>
<Reference Include="AyCode.Interfaces">
<HintPath>..\..\..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\Debug\net8.0\AyCode.Interfaces.dll</HintPath>
</Reference>
<Reference Include="AyCode.Interfaces.Server">
<HintPath>..\..\..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\Debug\net8.0\AyCode.Interfaces.Server.dll</HintPath>
</Reference>
<Reference Include="AyCode.Utils">
<HintPath>..\..\..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\Debug\net8.0\AyCode.Utils.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<Reference Include="AyCode.Core">
<HintPath>..\..\..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Core.dll</HintPath>
</Reference>
<Reference Include="AyCode.Core.Server">
<HintPath>..\..\..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Core.Server.dll</HintPath>
</Reference>
<Reference Include="AyCode.Entities">
<HintPath>..\..\..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Entities.dll</HintPath>
</Reference>
<Reference Include="AyCode.Entities.Server">
<HintPath>..\..\..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Entities.Server.dll</HintPath>
</Reference>
<Reference Include="AyCode.Interfaces">
<HintPath>..\..\..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Interfaces.dll</HintPath>
</Reference>
<Reference Include="AyCode.Interfaces.Server">
<HintPath>..\..\..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Interfaces.Server.dll</HintPath>
</Reference>
<Reference Include="AyCode.Utils">
<HintPath>..\..\..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Utils.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<None Include="docs\**\*.md" />
<None Include="**\README.md" Exclude="$(DefaultItemExcludes);docs\**" />
</ItemGroup>
</Project>

View File

@ -1,10 +1,14 @@
using Microsoft.AspNetCore.Http;
using AyCode.Core.Loggers;
using Mango.Nop.Core.Interfaces;
using Mango.Nop.Core.Loggers;
using Mango.Nop.Data.Interfaces;
using Mango.Nop.Data.Repositories;
using Microsoft.AspNetCore.Http;
using Nop.Core.Domain.Catalog;
using Nop.Core.Domain.Customers;
using Nop.Core.Domain.Orders;
using Nop.Core.Events;
using Nop.Services.Events;
using Nop.Services.Logging;
using Nop.Web.Framework.Events;
namespace Mango.Nop.Services;
@ -12,8 +16,9 @@ namespace Mango.Nop.Services;
/// <summary>
/// Represents plugin event consumer
/// </summary>
public class MgEventConsumer(IHttpContextAccessor httpContextAccessor, ILogger logger) :
public abstract class MgEventConsumerBase(IMgDbContextBase ctx, IHttpContextAccessor httpContextAccessor, IEnumerable<IAcLogWriterBase> logWriters) :
IConsumer<EntityUpdatedEvent<Product>>,
IConsumer<EntityInsertedEvent<Product>>,
IConsumer<CustomerRegisteredEvent>,
IConsumer<OrderPlacedEvent>,
IConsumer<PageRenderingEvent>,
@ -24,28 +29,36 @@ public class MgEventConsumer(IHttpContextAccessor httpContextAccessor, ILogger l
{
#region Fields
protected ILogger Logger { get; } = logger;
private readonly IHttpContextAccessor _httpContextAccessor = httpContextAccessor;
protected ILogger Logger { get; } = new Logger<MgEventConsumerBase>(logWriters.ToArray());
protected readonly IHttpContextAccessor HttpContextAccessor = httpContextAccessor;
#endregion
protected virtual async Task<Product> CheckAndUpdateProductManageInventoryMethodToManageStock(Product product)
{
if (product.ManageInventoryMethod == ManageInventoryMethod.ManageStock) return product;
product.ManageInventoryMethod = ManageInventoryMethod.ManageStock;
await ctx.Products.UpdateAsync(product, false);
return product;
}
public virtual async Task HandleEventAsync(EntityUpdatedEvent<Product> eventMessage)
{
}
{ }
public virtual async Task HandleEventAsync(EntityInsertedEvent<Product> eventMessage)
{ }
public virtual async Task HandleEventAsync(CustomerRegisteredEvent eventMessage)
{
}
{ }
public virtual async Task HandleEventAsync(OrderPlacedEvent eventMessage)
{
}
{ }
public virtual async Task HandleEventAsync(PageRenderingEvent eventMessage)
{
}
{ }
public virtual async Task HandleEventAsync(ProductSearchEvent eventMessage)
{
}
{ }
}

View File

@ -1,15 +0,0 @@
namespace Mango.Nop.Services;
public abstract class MgLockService : IMgLockService
{
public SemaphoreSlim SemaphoreSlim { get; protected init; }
protected MgLockService() : this(new SemaphoreSlim(1))
{
}
protected MgLockService(SemaphoreSlim semaphoreSlim)
{
SemaphoreSlim = semaphoreSlim;
}
}

View File

@ -0,0 +1,17 @@
using Mango.Nop.Core.Services;
namespace Mango.Nop.Services;
public abstract class MgLockServiceBase : IMgLockService
{
public SemaphoreSlim SemaphoreSlim { get; protected init; }
protected MgLockServiceBase() : this(new SemaphoreSlim(1))
{
}
protected MgLockServiceBase(SemaphoreSlim semaphoreSlim)
{
SemaphoreSlim = semaphoreSlim;
}
}

View File

@ -1,6 +1,6 @@
namespace Mango.Nop.Services;
public abstract class MgSessionItem(string sessionKey) : IMgSessionItem
public abstract class MgSessionItemBase(string sessionKey) : IMgSessionItem
{
public string SessionId { get; protected set; } = sessionKey;
public string? SignaRConnectionId { get; set; }

View File

@ -3,7 +3,7 @@ using AyCode.Utils.Extensions;
namespace Mango.Nop.Services;
public abstract class MgSessionService<TSessionItem> : IMgSessionService<TSessionItem> where TSessionItem : class, IMgSessionItem
public abstract class MgSessionServiceBase<TSessionItem> : IMgSessionService<TSessionItem> where TSessionItem : class, IMgSessionItem
{
protected ConcurrentDictionary<string, TSessionItem> Sessions { get; } = new();

View File

@ -0,0 +1,29 @@
# Mango.Nop.Services
@project {
type = "framework"
own-dep-projects = [
"AyCode.Core, AyCode.Core.Server, AyCode.Entities, AyCode.Entities.Server, AyCode.Interfaces, AyCode.Interfaces.Server, AyCode.Utils (in AyCode.Core repo)"
]
}
Service base classes for nopCommerce plugin development — background tasks, session management, events, locking, logging. **net9.0**.
## Documentation
| Document | Topic |
|---|---|
| `SERVICES.md` | MgBackgroundServiceBase, MgSessionServiceBase, MgEventConsumerBase, MgLockServiceBase |
| `LOGGING/README.md` | NopLogWriter — AyCode-to-nopCommerce log bridge, TransactionScope(Suppress) |
## Folder Structure
| Folder | Purpose |
|---|---|
| `Loggers/` | `NopLogWriter`, `NopLoggerMsSqlNopDataProvider` — AyCode -> nopCommerce log bridge |
| *(root)* | `MgBackgroundServiceBase`, `MgSessionServiceBase`, `MgEventConsumerBase`, `MgLockServiceBase`, interfaces |
## Dependencies
- `Mango.Nop.Core`, `Mango.Nop.Data` (ProjectReferences)
- `Nop.Core`, `Nop.Data`, `Nop.Services`, `Nop.Web.Framework` (nopCommerce ProjectReferences)

View File

@ -0,0 +1,40 @@
# NopLogWriter — Logging Bridge
> Part of `Mango.Nop.Services`. See `Mango.Nop.Services/README.md` for project overview.
> For AyCode base logging types (`AcLogItemWriterBase`, `AcLogItem`, log levels) see `AyCode.Core/AyCode.Core/docs/LOGGING/README.md`.
## Overview
Bridges AyCode logging to nopCommerce's `Nop.Core.Domain.Logging.Log` table via direct DB insert.
## NopLogWriter
| Feature | Detail |
|---|---|
| Inherits | `AcLogItemWriterBase<AcLogItem>` (AyCode.Core) |
| Log level mapping | AyCode `Detail/Trace/Debug/Info` -> nopCommerce `Information`; `Suggest/Warning` -> `Warning`; `Error` -> `Error` |
| Direct DB insert | Uses `NopLoggerMsSqlNopDataProvider` with `TransactionScope(Suppress)` to avoid transaction conflicts |
**Constructor:** `(INopLoggerMsSqlNopDataProvider, CommonSettings, CustomerSettings, IWebHelper, Nop.Services.Logging.ILogger, string? categoryName)`
## NopLoggerMsSqlNopDataProvider
Extends `MsSqlNopDataProvider`. Provides isolated DB access for log writes.
| Method | Purpose |
|---|---|
| `InsertLogItem<T>()` | Sync insert with own `TransactionScope(Suppress, ReadUncommitted)` |
| `InsertLogItemAsync<T>()` | Async insert with own `TransactionScope(Suppress, ReadUncommitted)` |
The `TransactionScope(Suppress)` is critical — without it, log writes would participate in the caller's transaction, causing nesting conflicts and potential deadlocks.
## Integration Point
```
Application code -> Mango.Nop.Core.Loggers.ILogger (extends IAcLoggerBase)
-> Logger<TCategory> (extends AcLoggerBase, delegates to IAcLogWriterBase[])
-> NopLogWriter -> Nop Log table (direct SQL insert)
-> [Other AyCode log writers: console, file, SignalR, etc.]
```
**Exception:** `MgBackgroundServiceBase` uses `Nop.Services.Logging.ILogger` directly (nopCommerce logger), not the Mango wrapper.

View File

@ -0,0 +1,21 @@
# Mango.Nop.Services documentation
Topic documentation for the `Mango.Nop.Services` project (Layer 2, service patterns).
## Reference docs (flat)
- [`SERVICES.md`](SERVICES.md) — Service pattern usage
## Topic folders
- [`LOGGING/`](LOGGING/README.md) — Logger bridge between NopCommerce's logger and AyCode.Core's logger (project-specific variant)
## Navigation
Per the AI Agent Core Protocol (folder navigation rule), start from this README when browsing `docs/`. Single-file reference docs remain flat; project-specific variants of framework topics (like LOGGING) live in named subfolders.
## See also
- **Base logger** (framework): `../../../../../Aycode/Source/AyCode.Core/AyCode.Core/docs/LOGGING/README.md`
- **Remote logger variant**: `../../../../../Aycode/Source/AyCode.Core/AyCode.Services/docs/LOGGING/README.md`
- **Server-side logger variant**: `../../../../../Aycode/Source/AyCode.Core/AyCode.Core.Server/docs/LOGGING/README.md`

View File

@ -0,0 +1,71 @@
# Service Base Classes
> Part of `Mango.Nop.Services`. See `Mango.Nop.Services/README.md` for project overview.
> For logging bridge see `docs/LOGGING/README.md`.
## MgBackgroundServiceBase
Abstract hosted background service. Inherits `Microsoft.Extensions.Hosting.BackgroundService`.
**Constructor:** `(ILogger, IServiceProvider, int executeIntervalMs)`
| Feature | Detail |
|---|---|
| Loop pattern | `ExecuteAsync` loops: `Task.Delay(ExecuteIntervalMs)` -> `OnExecuteAsync()`, with pause support |
| `OnExecuteAsync(CancellationToken)` | Abstract — subclass implements the actual work |
| `Pause(bool)` | Pauses/resumes the loop without stopping the service |
| `ExecuteIntervalMs` | Configurable interval between iterations |
| Exception handling | Catches and logs errors per iteration, never crashes the loop |
| Logging | Uses nopCommerce `Nop.Services.Logging.ILogger` (**not** `Mango.Nop.Core.Loggers.ILogger`) |
**Interface:** `IMgBackgroundService : IHostedService, IDisposable`
## MgSessionServiceBase\<TSessionItem\>
In-memory session management using `ConcurrentDictionary<string, TSessionItem>`.
| Method | Signature | Purpose |
|---|---|---|
| `GetOrCreateSessionItem` | `(string sessionId) -> TSessionItem?` | Get existing or create new via `Activator.CreateInstance` |
| `TryAddSessionItem` | `(TSessionItem) -> bool` | Add if not exists, throws if duplicate |
| `TryGetSessionItem` | `(string sessionId, out TSessionItem) -> bool` | Try-pattern lookup |
| `TryRemoveSessionItem` | `(string sessionId, out TSessionItem) -> bool` | Remove and return |
| `TryGetSessionItemBySignlaRConnectionId` | `(string connectionId, out TSessionItem?) -> bool` | Find session by SignalR connection |
| `Count` | `() -> int` | Active session count |
**Interface:** `IMgSessionService<TSessionItem> where TSessionItem : IMgSessionItem`
## MgSessionItemBase
Base session item. Primary constructor: `(string sessionKey)`.
| Property | Type | Purpose |
|---|---|---|
| `SessionId` | `string` | Unique session identifier |
| `SignaRConnectionId` | `string?` | Associated SignalR connection ID |
| `RequestCount` | `int` | Request counter |
**Interface:** `IMgSessionItem``SessionId`, `SignaRConnectionId`, `RequestCount`
## MgEventConsumerBase
Abstract nopCommerce event consumer. Subscribes to:
| Event | Handler |
|---|---|
| `EntityUpdatedEvent<Product>` | `HandleEventAsync(...)` — virtual, empty default |
| `EntityInsertedEvent<Product>` | `HandleEventAsync(...)` — virtual, empty default |
| `CustomerRegisteredEvent` | `HandleEventAsync(...)` — virtual, empty default |
| `OrderPlacedEvent` | `HandleEventAsync(...)` — virtual, empty default |
| `PageRenderingEvent` | `HandleEventAsync(...)` — virtual, empty default |
| `ProductSearchEvent` | `HandleEventAsync(...)` — virtual, empty default |
Built-in helper: `CheckAndUpdateProductManageInventoryMethodToManageStock(Product)` — ensures product has `ManageStock` inventory method.
**Constructor:** `(IMgDbContextBase ctx, IHttpContextAccessor, IEnumerable<IAcLogWriterBase> logWriters)`
## MgLockServiceBase
Simple in-process lock using `SemaphoreSlim(1)`. Implements `IMgLockService` (defined in `Mango.Nop.Core.Services`).
Used by `MgDbContextBase.TransactionSafe*` variants for global serialization.

View File

@ -1,12 +0,0 @@
namespace Nop.Core;
/// <summary>
/// Represents the base class for entities
/// </summary>
public abstract partial class BaseEntity
{
/// <summary>
/// Gets or sets the entity identifier
/// </summary>
public int Id { get; set; }
}

View File

@ -1,69 +0,0 @@
using Nop.Core.Configuration;
using Nop.Core.Infrastructure;
namespace Nop.Core.Caching;
/// <summary>
/// Represents key for caching objects
/// </summary>
public partial class CacheKey
{
#region Ctor
/// <summary>
/// Initialize a new instance with key and prefixes
/// </summary>
/// <param name="key">Key</param>
/// <param name="prefixes">Prefixes for remove by prefix functionality</param>
public CacheKey(string key, params string[] prefixes)
{
Key = key;
Prefixes.AddRange(prefixes.Where(prefix => !string.IsNullOrEmpty(prefix)));
}
#endregion
#region Methods
/// <summary>
/// Create a new instance from the current one and fill it with passed parameters
/// </summary>
/// <param name="createCacheKeyParameters">Function to create parameters</param>
/// <param name="keyObjects">Objects to create parameters</param>
/// <returns>Cache key</returns>
public virtual CacheKey Create(Func<object, object> createCacheKeyParameters, params object[] keyObjects)
{
var cacheKey = new CacheKey(Key, Prefixes.ToArray());
if (!keyObjects.Any())
return cacheKey;
cacheKey.Key = string.Format(cacheKey.Key, keyObjects.Select(createCacheKeyParameters).ToArray());
for (var i = 0; i < cacheKey.Prefixes.Count; i++)
cacheKey.Prefixes[i] = string.Format(cacheKey.Prefixes[i], keyObjects.Select(createCacheKeyParameters).ToArray());
return cacheKey;
}
#endregion
#region Properties
/// <summary>
/// Gets or sets a cache key
/// </summary>
public string Key { get; protected set; }
/// <summary>
/// Gets or sets prefixes for remove by prefix functionality
/// </summary>
public List<string> Prefixes { get; protected set; } = new();
/// <summary>
/// Gets or sets a cache time in minutes
/// </summary>
public int CacheTime { get; set; } = Singleton<AppSettings>.Instance.Get<CacheConfig>().DefaultCacheTime;
#endregion
}

View File

@ -1,63 +0,0 @@
using Nop.Core.Infrastructure;
namespace Nop.Core.Caching;
/// <summary>
/// Cache key manager
/// </summary>
/// <remarks>
/// This class should be registered on IoC as singleton instance
/// </remarks>
public partial class CacheKeyManager : ICacheKeyManager
{
protected readonly IConcurrentCollection<byte> _keys;
public CacheKeyManager(IConcurrentCollection<byte> keys)
{
_keys = keys;
}
/// <summary>
/// Add the key
/// </summary>
/// <param name="key">The key to add</param>
public void AddKey(string key)
{
_keys.Add(key, default);
}
/// <summary>
/// Remove the key
/// </summary>
/// <param name="key">The key to remove</param>
public void RemoveKey(string key)
{
_keys.Remove(key);
}
/// <summary>
/// Remove all keys
/// </summary>
public void Clear()
{
_keys.Clear();
}
/// <summary>
/// Remove keys by prefix
/// </summary>
/// <param name="prefix">Prefix to delete keys</param>
/// <returns>The list of removed keys</returns>
public IEnumerable<string> RemoveByPrefix(string prefix)
{
if (!_keys.Prune(prefix, out var subtree) || subtree?.Keys == null)
return Enumerable.Empty<string>();
return subtree.Keys;
}
/// <summary>
/// The list of keys
/// </summary>
public IEnumerable<string> Keys => _keys.Keys;
}

View File

@ -1,115 +0,0 @@
using System.Globalization;
using System.Text;
using Nop.Core.Configuration;
namespace Nop.Core.Caching;
/// <summary>
/// Represents the default cache key service implementation
/// </summary>
public abstract partial class CacheKeyService : ICacheKeyService
{
#region Fields
protected readonly AppSettings _appSettings;
#endregion
#region Ctor
protected CacheKeyService(AppSettings appSettings)
{
_appSettings = appSettings;
}
#endregion
#region Utilities
/// <summary>
/// Prepare the cache key prefix
/// </summary>
/// <param name="prefix">Cache key prefix</param>
/// <param name="prefixParameters">Parameters to create cache key prefix</param>
protected virtual string PrepareKeyPrefix(string prefix, params object[] prefixParameters)
{
return prefixParameters?.Any() ?? false
? string.Format(prefix, prefixParameters.Select(CreateCacheKeyParameters).ToArray())
: prefix;
}
/// <summary>
/// Create the hash value of the passed identifiers
/// </summary>
/// <param name="ids">Collection of identifiers</param>
/// <returns>String hash value</returns>
protected virtual string CreateIdsHash(IEnumerable<int> ids)
{
var identifiers = ids.ToList();
if (!identifiers.Any())
return string.Empty;
var identifiersString = string.Join(", ", identifiers.OrderBy(id => id));
return HashHelper.CreateHash(Encoding.UTF8.GetBytes(identifiersString), HashAlgorithm);
}
/// <summary>
/// Converts an object to cache parameter
/// </summary>
/// <param name="parameter">Object to convert</param>
/// <returns>Cache parameter</returns>
protected virtual object CreateCacheKeyParameters(object parameter)
{
return parameter switch
{
null => "null",
IEnumerable<int> ids => CreateIdsHash(ids),
IEnumerable<BaseEntity> entities => CreateIdsHash(entities.Select(entity => entity.Id)),
BaseEntity entity => entity.Id,
decimal param => param.ToString(CultureInfo.InvariantCulture),
_ => parameter
};
}
#endregion
#region Methods
/// <summary>
/// Create a copy of cache key and fills it by passed parameters
/// </summary>
/// <param name="cacheKey">Initial cache key</param>
/// <param name="cacheKeyParameters">Parameters to create cache key</param>
/// <returns>Cache key</returns>
public virtual CacheKey PrepareKey(CacheKey cacheKey, params object[] cacheKeyParameters)
{
return cacheKey.Create(CreateCacheKeyParameters, cacheKeyParameters);
}
/// <summary>
/// Create a copy of cache key using the default cache time and fills it by passed parameters
/// </summary>
/// <param name="cacheKey">Initial cache key</param>
/// <param name="cacheKeyParameters">Parameters to create cache key</param>
/// <returns>Cache key</returns>
public virtual CacheKey PrepareKeyForDefaultCache(CacheKey cacheKey, params object[] cacheKeyParameters)
{
var key = cacheKey.Create(CreateCacheKeyParameters, cacheKeyParameters);
key.CacheTime = _appSettings.Get<CacheConfig>().DefaultCacheTime;
return key;
}
#endregion
#region Properties
/// <summary>
/// Gets an algorithm used to create the hash value of identifiers need to cache
/// </summary>
protected string HashAlgorithm => "SHA1";
#endregion
}

View File

@ -1,29 +0,0 @@
namespace Nop.Core.Caching;
public static class CachingExtensions
{
/// <summary>
/// Get a cached item. If it's not in the cache yet, then load and cache it.
/// NOTE: this method is only kept for backwards compatibility: the async overload is preferred!
/// </summary>
/// <typeparam name="T">Type of cached item</typeparam>
/// <param name="cacheManager">Cache manager</param>
/// <param name="key">Cache key</param>
/// <param name="acquire">Function to load item if it's not in the cache yet</param>
/// <returns>The cached value associated with the specified key</returns>
public static T Get<T>(this IStaticCacheManager cacheManager, CacheKey key, Func<T> acquire)
{
return cacheManager.GetAsync(key, acquire).GetAwaiter().GetResult();
}
/// <summary>
/// Remove items by cache key prefix
/// </summary>
/// <param name="cacheManager">Cache manager</param>
/// <param name="prefix">Cache key prefix</param>
/// <param name="prefixParameters">Parameters to create cache key prefix</param>
public static void RemoveByPrefix(this IStaticCacheManager cacheManager, string prefix, params object[] prefixParameters)
{
cacheManager.RemoveByPrefixAsync(prefix, prefixParameters).Wait();
}
}

Some files were not shown because too many files have changed in this diff Show More