diff --git a/.editorconfig b/.editorconfig
index 892b63c33c..f731f7194a 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -1,6 +1,6 @@
-root = true
+# root = true
# Unix-style newlines with a newline ending every file
-[*]
-indent_style = tab
-indent_size = 4
+# [*]
+# indent_style = tab
+# indent_size = 4
diff --git a/.gitignore b/.gitignore
index 336b64107a..85bd82b7ff 100644
--- a/.gitignore
+++ b/.gitignore
@@ -300,6 +300,7 @@ Kopie von*
/src/Presentation/SmartStore.Web/App_Data/ImportProfiles/
/src/Presentation/SmartStore.Web/App_Data/Indexing/
/src/Presentation/SmartStore.Web/App_Data/Lucene/
+/src/Presentation/SmartStore.Web/App_Data/EfCache/
/src/Presentation/SmartStore.Web/App_Data/Settings.txt
/src/Presentation/SmartStore.Web/App_Data/InstalledPlugins.txt
/src/Presentation/SmartStore.Web/App_Data/Licenses.lic
@@ -320,3 +321,4 @@ Kopie von*
src/SmartStoreNET.Packager.sln
Log.txt
+src/Presentation/SmartStore.Web/Themes/FlexMuseo/
diff --git a/CREDITS.txt b/CREDITS.txt
index e421722989..5ea4c4a6fb 100644
--- a/CREDITS.txt
+++ b/CREDITS.txt
@@ -52,11 +52,11 @@ Copyright: Copyright Andrey Taritsyn 2014
License: Apache License 2.0 (Apache)
-CKEditor
+summernote
---------------------------------------------
-WebSite: http://ckeditor.com/
-Copyright: © 2014 CKSource - Frederico Knabben
-License: GNU Library General Public License (LGPL)
+WebSite: https://summernote.org/
+Copyright: Copyright (c) 2015~ Summernote Team (https://github.com/orgs/summernote/people)
+License: MIT
DotNetOpenAuth
diff --git a/README.md b/README.md
index bd0524f866..f8e84aa9bb 100644
--- a/README.md
+++ b/README.md
@@ -1,24 +1,60 @@
-## Overview
+
+
+
+
+
+
+
+
+
+
+
+
+Ready. Sell. Grow.
+
-
+
+
+
+
+
+
-SmartStore.NET is a free, open source, full-featured e-commerce solution for companies of any size. It is web standards compliant and incorporates the newest Microsoft web technology stack.
+
+ Try Online ∙
+ Docs ∙
+ Forum ∙
+ Marketplace ∙
+ Translations
+
+
+
+Smartstore.NET is a free, open source, full-featured e-commerce solution for companies of any size. It is web standards compliant and incorporates the newest Microsoft web technology stack.
-**SmartStore.NET includes all essential features to create multilingual and multi-currency stores** targeting desktop or mobile devices and enabling SEO optimized rich product catalogs with support for an unlimited number of products and categories, variants, bundles, datasheets, ESD, discounts, coupons and many more.
+**Smartstore.NET includes all essential features to create multilingual and multi-currency stores** targeting desktop or mobile devices and enabling SEO optimized rich product catalogs with support for an unlimited number of products and categories, variants, bundles, datasheets, ESD, discounts, coupons and many more.
-A comprehensive set of tools for CRM & CMS, sales, marketing, payment & shipping handling, etc. makes SmartStore.NET a powerful all-in-one solution fulfilling all your needs.
+A comprehensive set of tools for CRM & CMS, sales, marketing, payment & shipping handling, etc. makes Smartstore.NET a powerful all-in-one solution fulfilling all your needs.
-**SmartStore.NET delivers a beautiful and configurable shop front-end out-of-the-box**, built with a design approach on the highest level, including components like `Bootstrap 4`, `Sass` and others. The supplied theme _Flex_ is modern, clean and fully responsive, giving buyers the best possible shopping experience on any device.
+
+
+
+
+
-The state-of-the-art architecture of SmartStore.NET - with `ASP.NET 4.5` + `MVC 5`, `Entity Framework 6` and Domain Driven Design approach - makes it easy to extend, extremely flexible and essentially fun to work with ;-)
+**Smartstore.NET delivers a beautiful and configurable shop front-end out-of-the-box**, built with a design approach on the highest level, including components like `Bootstrap 4`, `Sass` and others. The supplied theme _Flex_ is modern, clean and fully responsive, giving buyers the best possible shopping experience on any device.
+
+The state-of-the-art architecture of Smartstore.NET - with `ASP.NET 4.5` + `MVC 5`, `Entity Framework 6` and Domain Driven Design approach - makes it easy to extend, extremely flexible and essentially fun to work with ;-)
* **Website:** [http://www.smartstore.com/en/net](http://www.smartstore.com/en/net)
* **Forum:** [http://community.smartstore.com](http://community.smartstore.com)
* **Marketplace:** [http://community.smartstore.com/marketplace](http://community.smartstore.com/marketplace)
* **Translations:** [http://translate.smartstore.com/](http://translate.smartstore.com/)
-* **Documentation:** [SmartStore.NET Documentation in English](http://docs.smartstore.com/display/SMNET/SmartStore.NET+Documentation+Home)
+* **Documentation:** [Smartstore.NET Documentation in English](http://docs.smartstore.com/display/SMNET/SmartStore.NET+Documentation+Home)
+
+
+
## Highlights
@@ -35,11 +71,13 @@ The state-of-the-art architecture of SmartStore.NET - with `ASP.NET 4.5` + `MVC
### Features
+* NEW: CMS Page Builder
+* NEW: CMS Menu Builder
* Multi-Store support
* Unlimited number of products and categories
* Product Bundles
* RESTful WebApi
-* Multi-language support
+* Multi-language and RTL support
* Modern, clean, SEO-optimized and fully responsive Theme based on Bootstrap 4
* Ultra fast search framework with faceted search support
* Extremely scalable thanks to output caching, REDIS & Microsoft Azure support.
@@ -55,36 +93,28 @@ The state-of-the-art architecture of SmartStore.NET - with `ASP.NET 4.5` + `MVC
* CMS (Blog, Forum, custom pages & HTML content etc.)
* and many more...
+
+
## Project Status
-SmartStore.NET V3.0.0 has been released on May 15, 2017. The highlights are:
-* **Flex**: New mobile-first responsive Theme based on Bootstrap 4
-* **Mega Search \***: highly professional search framework based on Lucene.NET
- * Ultra fast search results, even with millions of items
- * Faceted search
- * Synonyms
- * Compound word splitting
-* **Mega Menu \***: highly customizable catalog menu widgets
-* **Content Slider \***: creates eye-catching content to boost sales
-* **Output Cache \*** with "donut hole caching" for maximum speed and scalability
-* **Microsoft AZURE \*** provider for media storage
-* Web Farms: **REDIS \*** providers for Business Cache, Output Cache and Session State
-* Product variant **option sets**
-* New product specification attribute type: **numeric range**
-* Image support for variant attributes
-* ...and a lot more new features, enhancements and fixes
+Smartstore.NET V3.2.0 has been released on 10 May, 2019. The highlights are:
-\* Commercial plugin
+* (NEW) **Page Builder**: Create fascinating content that will boost your sales. No coding is required thanks to a powerful WYSIWYG editor which utilizes the revolutionary *CSS Grid* system (commercial plugin)
+* (NEW) **Menu Builder**: Visual manager for all sorts of menus. Change existing menus or create your own and place them anywhere you want.
+* (Perf) **Faster MegaSearch** thanks to Lucene.NET 4.8
+* (Perf) **Huge performance increase** in discount resolution and calculation.
## Try it online
-We have set up a live online demo for you so you are able to test SmartStore.NET without local installation. Get a first impression and test all available features in the front- and in the backend. Please keep in mind that the backend demo is shared and other testers can modify data at the same time.
+We have set up a live online demo for you so you are able to test Smartstore.NET without local installation. Get a first impression and test all available features in the front- and in the backend. Please keep in mind that the backend demo is shared and other testers can modify data at the same time.
* [**Frontend**](http://frontend.smartstore.net/en) (User: demo, PWD: 1234)
* [**Backend**](http://backend.smartstore.net/en/login) (User: demo, PWD: 1234)
+
+
## How to install
* Download the latest stable release from the download tab and unzip it to your web folder
@@ -92,7 +122,7 @@ We have set up a live online demo for you so you are able to test SmartStore.NET
* Fire up your browser and follow the installation instructions
* Enjoy ;-)
-NOTE: SmartStore.NET 3 requires [Visual C++ Redistributable für Visual Studio 2015](https://www.microsoft.com/en-US/download/details.aspx?id=52685) which is already pre-installed on most systems. If, nevertheless, it is missing on your web server, just download and execute the installer or ask your hosting provider to do that for you.
+NOTE: Smartstore.NET 3 requires [Visual C++ Redistributable für Visual Studio 2015](https://www.microsoft.com/en-US/download/details.aspx?id=52685) which is already pre-installed on most systems. If, nevertheless, it is missing on your web server, just download and execute the installer or ask your hosting provider to do that for you.
### System requirements
@@ -103,6 +133,7 @@ NOTE: SmartStore.NET 3 requires [Visual C++ Redistributable für Visual Studio 2
* Full Trust
+
## License
-SmartStore.NET is released under the [GPLv3 license](http://www.gnu.org/licenses/gpl-3.0.txt).
+Smartstore.NET Community Edition is released under the [GPLv3 license](http://www.gnu.org/licenses/gpl-3.0.txt).
diff --git a/SmartStoreNET.Tasks.Targets b/SmartStoreNET.Tasks.Targets
index dd6a1a1895..080c78d497 100644
--- a/SmartStoreNET.Tasks.Targets
+++ b/SmartStoreNET.Tasks.Targets
@@ -39,7 +39,7 @@
x86
$(BUILD_NUMBER)
- 3.0.3
+ 3.2.2
$(StageFolder)
.$(Version)
@@ -90,21 +90,20 @@
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
@@ -126,6 +125,7 @@
+
@@ -137,9 +137,9 @@
OutDir=$(StageFolder)\Administration\bin\;Configuration=$(Configuration)" />
-
-
-
+
+
+
@@ -159,11 +159,11 @@
-
-
+
+
-
-
+
+
diff --git a/assets/SmartStoreNET.png b/assets/SmartStoreNET.png
deleted file mode 100644
index 1fc5ca5409..0000000000
Binary files a/assets/SmartStoreNET.png and /dev/null differ
diff --git a/assets/smartstore-icon.png b/assets/smartstore-icon.png
new file mode 100644
index 0000000000..e6414db195
Binary files /dev/null and b/assets/smartstore-icon.png differ
diff --git a/assets/smartstore-text.png b/assets/smartstore-text.png
new file mode 100644
index 0000000000..25bf9558fb
Binary files /dev/null and b/assets/smartstore-text.png differ
diff --git a/build.bat b/build.bat
index 8d1b2b2f19..94a6c72519 100644
--- a/build.bat
+++ b/build.bat
@@ -1,37 +1,45 @@
+SETLOCAL ENABLEDELAYEDEXPANSION
+
for /f "usebackq tokens=1* delims=: " %%i in (`lib\vswhere\vswhere -latest -requires Microsoft.Component.MSBuild`) do (
- if /i "%%i"=="installationPath" set InstallDir=%%j
+ if /i "%%i"=="installationPath" (
+ set InstallDir=%%j
+ echo !InstallDir!
+ if exist "!InstallDir!\MSBuild\15.0\Bin\MSBuild.exe" (
+ echo "Using MSBuild from Visual Studio 2017"
+ set msbuild="!InstallDir!\MSBuild\15.0\Bin\MSBuild.exe"
+ goto build
+ )
+ if exist "!InstallDir!\MSBuild\Current\Bin\MSBuild.exe" (
+ echo "Using MSBuild from Visual Studio 2019"
+ set msbuild="!InstallDir!\MSBuild\Current\Bin\MSBuild.exe"
+ goto build
+ )
+ )
)
FOR %%b in (
- "%InstallDir%\Common7\Tools\VsMSBuildCmd.bat"
- "%VS140COMNTOOLS%\Common7\Tools\vsvars32.bat"
+ "%VS150COMNTOOLS%\vsvars32.bat"
+ "%VS140COMNTOOLS%\vsvars32.bat"
+ "%VS120COMNTOOLS%\vsvars32.bat"
+ "%VS110COMNTOOLS%\vsvars32.bat"
) do (
if exist %%b (
- call %%b
- goto findmsbuild
+ echo "Using MSBuild from %%b"
+ call %%b
+ set msbuild="msbuild"
+ goto build
)
)
echo "Unable to detect suitable environment. Build may not succeed."
-:findmsbuild
-
-SETLOCAL ENABLEDELAYEDEXPANSION
-
-if exist "%InstallDir%\MSBuild\15.0\Bin\MSBuild.exe" (
- if not defined MsBuildPath (
- SET "MsBuildPath=%InstallDir%\MSBuild\15.0\Bin\MsBuild.exe"
- goto build
- )
-)
-
-
-echo "Unable to detect suitable MsBuild version (15.0). Build may not succeed."
-
:build
+
cd /d %~dp0
echo "Restoring NuGet packages"
-lib\nuget\nuget.exe restore "src\SmartStoreNET.Full-sym.sln"
+lib\nuget\nuget.exe restore "src\SmartStoreNET.sln"
+
+%msbuild% SmartStoreNET.proj /p:SlnName=SmartStoreNET /m /p:DebugSymbols=false /p:DebugType=None /maxcpucount %*
-call "!MsBuildPath!" SmartStoreNET.proj /p:SlnName=SmartStoreNET /m /p:DebugSymbols=false /p:DebugType=None /maxcpucount %*
+:end
diff --git a/changelog.md b/changelog.md
index 4a275eeea8..81357cca18 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,32 +1,318 @@
-# Release Notes
+# Release Notes
+
+## SmartStore.NET 3.2.2
+
+### New Features
+* External category links: alternative links for categories in the main menu and in category listings.
+* Page Builder: multistore support for stories.
+* #1639 Shipping.ByTotal: Add option to calculate the shipping rate based on the net total instead of the gross total.
+* #1537 Implement "Ratenzahlung powered by PayPal" provider.
+* New Heidelpay payment plugin.
+* Shop-Connector: mapping of product SKUs that differ between client shops.
+* MegaMenu supports configuration of menu items
+
+
+### Improvements
+* Amazon Pay: PSD2. Strong customer authentication (SCA) upgrade.
+* Updated *MaxMind.GeoIP* library and country database
+* Removed HTML encoding for meta tags and document title.
+* #1662 Quantity Units: added field for name pluralization.
+* (SEO) combined SSL and canonical host 301 redirections to prevent successive redirects.
+* (SEO) Activated GZIP compression for all dynamic and static responses.
+* Set max. file upload size to 2 GB (globally)
+* (Dev) Added a sample block for the *Page Builder* in DevTools plugin.
+* Various other minor improvements.
+* PayPal PLUS: paying by instalments can be selected in payment wall.
+* Payone: send more information to API (e.g. shipping address) to stay as frictionless as possible for PSD2.
+* PageBuilder: Added keyboard shortcuts
+* #1582 Added structured data to reviews
+* BMEcat: added support for import of specification attributes
+
+### Bugfixes
+* Fixed wrong XML sitemap index numbering.
+* #1646 Setting ManufacturerItemsToDisplayOnHomepage to 1 will not show any manufacturer.
+* #1647 All manufacturers page: link of last character missing in alphabetical letter list.
+* #1664 RTL: CMS menu issues in backend.
+* #1677 Page Builder: moving and resizing blocks via drag & drop didn't work correctly in RTL.
+* Do not adjust inventory twice when an order is cancelled and deleted.
+* #1500 Fallback to fixed VAT rate instead of rate by region for countries that are not subject to vat.
+* Fixed "The entity wrapper stored in the proxy does not reference the same proxy" when copying an export profile.
+* Various other minor fixes.
+* BMEcat: set TaxCategoryId & MainPictureId for newly imported products
+
+
+
+## SmartStore.NET 3.2.1
+
+### Improvements
+* Implemented file range support for CachedFileResult
+* Sitemap should only include products visible for current customer, which is the search engine system customer
+* Fixed uniform spelling for query string parameter "returnUrl"
+* Fixed search engine issue (itemtype, "offers" required) on product detail page
+* Summernote: solved html formatting issues thanks to "Beautifier" library
+* Media middleware: simplified ETag generation & handling
+
+### Bugfixes
+
+* Mega Menu: Product rotator wasn't working
+* Tiny Image: Fixed periodic IOException
+* URL Rewriter: performance optimization
+* Fixed more menu for main catalog navigation
+* Fixed DateTime nor expressed as UTC in AzureFile & LocalFile
+* Content Slider: Fixed slide display bug on surface and other touch devices
+* Content Slider: Randomize slides > Value was stored in database but toggle button has always shown "off"
+* Page Builder: Fixed slider bug for list blocks (brand, category, products) in Firefox
+* Page Builder: fixes blurry background images in IOS
+* Output Cache: action widgets didn't render when they were part of a widgetzone within a donut child action
+* Multishop resolution failed under certain circumstances
+* Fixed menu issue "collection was modified, enumeration operation may not execute"
+* Fixed "The parameters dictionary contains a null entry for parameter 'catId' of non-nullable type 'System.Int32'"
+* Fixed discount coupon sometimes wasn't applied
+* Fixed "Value cannot be null. Parameter name: key" in order list
+* Fixed topic link resolving by system name in multistore environment
+* Async event consumers were not guaranteed to be awaited
+
+
+## SmartStore.NET 3.2
+
+### Highlights
+* (NEW) **CMS Page Builder**: Create fascinating content that will boost your sales. No coding is required thanks to a powerful WYSIWYG editor which utilizes the revolutionary *CSS Grid* system (commercial plugin)
+* (NEW) **CMS Menu Builder**: Visual manager for all sorts of menus. Change existing menus or create your own and place them anywhere you want.
+* (Perf) **Faster MegaSearch** thanks to Lucene.NET 4.8
+* (Perf) **Huge performance increase** in discount resolution and calculation.
+
+### New Features
+* **EmailReminder**:
+ * Reminders for open shopping carts
+ * Reminders for product reviews
+ * Reminders to visit the shop after a long absence
+* **Scheduling**
+ * Task execution history
+ * Web farms: tasks can run decidedly on each machine of a web farm
+ * #1144 MegaSearch: multi server search index
+* **Topics & Pages**
+ * Access control lists
+ * Grid paging & filtering
+ * New properties: added **IsPublished**, **Short Title** (link text) and **Intro** (teaser) properties.
+ * #1542: added options to set body CSS class and Html Id.
+* **MegaSearch**:
+ * Supports searching for forum posts.
+ * #1172 Option to display related search terms on search page.
+ * Command to continue a previously aborted search index rebuild.
+* **Import/export of**:
+ * Product tags
+ * Tier prices
+ * Product attribute options
+ * Product attribute combinations (update only)
+* **Forum**:
+ * Added option to display a captcha on forum pages when creating or replying to a topic.
+ * #417 Restrict forum groups to specific customer roles.
+ * Added published property to forum topic and post.
+ * Added voting for forum posts.
+ * Several performance improvements.
+* **BeezUp**:
+ * #1459 Add option to only submit one category name per product
+ * Allow to specify export categories per product
+* **Santander**:
+ * Added payment method for instalment purchase.
+ * Added financing calculator.
+* Updated icon library to **Font Awesome 5 Free** with the possibility to activate pro version in the frontend.
+* New storefront catalog options: **ShowSubCategoriesInSubPages**, **ShowDescriptionInSubPages** & **IncludeFeaturedProductsInSubPages** (Subpage = List index > 1 or any active filter).
+* New security option: Use **invisible reCAPTCHA**
+* Wallet: Allow customer to choose whether refund should be submitted to his wallet.
+* Added option to display preview pictures in product lists
+* Added option to add multiple file versions to product download section
+* Added options for alternating price display (in badges)
+* Customer avatar: Letter with colored background if no avatar image was uploaded.
+* Viveum: Supports payment via "Virtual Account Brands" (e.g. PayPal).
+* Added options for alternating price display (in badges).
+* #1515 Poll: Add result tab with a list of answers and customers for a poll
+* BMEcat: Added export and import of product tags.
+* Web-API: Added endpoints for ProductSpecificationAttribute.
+* Trusted Shops: Added new Trustbadge display for mobile devices
+* (Dev) Added *sm:EfCommandTimeout* setting to web.config (was 30 sec. fixed, now configurable)
+
+### Improvements
+* (Perf) Significantly increased query performance for products with a lot of category assignments (> 10).
+* (Perf) Increased app startup speed by up to 20%.
+* (Perf) Ultra-fast file-based XML sitemap generation for extremely large catalogs (> 1M)
+ * At least 10x faster
+ * Generated files are saved on the hard disk now: a rebuild after an app restart is no longer necessary.
+ * No exclusive locks during rebuilds anymore: if an (outdated) file already exists, it is returned instantly.
+* (Dev) Enhanced pub/sub: new `IConsumer` marker interface for classes that contain one or more event message handlers. The generic `IConsumer` implements `IConsumer` but has been marked as `Obsolete`.
+* Better protection against XSS attacks
+* Updated to Bootstrap version 4.1.3
+* **Debitoor**:
+ * Partially update customer instead of full update to avoid all fields being overwritten.
+ * #1540 Place company name in front of customer name (according to address format of the particular country).
+ * #1565 Add a button to manually create a draft invoice.
+* #1479 Show in messages the delivery time at the time of purchase
+* #1184 Sort current shopping carts & current wishlists by `ShoppingCartItem.CreatedOn`.
+* #1106 BMECat: import & export support for product keywords
+* #1499 Added hint to forms indicating that fields with an asterisk (*) are required
+* Added filter for newsletter subscriber export by working language
+* Refactored download section
+* Enhanced EntityPicker to pick from customers, manufacturers & categories
+* #1510 Breadcrumb of an associated product should include the grouped product if it has no assigned categories.
+* OpenTrans: added customer number to parties
+* Do not filter cookie using resources if cookie usage has not yet been consented to.
+* #1563 QueuedMessagesClearTask: add a setting for the age of the mails to be deleted.
+* #1569 Added a setting to show login note if no prices are displayed due to customer group permissions.
+* PayPal PLUS: Up to 10 more third party payment methods are allowed by PayPal now.
+* #1560 Tell-a-Friend and anonymous user: render alert box and tell guest to login to use this function
+* #1571 Compare products now shows all specification attribute options
+* #1539 Signing in is now allowed with e-mail and username
+* Trusted Shops: Trustbadge won't be displayed in Popups & Iframes anymore
+* #1461 Admin Grid: filter dialog will be displayed entirely even when grid has no data to display
+* Lots of minor enhancements for both frontend and admin theme
+* Many other minor improvements
+
+### Bugfixes
+* In a multi-store environment, multiple topics with the same system name can now be resolved reliably.
+* **GMC**:
+ * Export the product images if no attribute images are defined
+ * Do not export the first image twice for additional images
+ * Export image URL of full size image (not default size) for additional images
+ * Custom labels are now being exported
+* Media middleware: 0-byte files should be treated as missing.
+* Megamenu alpha/omega blends do now toggle correctly on touch devices
+* Summernote HTML editor exceeds parent container width when CodeMirror is activated
+* Only display a zero search hits warning if at least one filter is activated
+* #1436 Do not display delivery time in customer order completed messages
+* "ArgumentNullException: The value must not be NULL" if a topic is password protected
+* Tax by region: Fixes after inserting a tax rate country column shows "Unavailable"
+* #1014 Switching to default language keeps specific URL alias of current page
+* Shipping by total: When inserting a record the country now will be saved
+* #1460 Editing of the customer title was missing on customer and address pages in the backend
+* #1447 Checkout button payment methods (Amazon, PayPal Express) won't work in conjunction with mandatory checkout attributes
+* Fixed Autofac circularity error
+* When creating a topic, the widget zone input has shown System.String[]
+* Switching the language always redirected to the home page if SEO friendly URLs was deactivated.
+* File upload of a checkout attribute was not stored on cart page.
+* Redirecting within checkout may have displayed an incorrect URL in the browser.
+* Server cannot modify cookies after HTTP headers have been sent.
+* Wrong base price on product and cart page when a special price is active.
+* In a multi-store, message templates may have loaded the wrong disclaimer and conditions-of-use text.
+* NullReferenceException in manufacturer list when there is no manufacturer.
+* Wrong order of featured products on category page.
+* #1504 Cart item price calculation was wrong if attribute combinations with text types were involved.
+* #1485 Dropdown list for product sorting didn't not work with Internet Explorer 11.
+* #1468 Twitter authentication wasn't working anymore.
+* Newsletter subscription didn't work when customer privacy setting DisplayGdprConsentOnForms was turned off
+* Fixed social media image detection
+* Fixed redirection of bots when several languages were active
+* Region cannot be selected in checkout when entering a billing or shipping address
+* Fixed invalid conversion of "System.Int32" to "SmartStore.Core.Domain.Tax.VatNumberStatus" when placing an order
+* MegaMenu: Improved item rendering for third tier elements
+* Product display order on category and manufacturer pages was sometimes wrong when using linq search.
+* Debitoor: Wrong invoice total if a subtotal discount has been applied.
+* Import: Fixes invalid conversion "System.Double" to "SmartStore.Core.Domain.Catalog.QuantityControlType".
+* Topics: Fixes "Cannot insert duplicate key row in object 'dbo.UrlRecord' with unique index 'IX_UrlRecord_Slug'".
+* #1566 Santander: eliminate the 1 cent rounding difference at amountTotalNet.
+* Fixed redirection to the homepage for pages which are loaded while the application is restarted.
+* #1570 Filter option "Only deactivated customers" filters deleted instead of deactivated customers.
+* #1475 select boxes must be wrapped on mobile devices if data-select-url is set
+* Fixed the redirection to the homepage for pages which were loaded while the application was restarted
+* Fixes product feeds expect a different base price formatting.
+* #1369 Shopping cart shows "Discount code applied", although it is not applied due to a lower tier price.
+* Many other fixes...
+
+
+## SmartStore.NET 3.1.5
+
+### Highlights
+* Compliance with EU-GDPR requirements
+* Search engine friendly topic URLs
+* "Honeypot" bot detection for registration and contact forms.
+
+### New Features
+* #1429 Search engine friendly topic URLs
+* Implemented cookie consent according to EU-GDPR
+* Added checkboxes for data processing consent in all relevant forms
+* Implemented "Honeypot" bot detection for registration and contact forms.
+* Trusted Shops: Added consent checkbox to confirm order page for submission of customer email address to Trusted Shops if review widget is active
+* #1226 Shop-Connector: Added exchange of tier prices and delivery times
+* #1439 Debitoor: Option whether to display the payment method and SKU on invoices
+
+### Improvements
+* Added double opt-in feature for newsletter subscriptions during checkout (confirm order)
+* Allow forward slash in product tag URL slug
+* Theming: throttle AJAX cart refresh after spin up/down click
+* Moved StoreLastIpAddress & DisplayPrivacyAgreementOnContactUs from customer settings to privacy settings tab
+* #1450 Show the regular price only if it's higher than the final price
+* #1450 Do not ignore discounts with a negative amount
+* (Soft) deleted customers can be edited now
+* Customer IP addresses will be anonymized on (soft) deletion
+* Set catalogsettings.showsharebutton to false as its not compliant with GDPR
+* Made form fields for first & last name in customer registration optional
+* Implemented settings to make form fields for first & last name required again
+* Made form field for full name in contact us & product request optional
+* Implemented settings to make form field for full name in contact us & product request required again
+* #1453 Import: Use [IGNORE] to ignore a field value on record level
+* #1455 More detail on packing slip when bundled item
+* Display category alias as badge in grids and dropdowns
+
+### Bugfixes
+* Migration: take all same-named message templates into account
+* Messaging: OrderPlaced e-mail templates show main product image even when an attribute combination with a custom image has been selected
+* Theming: fix broken product review voting
+* Theming: added missing bottom space to .html-editor-content
+* Theming: Language switcher is not displayed if no currency options are available
+* No bundle item thumbnail displayed in bundle summary if item is not individually visible
+* Tracking number in shipment details was not saved
+* Assigning or removing product tags did not invalidate model cache
+* Reward points weren't displayed in message templates
+* Dashboard: link for not yet shipped orders loads list with all orders
+* Topic search button had no effect
+* #1442 Message factory should not throw an exception if a template has been deactivated
+* Fixes script error "$(...).tab is not a function" on product detail page
+* Title attribute for the product name in product lists was sometimes truncated
+* Relativizing font sizes should cast to double, not int
+* Fixes category list on product edit page shows empty category name
+* #1438 Debitoor: The country is displayed twice
+* MegaSearch: Fixes indexing ignores DeliveryTimeIdForEmptyStock setting
+* Web API: Fixes "No NavigationLink factory was found for the navigation property 'WalletHistory'"
+* #1449 IgnoreCharges of shipping methods is not working if a localized name is specified
+* Fixes "The object does not support the property or method 'startsWith'" on product edit page.
+* Wallet: Fixes "Child actions are not allowed to perform redirect actions" when there are cart warnings
+* Fixes the delivery time in the order notifications may differ from delivery time on the product detail page
+
## SmartStore.NET 3.1.0
-### Breaking changes
-* Message template customizations are lost due to the new template engine. You have to customize the templates again. No automatic migration, sorry :-(
-* Amazon Pay: The plugin has been changed to new *Login and pay with Amazon* services. A registration at Amazon and new access data are necessary for its use. The old access data can no longer be used.
-* (Dev) Calls to cache methods `Keys()` and `RemoveByPattern()` require glob chars to be present now (supported glob-styles see [https://redis.io/commands/keys](https://redis.io/commands/keys)). Previously these methods appended `*` to the passed pattern, which made pattern matching rather unflexible.
-* (Dev) Hook framework now passes `IHookedEntity` interface instead of `HookedEntity` class
-* (Dev) Completely removed all `EntityInserted`, `EntityUpdated` and `EntityDeleted` legacy events. We were using DbSaveHooks anyway, which provides a much more powerful and way faster pub-sub mechanism for database operations.
### Highlights
-* New [Liquid](https://github.com/Shopify/liquid/wiki/Liquid-for-Designers) based template engine
-* Multi-configurable rounding of order total ("cash rounding"). Can be adjusted and activated separately for each currency and payment method.
-* (Perf) Picture service: new processing and caching strategy! Thumbnails are not created synchronously during the main request anymore, instead a new middleware route defers processing until an image is actually requested by any client.
-* MegaMenu shrinker and *Brands* virtual menu item
-* Address formatting templates by country
-* Connection to translate.smartstore.com. For available languages, localized resources can be downloaded and installed directly.
+* **Wallet**: Enables full or partial order payment via credit account. Includes REST-Api. (commercial plugin)
+* **[Liquid](https://github.com/Shopify/liquid/wiki/Liquid-for-Designers) template engine**: very flexible templating for e-mails and campaigns with autocompletion and syntax highlighting.
+* **Cash Rounding**: define money rounding rules on currency and payment method level.
+* **Modern, responsive backend**: migrated backend to Bootstrap 4, overhauled and improved the user interface.
+* **Enhanced MegaMenu**: virtual dropdowns for surplus top-level categories and brands (commercial plugin exclusively bundled with Pro Edition).
+* **RTL**: comprehensive RTL (Right-to-left) and bidi(rectional) support.
* **Amazon Pay**:
* Supports merchants registered in the USA and Japan
* External authentication via *Login with Amazon* button in shop frontend
* Several improvements through the new *Login and pay with Amazon* services
+* (Perf) **Faster image processing**: new processing and caching strategy! Thumbnails are not created synchronously during the main request anymore, instead a new middleware route defers processing until an image is requested by any client.
+* **TinyImage**: scores ultra-high image compression rates (up to 80 %!) and enables WebP support (commercial plugin exclusively bundled with Premium Edition).
+* **UrlRewriter**: define URL redirection rules in the backend using *mod_rewrite* notation. (commercial plugin)
+* **Address formatting** templates by country
+* **Language packs**: downloader & auto-importer for packages available online.
+
+### Breaking changes
+* Message template customizations are lost due to the new template engine. You have to customize the templates again. No automatic migration, sorry :-(
+* Amazon Pay: The plugin has been changed to new *Login and pay with Amazon* services. The client ID has been added, which has to be created in Amazon Seller Central and saved in the payment method configuration.
+* (Dev) Calls to cache methods `Keys()` and `RemoveByPattern()` require glob chars to be present now (supported glob-styles see [https://redis.io/commands/keys](https://redis.io/commands/keys)). Previously these methods appended `*` to the passed pattern, which made pattern matching rather unflexible.
+* (Dev) Hook framework now passes `IHookedEntity` interface instead of `HookedEntity` class
+* (Dev) Completely removed all `EntityInserted`, `EntityUpdated` and `EntityDeleted` legacy events. We were using DbSaveHooks anyway, which provides a much more powerful and way faster pub-sub mechanism for database operations.
### New Features
* 1203 MegaMenu shrinker and *Brands* virtual menu item
+* [Summernote](https://summernote.org/) is now the primary HTML editor
* #431 Added option to randomize the display order for slides on each request
* #1258 Add option to filter shipping and payment methods by a specific customer role
* #1247 Allow to import non system customer roles in customer import
* #1117 Added an option to display a dropdown menu for manufacturers
* #1203 Added an option to define a maximum number of elements in the main menu for the first hierarchy of the catalog navigation
+* GMC: column chooser for edit grid
* #1100 Customer can register in frontend via "Login with Amazon" button
* **Web API**:
* #1292 Added endpoint to get order in PDF format
@@ -38,12 +324,17 @@
* XML and CSV export of shopping cart and wishlist items
* #1363 Make storing of IP addresses optional
* #729 Option for automatic order amount capturing when the shipping status changed to "shipped"
+* (Dev) ILocalizationFileResolver: responsible for finding localization files for client scripts
+* #998 GMC: Find a way to map attribute combination values to feed export values
+* Added Instagram icon to social media icons in footer
### Improvements
* Target .NET Framework changed: 4.5.2 > 4.6.1.
* Lower memory consumption
* #649 Media FileSystem provider: segmenting files in subfolders to increase IO perf with huge amount of files
-* #1141 Clearer backend order list. Added more infos like payment method.
+* #1141 Cleaner backend order list. Added more infos like payment method.
+* OuputCache: Simple product changes that affect visibility now correctly invalidate all assigned category and manufacturer pages
+* * OuputCache: When MegaSearch is active, invalidation occurs only during indexing and not ad-hoc anymore.
* #1248 New payment integration guidelines for Sofort\Klarna
* TwitterAuth: better error handling and enhanced admin instruction
* #1181 Debitoor: Add option to display shipping address on invoices
@@ -55,6 +346,10 @@
* #1143 Make shipping methods suitable for multi-stores
* #1320 Image import: Find out the content type of image URLs by response header rather than file extension (which is sometimes missing)
* #1219 Recently viewed products list should respect setting to hide manufacturer names
+* Import and export product quantity step
+* Add bundle item information to order messages
+* #1031 Enable offline payment methods to have brand icons
+* DevTools Plugin: Added example for cached output invalidation
### Bugfixes
* #1268 Data importer always inserts new pictures and does not detect equal pictures while importing
@@ -74,7 +369,9 @@
* The tax value per tax rate was not updated when adding\removing a product to\from the order.
* The option to send manually was ignored when sending e-mails
* #528 LimitedToStores is required on payment provider rather than plugin level
-
+* #1318 Disabled preselected attribute combination permanently hides the shopping cart button, even if another combination is selected.
+* Copy product: Fixes "Cannot insert duplicate key row in object dbo.UrlRecord with unique index IX_UrlRecord_Slug"
+* Fixed export publishing via email
## SmartStore.NET 3.0.3
### Bugfixes
@@ -141,7 +438,9 @@
* MegaSearch: Localized labels of filters were never displayed
* #1195 Exporter: don't send an email if no email account has been selected
* Product lists sometimes show the wrong delivery time
-* #1192 Lucene indexing performance decreases the longer it takes
+* #1192 Lucene indexing
+*
+* decreases the longer it takes
* #1198 MegaSearch: never sort numeric range by label, always by value
* Filter for attributes were always sorted by hit count
* #1200 PayPal PLUS: Invalid request if the order amount is zero
@@ -463,7 +762,8 @@
* #495 Implement option to search product detail description by default
### Improvements
-* (Perf) Implemented static caches for URL aliases and localized properties. Increases app startup and request speed by up to 30%.
+* (Perf) Implemented static caches for URL aliases and localized properties. Increases
+* and request speed by up to 30%.
* (Perf) Significantly reduced number of database reads during product list rendering. Increases request speed by up to 10%.
* (Perf) Implemented 2nd level cache for infrequently changed entities. Increases request speed by up to 10%.
* TaskScheduler: Rewritten from scratch to be suitable for Web Farms
@@ -1083,7 +1383,7 @@
* [SMNET-202] - SmartTabSelection mit verschachtelten Tabs fehlerhaft nach Reload einer Seite
### Improvement
-
+
* [SMNET-13] - Attributwerte: der Text "Aufpreis" muss um "Minderpreis" erweitert werden.
* [SMNET-15] - Umgestaltung der Darstellung der Staffelpreise (Popover ab dem fünften Element)
* [SMNET-30] - MessageTemplates teilweise auf Englisch
diff --git a/lib/SmartStore.Licensing/SmartStore.Licensing.dll b/lib/SmartStore.Licensing/SmartStore.Licensing.dll
index 2c82c7853f..17bf4e2ad9 100644
Binary files a/lib/SmartStore.Licensing/SmartStore.Licensing.dll and b/lib/SmartStore.Licensing/SmartStore.Licensing.dll differ
diff --git a/src/AssemblySharedInfo.cs b/src/AssemblySharedInfo.cs
index 79225de4c7..b58492422f 100644
--- a/src/AssemblySharedInfo.cs
+++ b/src/AssemblySharedInfo.cs
@@ -7,6 +7,6 @@
[assembly: AssemblyDescription("SmartStore.NET")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("SmartStore AG")]
-[assembly: AssemblyCopyright("Copyright © SmartStore AG 2016")]
+[assembly: AssemblyCopyright("Copyright © SmartStore AG 2019")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
diff --git a/src/AssemblyVersionInfo.cs b/src/AssemblyVersionInfo.cs
index 63fae0f3e7..719cd1d6ff 100644
--- a/src/AssemblyVersionInfo.cs
+++ b/src/AssemblyVersionInfo.cs
@@ -9,7 +9,7 @@
//
// You can specify all the values or you can default the Revision and Build Numbers
// by using the '*' as shown below:
-[assembly: AssemblyVersion("3.0.3.0")]
+[assembly: AssemblyVersion("3.2.2.0")]
-[assembly: AssemblyFileVersion("3.0.3.0")]
-[assembly: AssemblyInformationalVersion("3.0.3.0")]
+[assembly: AssemblyFileVersion("3.2.2.0")]
+[assembly: AssemblyInformationalVersion("3.2.2.0")]
diff --git a/src/Libraries/SmartStore.Core/Async/AsyncLock.cs b/src/Libraries/SmartStore.Core/Async/AsyncLock.cs
deleted file mode 100644
index b93da92afc..0000000000
--- a/src/Libraries/SmartStore.Core/Async/AsyncLock.cs
+++ /dev/null
@@ -1,110 +0,0 @@
-using System;
-using System.Collections.Concurrent;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
-using SmartStore.Utilities;
-
-namespace SmartStore.Core.Async
-{
- public sealed class AsyncLock : DisposableObject
- {
- private static readonly TimeSpan _noTimeout = TimeSpan.FromMilliseconds(-1);
- private static readonly ConcurrentDictionary _keyLocks = new ConcurrentDictionary();
-
- private readonly SemaphoreSlim _semaphore;
- private readonly int _initialCount;
-
- public AsyncLock()
- : this(1, int.MaxValue)
- {
- }
-
- public AsyncLock(int initialCount)
- : this(initialCount, int.MaxValue)
- {
- }
-
- public AsyncLock(int initialCount, int maxCount)
- {
- _semaphore = new SemaphoreSlim(initialCount, maxCount);
- _initialCount = initialCount;
- }
-
- public static AsyncLock Acquire(object key)
- {
- Guard.NotNull(key, nameof(key));
-
- var asyncLock = _keyLocks.GetOrAdd(key, x => new AsyncLock());
-
- // Perf: once we have more than 100 lock objects in the storage, remove the idle ones.
- if (_keyLocks.Count > 100)
- {
- // never remove the current one and the locked ones
- var idles = _keyLocks.Where(kvp => kvp.Key != key && !kvp.Value.IsLockHeld).ToArray();
- foreach (var kvp in idles)
- {
- var value = kvp.Value;
- // double check for held lock
- if (!value.IsLockHeld)
- {
- _keyLocks.TryRemove(kvp.Key, out value);
- }
- }
- }
-
- return asyncLock;
- }
-
- public bool IsLockHeld
- {
- get { return _semaphore.CurrentCount < _initialCount; }
- }
-
- public int Release()
- {
- return _semaphore.Release();
- }
-
- public int Release(int releaseCount)
- {
- return _semaphore.Release(releaseCount);
- }
-
- public Task LockAsync()
- {
- return LockAsync(_noTimeout, CancellationToken.None);
- }
-
- public Task LockAsync(TimeSpan timeout)
- {
- return LockAsync(timeout, CancellationToken.None);
- }
-
- public Task LockAsync(CancellationToken cancellationToken)
- {
- return LockAsync(_noTimeout, cancellationToken);
- }
-
- public Task LockAsync(TimeSpan timeout, CancellationToken cancellationToken)
- {
- var wait = _semaphore.WaitAsync(timeout, cancellationToken);
-
- return wait.IsCompleted
- ? Task.FromResult(new ActionDisposable(() => _semaphore.Release()) as IDisposable)
- : wait.ContinueWith((t, s) => new ActionDisposable(() => ((SemaphoreSlim)s).Release()) as IDisposable,
- _semaphore,
- CancellationToken.None,
- TaskContinuationOptions.ExecuteSynchronously,
- TaskScheduler.Default);
- }
-
- protected override void OnDispose(bool disposing)
- {
- if (disposing)
- _semaphore.Dispose();
- }
- }
-}
diff --git a/src/Libraries/SmartStore.Core/Async/AsyncRunner.cs b/src/Libraries/SmartStore.Core/Async/AsyncRunner.cs
index 1fce2cc436..2751259eef 100644
--- a/src/Libraries/SmartStore.Core/Async/AsyncRunner.cs
+++ b/src/Libraries/SmartStore.Core/Async/AsyncRunner.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
@@ -8,113 +9,166 @@
namespace SmartStore.Core.Async
{
- public static class AsyncRunner
+ using EventQueue = ConcurrentQueue>;
+
+ public class AsyncRunner : IDisposable
{
- private static readonly BackgroundWorkHost _host = new BackgroundWorkHost();
+ #region Instance members
- ///
- /// Gets the global cancellation token which signals the application shutdown
- ///
- public static CancellationToken AppShutdownCancellationToken
+ private ExclusiveSynchronizationContext CurrentContext;
+ private SynchronizationContext OldContext;
+ private int TaskCount;
+
+ private AsyncRunner()
{
- get { return _host.ShutdownCancellationTokenSource.Token; }
+ OldContext = SynchronizationContext.Current;
+ CurrentContext = new ExclusiveSynchronizationContext(OldContext);
+ SynchronizationContext.SetSynchronizationContext(CurrentContext);
+ }
+
+ public void Dispose()
+ {
+ try
+ {
+ CurrentContext.BeginMessageLoop();
+ }
+ catch (Exception e)
+ {
+ throw e;
+ }
+ finally
+ {
+ SynchronizationContext.SetSynchronizationContext(OldContext);
+ }
+ }
+
+ private void Increment()
+ {
+ Interlocked.Increment(ref TaskCount);
+ }
+
+ private void Decrement()
+ {
+ Interlocked.Decrement(ref TaskCount);
+ if (TaskCount == 0)
+ {
+ CurrentContext.EndMessageLoop();
+ }
}
///
/// Executes an async Task method which has a void return value synchronously
///
- /// Task method to execute
- public static void RunSync(Func func)
+ /// Task execute
+ public void Run(Task task, Action continuation = null)
{
- var oldContext = SynchronizationContext.Current;
- var synch = new ExclusiveSynchronizationContext();
- SynchronizationContext.SetSynchronizationContext(synch);
- synch.Post(async _ =>
+ CurrentContext.Post(async _ =>
{
try
{
- await func();
+ Increment();
+ await task;
+
+ continuation?.Invoke(task);
}
catch (Exception e)
{
- synch.InnerException = e;
- throw;
+ CurrentContext.InnerException = e;
}
finally
{
- synch.EndMessageLoop();
+ Decrement();
}
}, null);
- synch.BeginMessageLoop();
-
- SynchronizationContext.SetSynchronizationContext(oldContext);
}
///
/// Executes an async Task method which has a TResult return type synchronously
///
/// Return Type
- /// Task method to execute
- ///
- public static TResult RunSync(Func> func)
+ /// Task to execute
+ public TResult Run(Task task, Action> continuation = null)
{
- var oldContext = SynchronizationContext.Current;
- var synch = new ExclusiveSynchronizationContext();
- SynchronizationContext.SetSynchronizationContext(synch);
- TResult ret = default(TResult);
- synch.Post(async _ =>
+ var result = default(TResult);
+
+ CurrentContext.Post(async _ =>
{
try
{
- ret = await func();
+ Increment();
+ result = await task;
+
+ continuation?.Invoke(task);
}
catch (Exception e)
{
- synch.InnerException = e;
- throw;
+ CurrentContext.InnerException = e;
}
finally
{
- synch.EndMessageLoop();
+ Decrement();
}
}, null);
- synch.BeginMessageLoop();
- SynchronizationContext.SetSynchronizationContext(oldContext);
- return ret;
- }
- public static Task Run(Action action)
- {
- return Run(action, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default);
+ return result;
}
- public static Task Run(Action action, CancellationToken cancellationToken)
+ #endregion
+
+ private static readonly BackgroundWorkHost _host = new BackgroundWorkHost();
+
+ ///
+ /// Gets the global cancellation token which signals the application shutdown
+ ///
+ public static CancellationToken AppShutdownCancellationToken
{
- return Run(action, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default);
+ get { return _host.ShutdownCancellationTokenSource.Token; }
}
- public static Task Run(Action action, TaskCreationOptions options)
+ public static AsyncRunner Create()
{
- return Run(action, CancellationToken.None, options, TaskScheduler.Default);
+ return new AsyncRunner();
}
- public static Task Run(Action action, CancellationToken cancellationToken, TaskCreationOptions options)
+ ///
+ /// Executes an async Task method which has a void return value synchronously
+ ///
+ /// Task method to execute
+ public static void RunSync(Func func, Action continuation = null)
{
- return Run(action, cancellationToken, options, TaskScheduler.Default);
+ using (var runner = new AsyncRunner())
+ {
+ runner.Run(func(), continuation);
+ }
}
- public static Task Run(Action action, TaskScheduler scheduler)
+ ///
+ /// Executes an async Task method which has a TResult return type synchronously
+ ///
+ /// Return Type
+ /// Task method to execute
+ ///
+ public static TResult RunSync(Func> func, Action> continuation = null)
{
- return Run(action, CancellationToken.None, TaskCreationOptions.None, scheduler);
+ var result = default(TResult);
+
+ using (var runner = new AsyncRunner())
+ {
+ result = runner.Run(func(), continuation);
+ }
+
+ return result;
}
- public static Task Run(Action action, CancellationToken cancellationToken, TaskCreationOptions options, TaskScheduler scheduler)
+ public static Task Run(
+ Action action,
+ CancellationToken cancellationToken = default(CancellationToken),
+ TaskCreationOptions options = TaskCreationOptions.LongRunning,
+ TaskScheduler scheduler = null)
{
Guard.NotNull(action, nameof(action));
- Guard.NotNull(scheduler, nameof(scheduler));
var ct = _host.CreateCompositeCancellationTokenSource(cancellationToken).Token;
- options |= TaskCreationOptions.LongRunning; // enforce an exclusive thread (not from pool)
var t = Task.Factory.StartNew(() => {
var accessor = EngineContext.Current.ContainerManager.ScopeAccessor;
@@ -122,21 +176,24 @@ public static Task Run(Action action, Cancell
{
action(accessor.GetLifetimeScope(null), ct);
}
- }, ct, options, scheduler);
+ }, ct, options, scheduler ?? TaskScheduler.Default);
_host.Register(t, ct);
return t;
}
- public static Task Run(Action action, object state, CancellationToken cancellationToken, TaskCreationOptions options, TaskScheduler scheduler)
+ public static Task Run(
+ Action action,
+ object state,
+ CancellationToken cancellationToken = default(CancellationToken),
+ TaskCreationOptions options = TaskCreationOptions.LongRunning,
+ TaskScheduler scheduler = null)
{
Guard.NotNull(state, nameof(state));
Guard.NotNull(action, nameof(action));
- Guard.NotNull(scheduler, nameof(scheduler));
var ct = _host.CreateCompositeCancellationTokenSource(cancellationToken).Token;
- options |= TaskCreationOptions.LongRunning; // enforce an exclusive thread (not from pool)
var t = Task.Factory.StartNew((o) =>
{
@@ -145,47 +202,22 @@ public static Task Run(Action action,
{
action(accessor.GetLifetimeScope(null), ct, o);
}
- }, state, ct, options, scheduler);
+ }, state, ct, options, scheduler ?? TaskScheduler.Default);
_host.Register(t, ct);
return t;
}
-
-
- public static Task Run(Func function)
- {
- return Run(function, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default);
- }
-
- public static Task Run(Func function, CancellationToken cancellationToken)
- {
- return Run(function, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default);
- }
-
- public static Task Run(Func function, TaskCreationOptions options)
- {
- return Run(function, CancellationToken.None, options, TaskScheduler.Default);
- }
-
- public static Task Run(Func function, CancellationToken cancellationToken, TaskCreationOptions options)
- {
- return Run(function, cancellationToken, options, TaskScheduler.Default);
- }
-
- public static Task Run(Func function, TaskScheduler scheduler)
- {
- return Run(function, CancellationToken.None, TaskCreationOptions.None, scheduler);
- }
-
- public static Task Run(Func function, CancellationToken cancellationToken, TaskCreationOptions options, TaskScheduler scheduler)
+ public static Task Run(
+ Func function,
+ CancellationToken cancellationToken = default(CancellationToken),
+ TaskCreationOptions options = TaskCreationOptions.LongRunning,
+ TaskScheduler scheduler = null)
{
Guard.NotNull(function, nameof(function));
- Guard.NotNull(scheduler, nameof(scheduler));
var ct = _host.CreateCompositeCancellationTokenSource(cancellationToken).Token;
- options |= TaskCreationOptions.LongRunning; // enforce an exclusive thread (not from pool)
var t = Task.Factory.StartNew(() =>
{
@@ -194,21 +226,24 @@ public static Task Run(Func Run(Func function, object state, CancellationToken cancellationToken, TaskCreationOptions options, TaskScheduler scheduler)
+ public static Task Run(
+ Func function,
+ object state,
+ CancellationToken cancellationToken = default(CancellationToken),
+ TaskCreationOptions options = TaskCreationOptions.LongRunning,
+ TaskScheduler scheduler = null)
{
Guard.NotNull(state, nameof(state));
Guard.NotNull(function, nameof(function));
- Guard.NotNull(scheduler, nameof(scheduler));
var ct = _host.CreateCompositeCancellationTokenSource(cancellationToken).Token;
- options |= TaskCreationOptions.LongRunning; // enforce an exclusive thread (not from pool)
var t = Task.Factory.StartNew((o) =>
{
@@ -217,19 +252,61 @@ public static Task Run(Func function,
+ object state,
+ CancellationToken cancellationToken = default(CancellationToken))
+ {
+ Guard.NotNull(state, nameof(state));
+ Guard.NotNull(function, nameof(function));
+
+ var ct = _host.CreateCompositeCancellationTokenSource(cancellationToken).Token;
+
+ var accessor = EngineContext.Current.ContainerManager.ScopeAccessor;
+ var scope = accessor.BeginLifetimeScope(null);
+
+ Task task = null;
+
+ try
+ {
+ task = function(scope, ct).ContinueWith(x =>
+ {
+ scope.Dispose();
+ });
+ _host.Register(task, ct);
+ }
+ catch
+ {
+ scope.Dispose();
+ }
+
+ return task;
+ }
private class ExclusiveSynchronizationContext : SynchronizationContext
{
private bool _done;
- readonly AutoResetEvent _workItemsWaiting = new AutoResetEvent(false);
- readonly Queue> _items = new Queue>();
+ private readonly AutoResetEvent _workItemsWaiting = new AutoResetEvent(false);
+ private readonly EventQueue _items;
+
+ public ExclusiveSynchronizationContext(SynchronizationContext old)
+ {
+ if (old is ExclusiveSynchronizationContext oldEx)
+ {
+ this._items = oldEx._items;
+ }
+ else
+ {
+ this._items = new EventQueue();
+ }
+ }
public Exception InnerException { get; set; }
@@ -240,10 +317,7 @@ public override void Send(SendOrPostCallback d, object state)
public override void Post(SendOrPostCallback d, object state)
{
- lock (_items)
- {
- _items.Enqueue(Tuple.Create(d, state));
- }
+ _items.Enqueue(Tuple.Create(d, state));
_workItemsWaiting.Set();
}
@@ -256,26 +330,19 @@ public void BeginMessageLoop()
{
while (!_done)
{
- Tuple task = null;
- lock (_items)
+ if (!_items.TryDequeue(out var task))
{
- if (_items.Count > 0)
- {
- task = _items.Dequeue();
- }
+ _workItemsWaiting.WaitOne();
}
- if (task != null)
+ else
{
task.Item1(task.Item2);
- if (InnerException != null) // the method threw an exeption
+
+ if (InnerException != null) // method threw an exeption
{
throw new AggregateException("AsyncHelpers.Run method threw an exception.", InnerException);
}
}
- else
- {
- _workItemsWaiting.WaitOne();
- }
}
}
@@ -291,7 +358,7 @@ internal class BackgroundWorkHost : IRegisteredObject
{
private readonly CancellationTokenSource _shutdownCancellationTokenSource = new CancellationTokenSource();
private int _numRunningWorkItems;
-
+
public BackgroundWorkHost()
{
HostingEnvironment.RegisterObject(this);
@@ -337,7 +404,7 @@ public void Register(Task work, CancellationToken cancellationToken)
}
_numRunningWorkItems++;
}
-
+
work.ContinueWith(
WorkItemComplete,
CancellationToken.None,
@@ -353,7 +420,7 @@ private void WorkItemComplete(Task work)
lock (this)
{
num = --_numRunningWorkItems;
- isCancellationRequested = _shutdownCancellationTokenSource.IsCancellationRequested;
+ isCancellationRequested = _shutdownCancellationTokenSource.IsCancellationRequested;
}
if (num == 0 && isCancellationRequested)
{
diff --git a/src/Libraries/SmartStore.Core/BaseEntity.cs b/src/Libraries/SmartStore.Core/BaseEntity.cs
index c649970d6d..3fd0016909 100644
--- a/src/Libraries/SmartStore.Core/BaseEntity.cs
+++ b/src/Libraries/SmartStore.Core/BaseEntity.cs
@@ -1,13 +1,13 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
+using System.Data.Entity.Core.Objects;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.Serialization;
namespace SmartStore.Core
-{
-
+{
///
/// Base class for entities
///
@@ -21,18 +21,27 @@ public abstract partial class BaseEntity : IEquatable
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
- [SuppressMessage("ReSharper", "PossibleNullReferenceException")]
+ [SuppressMessage("ReSharper", "PossibleNullReferenceException")]
+ public virtual string GetEntityName()
+ {
+ return GetUnproxiedType().Name;
+ }
+
public Type GetUnproxiedType()
{
- var t = GetType();
- if (t.AssemblyQualifiedName.StartsWith("System.Data.Entity."))
- {
- // it's a proxied type
- t = t.BaseType;
- }
+ #region Old
+ //var t = GetType();
+ //if (t.AssemblyQualifiedName.StartsWith("System.Data.Entity."))
+ //{
+ // // it's a proxied type
+ // t = t.BaseType;
+ //}
- return t;
- }
+ //return t;
+ #endregion
+
+ return ObjectContext.GetObjectType(GetType());
+ }
///
/// Transient objects are not associated with an item already in storage. For instance,
diff --git a/src/Libraries/SmartStore.Core/Caching/CacheScope.cs b/src/Libraries/SmartStore.Core/Caching/CacheScope.cs
new file mode 100644
index 0000000000..b705ab10b6
--- /dev/null
+++ b/src/Libraries/SmartStore.Core/Caching/CacheScope.cs
@@ -0,0 +1,41 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace SmartStore.Core.Caching
+{
+ public class CacheScope
+ {
+ private HashSet _dependencies;
+
+ public CacheScope(string key)
+ {
+ Key = key;
+ }
+
+ public string Key { get; private set; }
+
+ public void AddDependency(string key)
+ {
+ if (key == null)
+ {
+ return;
+ }
+
+ if (_dependencies == null)
+ {
+ _dependencies = new HashSet();
+ }
+
+ _dependencies.Add(key);
+ }
+
+ public IEnumerable Dependencies
+ {
+ get
+ {
+ return _dependencies ?? Enumerable.Empty();
+ }
+ }
+ }
+}
diff --git a/src/Libraries/SmartStore.Core/Caching/CacheScopeAccessor.cs b/src/Libraries/SmartStore.Core/Caching/CacheScopeAccessor.cs
new file mode 100644
index 0000000000..b04e0a7d45
--- /dev/null
+++ b/src/Libraries/SmartStore.Core/Caching/CacheScopeAccessor.cs
@@ -0,0 +1,62 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using SmartStore.Utilities;
+
+namespace SmartStore.Core.Caching
+{
+ public class CacheScopeAccessor : ICacheScopeAccessor
+ {
+ private readonly Stack _stack = new Stack();
+
+ public CacheScopeAccessor()
+ {
+ }
+
+ public CacheScope Current
+ {
+ get
+ {
+ _stack.TryPeek(out var result);
+ return result;
+ }
+ }
+
+ public bool HasScope(string key)
+ {
+ return _stack.Any(x => x.Key.IsCaseInsensitiveEqual(key));
+ }
+
+ public void PropagateKey(string key)
+ {
+ var current = Current;
+ if (current != null)
+ {
+ current.AddDependency(key);
+ }
+ }
+
+ public IDisposable BeginScope(string key, bool independent = false)
+ {
+ if (Current?.Key == key)
+ {
+ // Void if parent scope has the same key.
+ return ActionDisposable.Empty;
+ }
+
+ _stack.Push(new CacheScope(key));
+
+ var action = new ActionDisposable(() =>
+ {
+ _stack.TryPop(out _);
+
+ if (!independent)
+ {
+ PropagateKey(key);
+ }
+ });
+
+ return action;
+ }
+ }
+}
diff --git a/src/Libraries/SmartStore.Core/Caching/ICacheManager.cs b/src/Libraries/SmartStore.Core/Caching/ICacheManager.cs
index c28fc3d0ff..855349106f 100644
--- a/src/Libraries/SmartStore.Core/Caching/ICacheManager.cs
+++ b/src/Libraries/SmartStore.Core/Caching/ICacheManager.cs
@@ -14,8 +14,9 @@ public interface ICacheManager
///
/// The type of the item to get
/// The cache item key
+ /// When true , no attemp will be made to invalidate depending/parent cache entries.
/// Cached item value or null if item with specified key does not exist in the cache
- T Get(string key);
+ T Get(string key, bool independent = false);
///
/// Gets a cache item associated with the specified key or adds the item
@@ -25,8 +26,9 @@ public interface ICacheManager
/// The cache item key
/// Func which returns value to be added to the cache
/// Absolute expiration time
+ /// When true , no attemp will be made to invalidate depending/parent cache entries.
/// Cached item value
- T Get(string key, Func acquirer, TimeSpan? duration = null);
+ T Get(string key, Func acquirer, TimeSpan? duration = null, bool independent = false);
///
/// Gets a cache item associated with the specified key or - if it doesn't exist in the cache -
@@ -36,16 +38,18 @@ public interface ICacheManager
/// The cache item key
/// Func which returns value to be added to the cache
/// Absolute expiration time
+ /// When true , no attemp will be made to invalidate depending/parent cache entries.
/// Cached item value
- Task GetAsync(string key, Func> acquirer, TimeSpan? duration = null);
+ Task GetAsync(string key, Func> acquirer, TimeSpan? duration = null, bool independent = false);
///
/// Gets a hashset associated with the specified key or. If key does not exist,
/// a new set is created and put to cache automatically
///
/// The set cache item key
+ /// Optional acquirer callback that is invoked when requested set does not exist yet.
/// Cached item value
- ISet GetHashSet(string key);
+ ISet GetHashSet(string key, Func> acquirer = null);
///
/// Adds a cache item with the specified key
@@ -53,7 +57,10 @@ public interface ICacheManager
/// Key
/// Value
/// Absolute expiration time
- void Put(string key, object value, TimeSpan? duration = null);
+ ///
+ /// A list of keys for cache entries that should be removed from the cache whenever this entry changes.
+ ///
+ void Put(string key, object value, TimeSpan? duration = null, IEnumerable dependencies = null);
///
/// Gets a value indicating whether the value associated with the specified key is cached
diff --git a/src/Libraries/SmartStore.Core/Caching/ICacheScopeAccessor.cs b/src/Libraries/SmartStore.Core/Caching/ICacheScopeAccessor.cs
new file mode 100644
index 0000000000..a640e6b200
--- /dev/null
+++ b/src/Libraries/SmartStore.Core/Caching/ICacheScopeAccessor.cs
@@ -0,0 +1,34 @@
+using System;
+
+namespace SmartStore.Core.Caching
+{
+ public interface ICacheScopeAccessor
+ {
+ ///
+ /// Returns the current cache acquire scope (for current thread)
+ ///
+ CacheScope Current { get; }
+
+ ///
+ /// Whether any scope with the passed exists already. Used to prevent lock recursions.
+ ///
+ /// Key
+ bool HasScope(string key);
+
+ ///
+ /// Notifies the current scope about a cache dependency.
+ ///
+ ///
+ void PropagateKey(string key);
+
+ ///
+ /// Begins a new cache acquire scope.
+ ///
+ /// Key
+ ///
+ /// true means that no key propagation will be performed, making the current cache entry independent from its parent.
+ /// Meaning: parent cache entries will not be auto-invalidated when this entry changes.
+ ///
+ IDisposable BeginScope(string key, bool independent = false);
+ }
+}
diff --git a/src/Libraries/SmartStore.Core/Caching/ISet.cs b/src/Libraries/SmartStore.Core/Caching/ISet.cs
index 59decb5d52..d0bff72bd7 100644
--- a/src/Libraries/SmartStore.Core/Caching/ISet.cs
+++ b/src/Libraries/SmartStore.Core/Caching/ISet.cs
@@ -6,6 +6,7 @@ namespace SmartStore.Core.Caching
public interface ISet : IEnumerable
{
bool Add(string item);
+ void AddRange(IEnumerable items);
void Clear();
bool Contains(string item);
bool Remove(string item);
diff --git a/src/Libraries/SmartStore.Core/Caching/MemoryCacheManager.cs b/src/Libraries/SmartStore.Core/Caching/MemoryCacheManager.cs
index 89a2c707e6..a00224784d 100644
--- a/src/Libraries/SmartStore.Core/Caching/MemoryCacheManager.cs
+++ b/src/Libraries/SmartStore.Core/Caching/MemoryCacheManager.cs
@@ -4,27 +4,37 @@
using System.Threading.Tasks;
using System.Collections.Concurrent;
using System.Threading;
-using SmartStore.Core.Async;
-using System.Collections;
using System.Collections.Generic;
using SmartStore.Utilities;
using System.Text.RegularExpressions;
+using System.Diagnostics;
+using SmartStore.Utilities.Threading;
+using SmartStore.Core.Infrastructure.DependencyManagement;
namespace SmartStore.Core.Caching
{
public partial class MemoryCacheManager : DisposableObject, ICacheManager
{
+ const string LockRecursionExceptionMessage = "Acquiring identical cache items recursively is not supported. Key: {0}";
+
// Wwe put a special string into cache if value is null,
// otherwise our 'Contains()' would always return false,
// which is bad if we intentionally wanted to save NULL values.
public const string FakeNull = "__[NULL]__";
- private readonly MemoryCache _cache;
+ private readonly Work _scopeAccessor;
+ private MemoryCache _cache;
private readonly ConcurrentDictionary _keyLocks = new ConcurrentDictionary();
- public MemoryCacheManager()
+ public MemoryCacheManager(Work scopeAccessor)
{
- _cache = new MemoryCache("SmartStore");
+ _scopeAccessor = scopeAccessor;
+ _cache = CreateCache();
+ }
+
+ private MemoryCache CreateCache()
+ {
+ return new MemoryCache("SmartStore");
}
public bool IsDistributedCache
@@ -32,7 +42,7 @@ public bool IsDistributedCache
get { return false; }
}
- private bool TryGet(string key, out T value)
+ private bool TryGet(string key, bool independent, out T value)
{
value = default(T);
@@ -40,6 +50,12 @@ private bool TryGet(string key, out T value)
if (obj != null)
{
+ // Make the parent scope's entry depend on this
+ if (!independent)
+ {
+ _scopeAccessor.Value.PropagateKey(key);
+ }
+
if (obj.Equals(FakeNull))
{
return true;
@@ -52,65 +68,74 @@ private bool TryGet(string key, out T value)
return false;
}
- public T Get(string key)
+ public T Get(string key, bool independent = false)
{
- T value;
- TryGet(key, out value);
-
+ TryGet(key, independent, out T value);
return value;
}
- public T Get(string key, Func acquirer, TimeSpan? duration = null)
+ public T Get(string key, Func acquirer, TimeSpan? duration = null, bool independent = false)
{
- T value;
-
- if (TryGet(key, out value))
+ if (TryGet(key, independent, out T value))
{
return value;
}
- lock (String.Intern(key))
+ if (_scopeAccessor.Value.HasScope(key))
{
- // atomic operation must be outer locked
- if (!TryGet(key, out value))
+ throw new LockRecursionException(LockRecursionExceptionMessage.FormatInvariant(key));
+ }
+
+ // Get the (semaphore) locker specific to this key
+ using (KeyedLock.Lock("cache:" + key, TimeSpan.FromMinutes(1)))
+ {
+ // Atomic operation must be outer locked
+ if (!TryGet(key, independent, out value))
{
- value = acquirer();
- Put(key, value, duration);
- return value;
+ using (_scopeAccessor.Value.BeginScope(key))
+ {
+ value = acquirer();
+ Put(key, value, duration, _scopeAccessor.Value.Current.Dependencies);
+ return value;
+ }
}
}
return value;
}
- public async Task GetAsync(string key, Func> acquirer, TimeSpan? duration = null)
+ public async Task GetAsync(string key, Func> acquirer, TimeSpan? duration = null, bool independent = false)
{
- T value;
-
- if (TryGet(key, out value))
+ if (TryGet(key, independent, out T value))
{
return value;
}
- // get the async (semaphore) locker specific to this key
- var keyLock = AsyncLock.Acquire(key);
+ if (_scopeAccessor.Value.HasScope(key))
+ {
+ throw new LockRecursionException(LockRecursionExceptionMessage.FormatInvariant(key));
+ }
- using (await keyLock.LockAsync())
+ // Get the async (semaphore) locker specific to this key
+ using (await KeyedLock.LockAsync("cache:" + key, TimeSpan.FromMinutes(1)))
{
- if (!TryGet(key, out value))
+ if (!TryGet(key, independent, out value))
{
- value = await acquirer().ConfigureAwait(false);
- Put(key, value, duration);
- return value;
+ using (_scopeAccessor.Value.BeginScope(key))
+ {
+ value = await acquirer();
+ Put(key, value, duration, _scopeAccessor.Value.Current.Dependencies);
+ return value;
+ }
}
}
return value;
}
- public void Put(string key, object value, TimeSpan? duration = null)
+ public void Put(string key, object value, TimeSpan? duration = null, IEnumerable dependencies = null)
{
- _cache.Set(key, value ?? FakeNull, GetCacheItemPolicy(duration));
+ _cache.Set(key, value ?? FakeNull, GetCacheItemPolicy(duration, dependencies));
}
public bool Contains(string key)
@@ -135,39 +160,53 @@ public IEnumerable Keys(string pattern)
}
var wildcard = new Wildcard(pattern, RegexOptions.IgnoreCase);
- return keys.Where(x => wildcard.IsMatch(x));
+ return keys.Where(x => wildcard.IsMatch(x)).ToArray();
}
public int RemoveByPattern(string pattern)
{
- var keysToRemove = Keys(pattern);
- int count = 0;
-
lock (_cache)
{
+ var keysToRemove = Keys(pattern);
+ int count = 0;
+
// lock atomic operation
foreach (string key in keysToRemove)
{
_cache.Remove(key);
count++;
}
- }
- return count;
+ return count;
+ }
}
public void Clear()
{
- RemoveByPattern("*");
- }
+ // Faster way of clearing cache: https://stackoverflow.com/questions/8043381/how-do-i-clear-a-system-runtime-caching-memorycache
+ var oldCache = Interlocked.Exchange(ref _cache, CreateCache());
+ oldCache.Dispose();
+ GC.Collect();
+ }
- public virtual ISet GetHashSet(string key)
+ public virtual ISet GetHashSet(string key, Func> acquirer = null)
{
- var set = Get(key, () => new MemorySet(this));
- return set;
+ var result = Get(key, () =>
+ {
+ var set = new MemorySet(this);
+ var items = acquirer?.Invoke();
+ if (items != null)
+ {
+ set.AddRange(items);
+ }
+
+ return set;
+ });
+
+ return result;
}
- private CacheItemPolicy GetCacheItemPolicy(TimeSpan? duration)
+ private CacheItemPolicy GetCacheItemPolicy(TimeSpan? duration, IEnumerable dependencies)
{
var absoluteExpiration = ObjectCache.InfiniteAbsoluteExpiration;
@@ -175,16 +214,35 @@ private CacheItemPolicy GetCacheItemPolicy(TimeSpan? duration)
{
absoluteExpiration = DateTime.UtcNow + duration.Value;
}
-
+
var cacheItemPolicy = new CacheItemPolicy
{
AbsoluteExpiration = absoluteExpiration,
SlidingExpiration = ObjectCache.NoSlidingExpiration
};
+
+ if (dependencies != null && dependencies.Any())
+ {
+ // INFO: we can only depend on existing items, otherwise this entry will be removed immediately.
+ dependencies = dependencies.Where(x => x != null && _cache.Contains(x));
+ if (dependencies.Any())
+ {
+ cacheItemPolicy.ChangeMonitors.Add(_cache.CreateCacheEntryChangeMonitor(dependencies));
+ }
+ }
+
+ //cacheItemPolicy.RemovedCallback = OnRemoveEntry;
return cacheItemPolicy;
}
+ //private void OnRemoveEntry(CacheEntryRemovedArguments args)
+ //{
+ // if (args.RemovedReason == CacheEntryRemovedReason.ChangeMonitorChanged)
+ // {
+ // Debug.WriteLine("MEMCACHE: remove depending entry '{0}'.".FormatInvariant(args.CacheItem.Key));
+ // }
+ //}
protected override void OnDispose(bool disposing)
{
diff --git a/src/Libraries/SmartStore.Core/Caching/MemorySet.cs b/src/Libraries/SmartStore.Core/Caching/MemorySet.cs
index 51254daa26..c88cacd703 100644
--- a/src/Libraries/SmartStore.Core/Caching/MemorySet.cs
+++ b/src/Libraries/SmartStore.Core/Caching/MemorySet.cs
@@ -16,6 +16,11 @@ public MemorySet(ICacheManager cache)
_cache = cache;
}
+ public void AddRange(IEnumerable items)
+ {
+ base.UnionWith(items);
+ }
+
public bool Move(string destinationKey, string item)
{
if (this.Contains(item))
diff --git a/src/Libraries/SmartStore.Core/Caching/NullCache.cs b/src/Libraries/SmartStore.Core/Caching/NullCache.cs
index ae8d19b285..e75abfd91d 100644
--- a/src/Libraries/SmartStore.Core/Caching/NullCache.cs
+++ b/src/Libraries/SmartStore.Core/Caching/NullCache.cs
@@ -23,12 +23,12 @@ public bool IsDistributedCache
get { return false; }
}
- public T Get(string key)
+ public T Get(string key, bool independent = false)
{
return default(T);
}
- public T Get(string key, Func acquirer, TimeSpan? duration = null)
+ public T Get(string key, Func acquirer, TimeSpan? duration = null, bool independent = false)
{
if (acquirer == null)
{
@@ -37,7 +37,7 @@ public T Get(string key, Func acquirer, TimeSpan? duration = null)
return acquirer();
}
- public Task GetAsync(string key, Func> acquirer, TimeSpan? duration = null)
+ public Task GetAsync(string key, Func> acquirer, TimeSpan? duration = null, bool independent = false)
{
if (acquirer == null)
{
@@ -47,12 +47,12 @@ public Task GetAsync(string key, Func> acquirer, TimeSpan? duratio
}
- public ISet GetHashSet(string key)
+ public ISet GetHashSet(string key, Func> acquirer = null)
{
return new MemorySet(this);
}
- public void Put(string key, object value, TimeSpan? duration = null)
+ public void Put(string key, object value, TimeSpan? duration = null, IEnumerable dependencies = null)
{
}
diff --git a/src/Libraries/SmartStore.Core/Caching/OutputCache/DisplayControl.cs b/src/Libraries/SmartStore.Core/Caching/OutputCache/DisplayControl.cs
index ec6fc43daf..9629fee3c5 100644
--- a/src/Libraries/SmartStore.Core/Caching/OutputCache/DisplayControl.cs
+++ b/src/Libraries/SmartStore.Core/Caching/OutputCache/DisplayControl.cs
@@ -1,57 +1,185 @@
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
+using Autofac;
using SmartStore.Core.Data;
using SmartStore.Core.Domain.Blogs;
using SmartStore.Core.Domain.Catalog;
using SmartStore.Core.Domain.Discounts;
+using SmartStore.Core.Domain.Localization;
using SmartStore.Core.Domain.News;
using SmartStore.Core.Domain.Topics;
using SmartStore.Utilities;
namespace SmartStore.Core.Caching
{
+ public delegate IEnumerable DisplayControlHandler(BaseEntity entity, IComponentContext ctx);
+
public partial class DisplayControl : IDisplayControl
{
- public static readonly HashSet CandidateTypes = new HashSet(new Type[]
+ private static readonly ConcurrentDictionary _handlers = new ConcurrentDictionary
{
- typeof(BlogComment),
- typeof(BlogPost),
- typeof(Category),
- typeof(Manufacturer),
- typeof(Product),
- typeof(ProductBundleItem),
- typeof(ProductPicture),
- typeof(SpecificationAttribute),
- typeof(ProductSpecificationAttribute),
- typeof(SpecificationAttributeOption),
- typeof(ProductVariantAttribute),
- typeof(ProductVariantAttributeValue),
- typeof(ProductVariantAttributeCombination),
- typeof(TierPrice),
- typeof(Discount),
- typeof(CrossSellProduct),
- typeof(RelatedProduct),
- typeof(ProductCategory),
- typeof(ProductManufacturer),
- typeof(NewsItem),
- typeof(NewsComment),
- typeof(Topic)
- });
+ [typeof(BlogComment)] = (x, c) => new[] { "b" + ((BlogComment)x).BlogPostId },
+ [typeof(BlogPost)] = (x, c) => new[] { "b" + x.Id },
+ [typeof(Category)] = (x, c) => new[] { "c" + x.Id },
+ [typeof(Manufacturer)] = (x, c) => new[] { "m" + x.Id },
+ [typeof(ProductBundleItem)] = (x, c) => new[] { "p" + ((ProductBundleItem)x).ProductId },
+ [typeof(ProductPicture)] = (x, c) => new[] { "p" + ((ProductPicture)x).ProductId },
+ [typeof(ProductSpecificationAttribute)] = (x, c) => new[] { "p" + ((ProductSpecificationAttribute)x).ProductId },
+ [typeof(ProductVariantAttributeCombination)]= (x, c) => new[] { "p" + ((ProductVariantAttributeCombination)x).ProductId },
+ [typeof(TierPrice)] = (x, c) => new[] { "p" + ((TierPrice)x).ProductId },
+ [typeof(CrossSellProduct)] = (x, c) => new[] { "p" + ((CrossSellProduct)x).ProductId1, "p" + ((CrossSellProduct)x).ProductId2 },
+ [typeof(RelatedProduct)] = (x, c) => new[] { "p" + ((RelatedProduct)x).ProductId1, "p" + ((RelatedProduct)x).ProductId2 },
+ [typeof(ProductCategory)] = (x, c) => new[] { "p" + ((ProductCategory)x).CategoryId, "p" + ((ProductCategory)x).ProductId },
+ [typeof(ProductManufacturer)] = (x, c) => new[] { "p" + ((ProductManufacturer)x).ManufacturerId, "p" + ((ProductManufacturer)x).ProductId },
+ [typeof(NewsItem)] = (x, c) => new[] { "n" + x.Id },
+ [typeof(NewsComment)] = (x, c) => new[] { "n" + ((NewsComment)x).NewsItemId },
+ [typeof(Topic)] = (x, c) => new[] { "t" + x.Id },
+ [typeof(SpecificationAttributeOption)] = (x, c) => ((SpecificationAttributeOption)x).ProductSpecificationAttributes.Select(y => "p" + y.ProductId),
+ [typeof(ProductTag)] = (x, c) => ((ProductTag)x).Products.Select(y => "p" + y.Id),
+ [typeof(Product)] = HandleProduct,
+ [typeof(SpecificationAttribute)] = HandleSpecificationAttribute,
+ [typeof(ProductVariantAttributeValue)] = HandleProductVariantAttributeValue,
+ [typeof(Discount)] = HandleDiscount,
+ [typeof(LocalizedProperty)] = HandleLocalizedProperty
+ };
private readonly HashSet _entities = new HashSet();
- private readonly Lazy> _rsProductSpecAttr;
+ private readonly IComponentContext _componentContext;
private bool _isIdle;
private bool? _isUncacheableRequest;
- public DisplayControl(Lazy> rsProductSpecAttr)
+ public DisplayControl(IComponentContext componentContext)
+ {
+ _componentContext = componentContext;
+ }
+
+ #region Static
+
+ public static bool ContainsHandlerFor(Type type)
+ {
+ Guard.NotNull(type, nameof(type));
+
+ return _handlers.ContainsKey(type);
+ }
+
+ public static void RegisterHandlerFor(Type type, DisplayControlHandler handler)
+ {
+ Guard.NotNull(type, nameof(type));
+ Guard.NotNull(handler, nameof(handler));
+
+ _handlers.TryAdd(type, handler);
+ }
+
+ #endregion
+
+ #region Handlers
+
+ private static IEnumerable HandleProduct(BaseEntity entity, IComponentContext ctx)
+ {
+ var product = ((Product)entity);
+ yield return "p" + entity.Id;
+ if (product.ProductType == ProductType.GroupedProduct && product.ParentGroupedProductId > 0)
+ {
+ yield return "p" + product.ParentGroupedProductId;
+ }
+ }
+
+ private static IEnumerable HandleSpecificationAttribute(BaseEntity entity, IComponentContext ctx)
+ {
+ // Determine all affected products (which are assigned to this attribute).
+ var specAttrId = ((SpecificationAttribute)entity).Id;
+ var affectedProductIds = ctx.Resolve().Set().AsNoTracking()
+ .Where(x => x.SpecificationAttributeOption.SpecificationAttribute.Id == specAttrId)
+ .Select(x => x.ProductId)
+ .Distinct()
+ .ToList();
+
+ foreach (var id in affectedProductIds)
+ {
+ yield return "p" + id;
+ }
+ }
+
+ private static IEnumerable HandleProductVariantAttributeValue(BaseEntity entity, IComponentContext ctx)
{
- _rsProductSpecAttr = rsProductSpecAttr;
+ var pva = ((ProductVariantAttributeValue)entity).ProductVariantAttribute;
+ if (pva != null)
+ {
+ yield return "p" + pva.ProductId;
+ }
+ }
+
+ private static IEnumerable HandleDiscount(BaseEntity entity, IComponentContext ctx)
+ {
+ var discount = (Discount)entity;
+ if (discount.DiscountType == DiscountType.AssignedToCategories)
+ {
+ foreach (var category in discount.AppliedToCategories)
+ {
+ yield return "c" + category.Id;
+ }
+ }
+ else if (discount.DiscountType == DiscountType.AssignedToSkus)
+ {
+ foreach (var product in discount.AppliedToProducts)
+ {
+ yield return "p" + product.Id;
+ }
+ }
}
+ private static IEnumerable HandleLocalizedProperty(BaseEntity entity, IComponentContext ctx)
+ {
+ var lp = (LocalizedProperty)entity;
+ string prefix = null;
+ BaseEntity targetEntity = null;
+
+ var dbContext = ctx.Resolve();
+
+ switch (lp.LocaleKeyGroup)
+ {
+ case nameof(Product):
+ prefix = "p";
+ break;
+ case nameof(Category):
+ prefix = "c";
+ break;
+ case nameof(Manufacturer):
+ prefix = "m";
+ break;
+ case nameof(Topic):
+ prefix = "t";
+ break;
+ case nameof(SpecificationAttribute):
+ targetEntity = dbContext.Set().Find(lp.EntityId);
+ break;
+ case nameof(SpecificationAttributeOption):
+ targetEntity = dbContext.Set().Find(lp.EntityId);
+ break;
+ case nameof(ProductVariantAttributeValue):
+ targetEntity = dbContext.Set().Find(lp.EntityId);
+ break;
+ }
+
+ if (prefix.HasValue())
+ {
+ yield return prefix + lp.EntityId;
+ }
+ else if (targetEntity != null)
+ {
+ var tags = ctx.Resolve().GetCacheControlTagsFor(targetEntity);
+ foreach (var tag in tags)
+ {
+ yield return tag;
+ }
+ }
+ }
+
+ #endregion
+
public IDisposable BeginIdleScope()
{
_isIdle = true;
@@ -91,157 +219,21 @@ public bool IsUncacheableRequest
public virtual IEnumerable GetCacheControlTagsFor(BaseEntity entity)
{
- Guard.NotNull(entity, nameof(entity));
+ var empty = Enumerable.Empty();
- if (entity.IsTransientRecord())
+ if (entity == null || entity.IsTransientRecord())
{
- yield break;
+ return empty;
}
var type = entity.GetUnproxiedType();
- if (!CandidateTypes.Contains(type))
+ if (!_handlers.TryGetValue(type, out var handler))
{
- yield break;
+ return empty;
}
- if (type == typeof(BlogComment))
- {
- yield return "b" + ((BlogComment)entity).BlogPostId;
- }
- else if (type == typeof(BlogPost))
- {
- yield return "b" + entity.Id;
- }
- else if (type == typeof(Category))
- {
- yield return "c" + entity.Id;
- }
- else if (type == typeof(Manufacturer))
- {
- yield return "m" + entity.Id;
- }
- else if (type == typeof(Product))
- {
- var product = ((Product)entity);
- yield return "p" + entity.Id;
- if (product.ProductType == ProductType.GroupedProduct && product.ParentGroupedProductId > 0)
- {
- yield return "p" + product.ParentGroupedProductId;
- }
- }
- else if (type == typeof(ProductTag))
- {
- var ids = ((ProductTag)entity).Products.Select(x => x.Id);
- foreach (var id in ids)
- {
- yield return "p" + id;
- }
- }
- else if (type == typeof(ProductBundleItem))
- {
- yield return "p" + ((ProductBundleItem)entity).ProductId;
- }
- else if (type == typeof(ProductPicture))
- {
- yield return "p" + ((ProductPicture)entity).ProductId;
- }
- else if (type == typeof(SpecificationAttribute))
- {
- // Determine all affected products (which are assigned to this attribute).
- var specAttrId = ((SpecificationAttribute)entity).Id;
- var affectedProductIds = _rsProductSpecAttr.Value.TableUntracked
- .Where(x => x.SpecificationAttributeOption.SpecificationAttribute.Id == specAttrId)
- .Select(x => x.ProductId)
- .Distinct()
- .ToList();
-
- foreach (var id in affectedProductIds)
- {
- yield return "p" + id;
- }
- }
- else if (type == typeof(ProductSpecificationAttribute))
- {
- yield return "p" + ((ProductSpecificationAttribute)entity).ProductId;
- }
- else if (type == typeof(SpecificationAttributeOption))
- {
- foreach (var attr in ((SpecificationAttributeOption)entity).ProductSpecificationAttributes)
- {
- yield return "p" + attr.ProductId;
- }
- }
- else if (type == typeof(ProductVariantAttribute))
- {
- yield return "p" + ((ProductVariantAttribute)entity).ProductId;
- }
- else if (type == typeof(ProductVariantAttributeValue))
- {
- var pva = ((ProductVariantAttributeValue)entity).ProductVariantAttribute;
- if (pva != null)
- {
- yield return "p" + pva.ProductId;
- }
- }
- else if (type == typeof(ProductVariantAttributeCombination))
- {
- yield return "p" + ((ProductVariantAttributeCombination)entity).ProductId;
- }
- else if (type == typeof(TierPrice))
- {
- yield return "p" + ((TierPrice)entity).ProductId;
- }
- else if (type == typeof(Discount))
- {
- var discount = (Discount)entity;
- if (discount.DiscountType == DiscountType.AssignedToCategories)
- {
- foreach (var category in discount.AppliedToCategories)
- {
- yield return "c" + category.Id;
- }
- }
- else if (discount.DiscountType == DiscountType.AssignedToSkus)
- {
- foreach (var product in discount.AppliedToProducts)
- {
- yield return "p" + product.Id;
- }
- }
- }
- else if (type == typeof(CrossSellProduct))
- {
- yield return "p" + ((CrossSellProduct)entity).ProductId1;
- yield return "p" + ((CrossSellProduct)entity).ProductId2;
- }
- else if (type == typeof(RelatedProduct))
- {
- yield return "p" + ((RelatedProduct)entity).ProductId1;
- yield return "p" + ((RelatedProduct)entity).ProductId2;
- }
- else if (type == typeof(ProductCategory))
- {
- yield return "c" + ((ProductCategory)entity).CategoryId;
- yield return "p" + ((ProductCategory)entity).ProductId;
- }
- else if (type == typeof(ProductManufacturer))
- {
- yield return "m" + ((ProductManufacturer)entity).ManufacturerId;
- yield return "p" + ((ProductManufacturer)entity).ProductId;
- }
- else if (type == typeof(NewsItem))
- {
- yield return "n" + entity.Id;
- }
- else if (type == typeof(NewsComment))
- {
- yield return "n" + ((NewsComment)entity).NewsItemId;
- }
- else if (type == typeof(Topic))
- {
- yield return "t" + entity.Id;
- }
+ return handler.Invoke(entity, _componentContext);
}
public IEnumerable GetAllCacheControlTags()
diff --git a/src/Libraries/SmartStore.Core/Caching/OutputCache/IOutputCacheInvalidationObserver.cs b/src/Libraries/SmartStore.Core/Caching/OutputCache/IOutputCacheInvalidationObserver.cs
index 6542f2a4f6..234b67ced2 100644
--- a/src/Libraries/SmartStore.Core/Caching/OutputCache/IOutputCacheInvalidationObserver.cs
+++ b/src/Libraries/SmartStore.Core/Caching/OutputCache/IOutputCacheInvalidationObserver.cs
@@ -1,11 +1,9 @@
using System;
-using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
-using Autofac;
using SmartStore.Core.Configuration;
-using SmartStore.Core.Data;
+using SmartStore.Core.Data.Hooks;
using SmartStore.Core.Infrastructure.DependencyManagement;
using SmartStore.Utilities;
@@ -14,8 +12,9 @@ namespace SmartStore.Core.Caching
public class ObserveEntityContext
{
public IOutputCacheProvider OutputCacheProvider { get; set; }
+ public IDisplayControl DisplayControl { get; set; }
public BaseEntity Entity { get; set; }
- public EntityState EntityState { get; set; }
+ public IHookedEntity EntityEntry { get; set; }
public bool Handled { get; set; }
public ContainerManager ServiceContainer { get; set; }
}
diff --git a/src/Libraries/SmartStore.Core/Caching/RequestCache.cs b/src/Libraries/SmartStore.Core/Caching/RequestCache.cs
index cb05f3f288..5fb74d2400 100644
--- a/src/Libraries/SmartStore.Core/Caching/RequestCache.cs
+++ b/src/Libraries/SmartStore.Core/Caching/RequestCache.cs
@@ -8,7 +8,7 @@
namespace SmartStore.Core.Caching
{
- public class RequestCache : IRequestCache
+ public class RequestCache : DisposableObject, IRequestCache
{
const string RegionName = "SmartStoreNET:";
@@ -124,5 +124,11 @@ private string BuildKey(string key)
{
return RegionName + key.EmptyNull();
}
+
+ protected override void OnDispose(bool disposing)
+ {
+ if (disposing)
+ Clear();
+ }
}
}
diff --git a/src/Libraries/SmartStore.Core/Collections/ConcurrentMultimap.cs b/src/Libraries/SmartStore.Core/Collections/ConcurrentMultimap.cs
index d9e6c700b6..86d5b00249 100644
--- a/src/Libraries/SmartStore.Core/Collections/ConcurrentMultimap.cs
+++ b/src/Libraries/SmartStore.Core/Collections/ConcurrentMultimap.cs
@@ -3,8 +3,9 @@
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.Linq;
-using System.Linq.Expressions;
using Newtonsoft.Json;
+using SmartStore.Utilities;
+using System.Threading;
namespace SmartStore.Collections
{
@@ -15,40 +16,65 @@ namespace SmartStore.Collections
/// The type of value.
[JsonConverter(typeof(ConcurrentMultiMapConverter))]
[Serializable]
- public class ConcurrentMultimap : IEnumerable>>
+ public class ConcurrentMultimap : IEnumerable>>
{
- private readonly ConcurrentDictionary> _items;
- private static readonly Func, IProducerConsumerCollection> _bagCreator = (col) => new ConcurrentBag(col);
+ private readonly ConcurrentDictionary> _dict;
+ private readonly Func, ICollection> _collectionCreator;
public ConcurrentMultimap()
- : this(null, null)
+ : this(null, null, null)
{
}
public ConcurrentMultimap(IEqualityComparer comparer)
- : this(null, comparer)
+ : this(null, comparer, null)
{
}
- public ConcurrentMultimap(IEnumerable>> items)
- : this(items, null)
+ public ConcurrentMultimap(IEnumerable>> items)
+ : this(items, null, null)
{
// for serialization
}
- public ConcurrentMultimap(IEnumerable>> items, IEqualityComparer comparer)
+ public ConcurrentMultimap(
+ IEnumerable>> items,
+ IEqualityComparer comparer)
+ : this(items, comparer, null)
{
- _items = new ConcurrentDictionary>(comparer ?? EqualityComparer.Default);
+ // for serialization
+ }
+
+ public ConcurrentMultimap(
+ IEnumerable>> items,
+ IEqualityComparer comparer,
+ Func, ICollection> collectionCreator)
+ {
+ _collectionCreator = collectionCreator;
+ _dict = new ConcurrentDictionary>(
+ ConvertItems(items),
+ comparer ?? EqualityComparer.Default);
+ }
+
+ private IEnumerable>> ConvertItems(IEnumerable>> items)
+ {
+ if (items == null)
+ yield break;
- if (items != null)
+ foreach (var item in items)
{
- foreach (var kvp in items)
- {
- _items.TryAdd(kvp.Key, _bagCreator(kvp.Value ?? Enumerable.Empty()));
- }
+ yield return new KeyValuePair>(item.Key, CreateCollection(item.Value));
}
}
+ protected virtual SyncedCollection CreateCollection(IEnumerable values)
+ {
+ var creator = _collectionCreator ?? Multimap.DefaultCollectionCreator;
+ var col = creator(values ?? Enumerable.Empty());
+
+ return col.AsSynchronized();
+ }
+
///
/// Gets the count of groups/keys.
///
@@ -56,7 +82,7 @@ public int Count
{
get
{
- return _items.Keys.Count;
+ return _dict.Keys.Count;
}
}
@@ -67,25 +93,33 @@ public int TotalValueCount
{
get
{
- return this._items.Values.Sum(x => x.Count);
+ return this._dict.Values.Sum(x => x.Count);
}
}
+ ///
+ /// Gets the count of items in the requested group.
+ ///
+ public int ValueCount(TKey key)
+ {
+ if (_dict.TryGetValue(key, out var col))
+ {
+ return col.Count;
+ }
+
+ return 0;
+ }
+
///
/// Gets the collection of values stored under the specified key.
///
/// The key.
- public virtual IProducerConsumerCollection this[TKey key]
+ public virtual SyncedCollection this[TKey key]
{
get
{
- IProducerConsumerCollection value;
- if (!_items.TryGetValue(key, out value))
- {
- _items.TryAdd(key, _bagCreator(Enumerable.Empty()));
- }
-
- return value;
+ GetOrCreateValues(key, null, out var col);
+ return col;
}
}
@@ -94,15 +128,15 @@ public virtual IProducerConsumerCollection this[TKey key]
///
public virtual ICollection Keys
{
- get { return _items.Keys; }
+ get { return _dict.Keys; }
}
///
- /// Gets the collection of collections of values.
+ /// Gets all value collections.
///
- public virtual ICollection> Values
+ public virtual ICollection> Values
{
- get { return _items.Values; }
+ get { return _dict.Values; }
}
///
@@ -112,7 +146,10 @@ public virtual ICollection> Values
/// The value.
public virtual void TryAdd(TKey key, TValue value)
{
- this[key].TryAdd(value);
+ if (!GetOrCreateValues(key, new[] { value }, out var col))
+ {
+ col.Add(value);
+ }
}
///
@@ -124,18 +161,63 @@ public virtual void TryAddRange(TKey key, IEnumerable values)
{
Guard.NotNull(values, nameof(values));
- values.Each(x => this[key].TryAdd(x));
+ if (!GetOrCreateValues(key, values, out var col))
+ {
+ col.AddRange(values);
+ }
+ }
+
+ ///
+ /// Attempts to remove the specified value for the specified key.
+ ///
+ /// The key.
+ /// The value.
+ /// True if such a value existed and was removed; otherwise false .
+ public virtual bool TryRemove(TKey key, TValue value)
+ {
+ if (!_dict.ContainsKey(key))
+ return false;
+
+ var col = _dict[key];
+ var removed = false;
+
+ removed = col.Remove(value);
+ if (col.Count == 0)
+ {
+ _dict.TryRemove(key, out _);
+ }
+
+ return removed;
+ }
+
+ ///
+ /// Attempts to remove a range of values for the specified key.
+ ///
+ /// The key.
+ /// The values to remove from the group.
+ /// True if at least one item in group has been removed; otherwise false .
+ public virtual bool TryRemoveRange(TKey key, IEnumerable values)
+ {
+ Guard.NotNull(values, nameof(values));
+
+ int numRemoved = 0;
+
+ if (_dict.TryGetValue(key, out var col))
+ {
+ numRemoved = col.RemoveRange(values);
+ }
+
+ return numRemoved > 0;
}
///
- /// Attempts to remove all values for the specified key.
+ /// Attempts to remove and return all values for the specified key.
///
/// The key.
/// True if any such values existed; otherwise false .
- public virtual bool TryRemoveAll(TKey key)
+ public virtual bool TryRemoveAll(TKey key, out SyncedCollection collection)
{
- IProducerConsumerCollection collection;
- return _items.TryRemove(key, out collection);
+ return _dict.TryRemove(key, out collection);
}
///
@@ -143,7 +225,7 @@ public virtual bool TryRemoveAll(TKey key)
///
public virtual void Clear()
{
- _items.Clear();
+ _dict.Clear();
}
///
@@ -153,7 +235,7 @@ public virtual void Clear()
/// True if the multimap has one or more values for the specified key, otherwise false .
public virtual bool ContainsKey(TKey key)
{
- return _items.ContainsKey(key);
+ return _dict.ContainsKey(key);
}
///
@@ -164,7 +246,7 @@ public virtual bool ContainsKey(TKey key)
/// True if the multimap contains such a value; otherwise, false .
public virtual bool ContainsValue(TKey key, TValue value)
{
- return _items.ContainsKey(key) && _items[key].Contains(value);
+ return _dict.ContainsKey(key) && _dict[key].Contains(value);
}
///
@@ -180,12 +262,26 @@ IEnumerator IEnumerable.GetEnumerator()
/// Returns an enumerator that iterates through the multimap.
///
/// An object that can be used to iterate through the multimap.
- public virtual IEnumerator>> GetEnumerator()
+ public virtual IEnumerator>> GetEnumerator()
{
- foreach (var pair in _items)
+ foreach (var pair in _dict)
yield return pair;
}
+ private bool GetOrCreateValues(TKey key, IEnumerable initial, out SyncedCollection col)
+ {
+ // Return true when created
+ var created = false;
+
+ col = _dict.GetOrAdd(key, k =>
+ {
+ created = true;
+ return CreateCollection(initial);
+ });
+
+ return created;
+ }
+
#region Static members
public static ConcurrentMultimap CreateFromLookup(ILookup source)
diff --git a/src/Libraries/SmartStore.Core/Collections/MostRecentlyUsedList.cs b/src/Libraries/SmartStore.Core/Collections/MostRecentlyUsedList.cs
deleted file mode 100644
index 6d81d0bc3a..0000000000
--- a/src/Libraries/SmartStore.Core/Collections/MostRecentlyUsedList.cs
+++ /dev/null
@@ -1,101 +0,0 @@
-using System.Collections;
-using System.Collections.Generic;
-using System.Linq;
-
-namespace SmartStore.Collections
-{
- public class MostRecentlyUsedList : IEnumerable
- {
- private readonly int _maxSize;
- private readonly List _mru;
-
- public MostRecentlyUsedList(int maxSize)
- {
- _maxSize = maxSize;
- _mru = new List();
- }
-
- public MostRecentlyUsedList(IEnumerable collection, int maxSize)
- {
- _maxSize = maxSize;
- _mru = collection.ToList();
-
- Normalize();
- }
-
- public MostRecentlyUsedList(string collection, int maxSize)
- {
- _maxSize = maxSize;
- _mru = collection.SplitSafe(Delimiter).Cast().ToList();
-
- Normalize();
- }
-
- public MostRecentlyUsedList(IEnumerable collection, T newItem, int maxSize)
- {
- _maxSize = maxSize;
- _mru = collection.ToList();
-
- Add(newItem);
- }
-
- public MostRecentlyUsedList(string collection, T newItem, int maxSize)
- {
- _maxSize = maxSize;
- _mru = collection.SplitSafe(Delimiter).Cast().ToList();
-
- Add(newItem);
- }
-
- public static string Delimiter { get { return ";"; } }
-
- private void Normalize()
- {
- if (_maxSize >= 0)
- {
- while (_mru.Count > _maxSize)
- _mru.RemoveAt(_mru.Count - 1);
- }
- }
-
- public IEnumerator GetEnumerator()
- {
- return _mru.GetEnumerator();
- }
-
- IEnumerator IEnumerable.GetEnumerator()
- {
- return _mru.GetEnumerator();
- }
-
- public override string ToString()
- {
- return string.Join(Delimiter, _mru);
- }
-
- public T this[int key]
- {
- get
- {
- return _mru[key];
- }
- set
- {
- _mru[key] = value;
- }
- }
-
- public int Count { get { return _mru.Count; } }
-
- public void Add(T item)
- {
- int i = _mru.IndexOf(item);
- if (i > -1)
- _mru.RemoveAt(i);
-
- _mru.Insert(0, item);
-
- Normalize();
- }
- }
-}
diff --git a/src/Libraries/SmartStore.Core/Collections/MultiMap.cs b/src/Libraries/SmartStore.Core/Collections/MultiMap.cs
index 6c8f85fc78..738ffab5fa 100644
--- a/src/Libraries/SmartStore.Core/Collections/MultiMap.cs
+++ b/src/Libraries/SmartStore.Core/Collections/MultiMap.cs
@@ -1,7 +1,6 @@
using System;
using System.Collections;
using System.Collections.Generic;
-using System.Collections.Concurrent;
using System.Linq;
using System.Linq.Expressions;
using Newtonsoft.Json;
@@ -17,11 +16,12 @@ namespace SmartStore.Collections
[Serializable]
public class Multimap : IEnumerable>>
{
- private readonly IDictionary> _items;
+ private readonly IDictionary> _dict;
private readonly Func, ICollection> _collectionCreator;
private readonly bool _isReadonly = false;
- private readonly static Func, ICollection> _defaultCollectionCreator = x => new List(x ?? Enumerable.Empty());
+ internal readonly static Func, ICollection> DefaultCollectionCreator =
+ x => new List(x ?? Enumerable.Empty());
public Multimap()
: this(EqualityComparer.Default)
@@ -30,8 +30,8 @@ public Multimap()
public Multimap(IEqualityComparer comparer)
{
- _items = new Dictionary>(comparer ?? EqualityComparer.Default);
- _collectionCreator = _defaultCollectionCreator;
+ _dict = new Dictionary>(comparer ?? EqualityComparer.Default);
+ _collectionCreator = DefaultCollectionCreator;
}
public Multimap(Func, ICollection> collectionCreator)
@@ -49,7 +49,7 @@ internal Multimap(IDictionary> dictionary, Func> dictionary, bool isRea
{
Guard.NotNull(dictionary, nameof(dictionary));
- _items = dictionary;
+ _dict = dictionary;
if (isReadonly && dictionary != null)
{
@@ -81,21 +81,20 @@ public Multimap(IEnumerable>> items, IEqu
// for serialization
Guard.NotNull(items, nameof(items));
- _items = new Dictionary>(comparer ?? EqualityComparer.Default);
- _collectionCreator = _defaultCollectionCreator;
+ _dict = new Dictionary>(comparer ?? EqualityComparer.Default);
if (items != null)
{
foreach (var kvp in items)
{
- _items[kvp.Key] = CreateCollection(kvp.Value);
+ _dict[kvp.Key] = CreateCollection(kvp.Value);
}
}
}
protected virtual ICollection CreateCollection(IEnumerable values)
{
- return (_collectionCreator ?? _defaultCollectionCreator)(values ?? Enumerable.Empty());
+ return (_collectionCreator ?? DefaultCollectionCreator)(values ?? Enumerable.Empty());
}
///
@@ -105,7 +104,7 @@ public int Count
{
get
{
- return this._items.Keys.Count;
+ return this._dict.Keys.Count;
}
}
@@ -116,7 +115,7 @@ public int TotalValueCount
{
get
{
- return this._items.Values.Sum(x => x.Count);
+ return this._dict.Values.Sum(x => x.Count);
}
}
@@ -128,15 +127,15 @@ public virtual ICollection this[TKey key]
{
get
{
- if (!_items.ContainsKey(key))
+ if (!_dict.ContainsKey(key))
{
if (!_isReadonly)
- _items[key] = CreateCollection(null);
+ _dict[key] = CreateCollection(null);
else
return null;
}
- return _items[key];
+ return _dict[key];
}
}
@@ -145,15 +144,15 @@ public virtual ICollection this[TKey key]
///
public virtual ICollection Keys
{
- get { return _items.Keys; }
+ get { return _dict.Keys; }
}
- ///
- /// Gets the collection of collections of values.
- ///
+ ///
+ /// Gets all value collections.
+ ///
public virtual ICollection> Values
{
- get { return _items.Values; }
+ get { return _dict.Values; }
}
public IEnumerable Find(TKey key, Expression> predicate)
@@ -161,9 +160,9 @@ public IEnumerable Find(TKey key, Expression> predica
Guard.NotNull(key, nameof(key));
Guard.NotNull(predicate, nameof(predicate));
- if (_items.ContainsKey(key))
+ if (_dict.ContainsKey(key))
{
- return _items[key].Where(predicate.Compile());
+ return _dict[key].Where(predicate.Compile());
}
return Enumerable.Empty();
@@ -188,7 +187,10 @@ public virtual void Add(TKey key, TValue value)
/// The values.
public virtual void AddRange(TKey key, IEnumerable values)
{
- CheckNotReadonly();
+ if (values == null || !values.Any())
+ return;
+
+ CheckNotReadonly();
this[key].AddRange(values);
}
@@ -203,12 +205,12 @@ public virtual bool Remove(TKey key, TValue value)
{
CheckNotReadonly();
- if (!_items.ContainsKey(key))
+ if (!_dict.ContainsKey(key))
return false;
- bool result = _items[key].Remove(value);
- if (_items[key].Count == 0)
- _items.Remove(key);
+ bool result = _dict[key].Remove(value);
+ if (_dict[key].Count == 0)
+ _dict.Remove(key);
return result;
}
@@ -221,7 +223,7 @@ public virtual bool Remove(TKey key, TValue value)
public virtual bool RemoveAll(TKey key)
{
CheckNotReadonly();
- return _items.Remove(key);
+ return _dict.Remove(key);
}
///
@@ -230,7 +232,7 @@ public virtual bool RemoveAll(TKey key)
public virtual void Clear()
{
CheckNotReadonly();
- _items.Clear();
+ _dict.Clear();
}
///
@@ -240,7 +242,7 @@ public virtual void Clear()
/// True if the multimap has one or more values for the specified key, otherwise false .
public virtual bool ContainsKey(TKey key)
{
- return _items.ContainsKey(key);
+ return _dict.ContainsKey(key);
}
///
@@ -251,7 +253,7 @@ public virtual bool ContainsKey(TKey key)
/// True if the multimap contains such a value; otherwise, false .
public virtual bool ContainsValue(TKey key, TValue value)
{
- return _items.ContainsKey(key) && _items[key].Contains(value);
+ return _dict.ContainsKey(key) && _dict[key].Contains(value);
}
///
@@ -269,7 +271,7 @@ IEnumerator IEnumerable.GetEnumerator()
/// An object that can be used to iterate through the multimap.
public virtual IEnumerator>> GetEnumerator()
{
- foreach (KeyValuePair> pair in _items)
+ foreach (KeyValuePair> pair in _dict)
yield return pair;
}
diff --git a/src/Libraries/SmartStore.Core/Collections/ReferenceEqualityComparer.cs b/src/Libraries/SmartStore.Core/Collections/ReferenceEqualityComparer.cs
new file mode 100644
index 0000000000..b70b4302d2
--- /dev/null
+++ b/src/Libraries/SmartStore.Core/Collections/ReferenceEqualityComparer.cs
@@ -0,0 +1,15 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+
+namespace SmartStore.ComponentModel
+{
+ public sealed class ReferenceEqualityComparer : IEqualityComparer, IEqualityComparer
+ {
+ public static ReferenceEqualityComparer Default { get; } = new ReferenceEqualityComparer();
+
+ public new bool Equals(object x, object y) => ReferenceEquals(x, y);
+ public int GetHashCode(object obj) => RuntimeHelpers.GetHashCode(obj);
+ }
+}
diff --git a/src/Libraries/SmartStore.Core/Collections/SyncedCollection.cs b/src/Libraries/SmartStore.Core/Collections/SyncedCollection.cs
new file mode 100644
index 0000000000..d8100effd8
--- /dev/null
+++ b/src/Libraries/SmartStore.Core/Collections/SyncedCollection.cs
@@ -0,0 +1,222 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace SmartStore.Collections
+{
+ public sealed class SyncedCollection : ICollection
+ {
+ // INFO: Don't call it SynchronizedCollection because of framework dupe.
+ private readonly ICollection _col;
+
+ public SyncedCollection(ICollection wrappedCollection)
+ : this(wrappedCollection, new object())
+ {
+ }
+
+ public SyncedCollection(ICollection wrappedCollection, object syncRoot)
+ {
+ Guard.NotNull(wrappedCollection, nameof(wrappedCollection));
+ Guard.NotNull(syncRoot, nameof(syncRoot));
+
+ _col = wrappedCollection;
+ SyncRoot = syncRoot;
+ }
+
+ public object SyncRoot { get; }
+
+ public bool ReadLockFree { get; set; }
+
+ public void AddRange(IEnumerable collection)
+ {
+ lock (SyncRoot)
+ {
+ _col.AddRange(collection);
+ }
+ }
+
+ public void Insert(int index, T item)
+ {
+ if (_col is List list)
+ {
+ lock (SyncRoot)
+ {
+ list.Insert(index, item);
+ }
+ }
+
+ throw new NotSupportedException();
+ }
+
+ public void InsertRange(int index, IEnumerable values)
+ {
+ if (_col is List list)
+ {
+ lock (SyncRoot)
+ {
+ list.InsertRange(index, values);
+ }
+ }
+
+ throw new NotSupportedException();
+ }
+
+ public int RemoveRange(IEnumerable values)
+ {
+ int numRemoved = 0;
+
+ lock (SyncRoot)
+ {
+ foreach (var value in values)
+ {
+ if (_col.Remove(value))
+ numRemoved++;
+ }
+ }
+
+ return numRemoved;
+ }
+
+ public void RemoveRange(int index, int count)
+ {
+ if (_col is List list)
+ {
+ lock (SyncRoot)
+ {
+ list.RemoveRange(index, count);
+ }
+ }
+
+ throw new NotSupportedException();
+ }
+
+ public void RemoveAt(int index)
+ {
+ lock (SyncRoot)
+ {
+ if (_col is List list)
+ {
+ list.RemoveAt(index);
+ }
+ else
+ {
+ var item = _col.ElementAtOrDefault(index);
+ if (item != null)
+ {
+ _col.Remove(item);
+ }
+ }
+ }
+ }
+
+ public T this[int index]
+ {
+ get
+ {
+ if (ReadLockFree)
+ {
+ return _col.ElementAt(index);
+ }
+ else
+ {
+ lock (SyncRoot)
+ {
+ return _col.ElementAt(index);
+ }
+ }
+ }
+ }
+
+ #region ICollection
+
+ public int Count
+ {
+ get
+ {
+ if (ReadLockFree)
+ {
+ return _col.Count();
+ }
+ else
+ {
+ lock (SyncRoot)
+ {
+ return _col.Count();
+ }
+ }
+ }
+ }
+
+ public bool IsReadOnly => _col.IsReadOnly;
+
+ public void Add(T item)
+ {
+ lock (SyncRoot)
+ {
+ _col.Add(item);
+ }
+ }
+
+ public void Clear()
+ {
+ lock (SyncRoot)
+ {
+ _col.Clear();
+ }
+ }
+
+ public bool Contains(T item)
+ {
+ if (ReadLockFree)
+ {
+ return _col.Contains(item);
+ }
+ else
+ {
+ lock (SyncRoot)
+ {
+ return _col.Contains(item);
+ }
+ }
+ }
+
+ public void CopyTo(T[] array, int arrayIndex)
+ {
+ lock (SyncRoot)
+ {
+ _col.CopyTo(array, arrayIndex);
+ }
+ }
+
+ public bool Remove(T item)
+ {
+ lock (SyncRoot)
+ {
+ return _col.Remove(item);
+ }
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return this.GetEnumerator();
+ }
+
+ public IEnumerator GetEnumerator()
+ {
+ if (ReadLockFree)
+ {
+ return _col.GetEnumerator();
+ }
+ else
+ {
+ lock (SyncRoot)
+ {
+ return _col.GetEnumerator();
+ }
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/src/Libraries/SmartStore.Core/Collections/TreeNode.cs b/src/Libraries/SmartStore.Core/Collections/TreeNode.cs
index e9710fac8c..00034d0778 100644
--- a/src/Libraries/SmartStore.Core/Collections/TreeNode.cs
+++ b/src/Libraries/SmartStore.Core/Collections/TreeNode.cs
@@ -172,7 +172,7 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist
reader.Read();
objChildren = serializer.Deserialize(reader, sequenceType);
}
- if (string.Equals(a, "Id", StringComparison.OrdinalIgnoreCase))
+ else if (string.Equals(a, "Id", StringComparison.OrdinalIgnoreCase))
{
reader.Read();
id = serializer.Deserialize(reader);
@@ -196,14 +196,15 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist
{
var metadataProp = FastProperty.GetProperty(objectType, "Metadata", PropertyCachingStrategy.Cached);
metadataProp.SetValue(treeNode, metadata);
+ }
- if (id.HasValue())
- {
- var idProp = FastProperty.GetProperty(objectType, "Id", PropertyCachingStrategy.Cached);
- idProp.SetValue(treeNode, id);
- }
+ // Set Id
+ if (id.HasValue())
+ {
+ var idProp = FastProperty.GetProperty(objectType, "Id", PropertyCachingStrategy.Cached);
+ idProp.SetValue(treeNode, id);
}
-
+
return treeNode;
}
@@ -226,7 +227,17 @@ public override void WriteJson(JsonWriter writer, object value, JsonSerializer s
if (GetPropValue("Metadata", value) is IDictionary dict && dict.Count > 0)
{
writer.WritePropertyName("Metadata");
- serializer.Serialize(writer, dict);
+
+ var typeNameHandling = serializer.TypeNameHandling;
+ try
+ {
+ serializer.TypeNameHandling = TypeNameHandling.All;
+ serializer.Serialize(writer, dict);
+ }
+ finally
+ {
+ serializer.TypeNameHandling = typeNameHandling;
+ }
}
// Children
diff --git a/src/Libraries/SmartStore.Core/Collections/TreeNodeBase.cs b/src/Libraries/SmartStore.Core/Collections/TreeNodeBase.cs
index 695866869a..8264db12e1 100644
--- a/src/Libraries/SmartStore.Core/Collections/TreeNodeBase.cs
+++ b/src/Libraries/SmartStore.Core/Collections/TreeNodeBase.cs
@@ -34,17 +34,16 @@ public object Id
}
set
{
+ _id = value;
+
if (_parent != null)
{
var map = GetIdNodeMap();
- if (_id != value)
+ if (_id != null && map.ContainsKey(_id))
{
- if (_id != null && map.ContainsKey(_id))
- {
- // Remove old id from map
- map.Remove(_id);
- }
+ // Remove old id from map
+ map.Remove(_id);
}
if (value != null)
@@ -52,8 +51,6 @@ public object Id
map[value] = this;
}
}
-
- _id = value;
}
}
@@ -142,7 +139,12 @@ public TMetadata GetMetadata(string key, bool recursive = true)
break;
}
- return (TMetadata)metadata;
+ if (metadata != null)
+ {
+ return (TMetadata)metadata;
+ }
+
+ return default(TMetadata);
}
private bool TryGetMetadataForNode(TreeNodeBase node, string key, out object metadata)
@@ -203,20 +205,13 @@ private void AttachTo(T newParent, int? index)
{
Guard.NotNull(newParent, nameof(newParent));
+ var prevParent = _parent;
+
if (_parent != null)
{
// Detach from parent
_parent.Remove((T)this);
}
- else
- {
- // Is a root node with a map: get rid of it.
- if (_idNodeMap != null)
- {
- _idNodeMap.Clear();
- _idNodeMap = null;
- }
- }
if (index == null)
{
@@ -232,12 +227,70 @@ private void AttachTo(T newParent, int? index)
_parent = newParent;
- // Set id in new id-node map
- if (_id != null)
+ FixIdNodeMap(prevParent, newParent);
+ }
+
+ ///
+ /// Responsible for propagating node ids when detaching/attaching nodes
+ ///
+ private void FixIdNodeMap(T prevParent, T newParent)
+ {
+ ICollection> keyedNodes = null;
+
+ if (prevParent != null)
+ {
+ // A node is moved. We need to detach first.
+ keyedNodes = new List>();
+
+ // Detach ids from prev map
+ var prevMap = prevParent.GetIdNodeMap();
+
+ Traverse(x =>
+ {
+ // Collect all child node's ids
+ if (x._id != null)
+ {
+ keyedNodes.Add(x);
+ if (prevMap.ContainsKey(x._id))
+ {
+ // Remove from map
+ prevMap.Remove(x._id);
+ }
+ }
+ }, true);
+ }
+
+ if (keyedNodes == null && _idNodeMap != null)
+ {
+ // An orphan/root node is attached
+ keyedNodes = _idNodeMap.Values;
+ }
+
+ if (newParent != null)
{
- var map = GetIdNodeMap();
- if (map != null)
+ // Get new *root map
+ var map = newParent.GetIdNodeMap();
+
+ // Merge *this map with *root map
+ if (keyedNodes != null)
+ {
+ foreach (var node in keyedNodes)
+ {
+ map[node._id] = node;
+ }
+
+ // Get rid of *this map after memorizing keyed nodes
+ if (_idNodeMap != null)
+ {
+ _idNodeMap.Clear();
+ _idNodeMap = null;
+ }
+ }
+
+ if (prevParent == null && _id != null)
{
+ // When *this was a root, but is keyed, then *this id
+ // was most likely missing in the prev id-node-map.
map[_id] = (T)this;
}
}
@@ -582,7 +635,7 @@ public T SelectNode(Expression> predicate, bool includeSelf = fals
}
///
- /// Selects all nodes (recursively) with match the given predicate
+ /// Selects all nodes (recursively) witch match the given predicate
///
/// The predicate to match against
/// A readonly collection of node matches
@@ -613,15 +666,7 @@ public void Remove(T node)
var list = node._parent?._children;
if (list.Remove(node))
{
- // Remove id from id node map
- if (node._id != null)
- {
- var map = node.GetIdNodeMap();
- if (map != null && map.ContainsKey(node._id))
- {
- map.Remove(node._id);
- }
- }
+ node.FixIdNodeMap(node._parent, null);
FixIndexes(list, node._index, -1);
@@ -640,11 +685,7 @@ public void Clear()
_children.Clear();
}
- var map = GetIdNodeMap();
- if (map != null)
- {
- map.Clear();
- }
+ FixIdNodeMap(_parent, null);
}
public void Traverse(Action action, bool includeSelf = false)
diff --git a/src/Libraries/SmartStore.Core/Collections/TrimmedBuffer.cs b/src/Libraries/SmartStore.Core/Collections/TrimmedBuffer.cs
new file mode 100644
index 0000000000..db54bf8936
--- /dev/null
+++ b/src/Libraries/SmartStore.Core/Collections/TrimmedBuffer.cs
@@ -0,0 +1,106 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace SmartStore.Collections
+{
+ public sealed class TrimmedBuffer : ICollection
+ {
+ private readonly int _maxSize;
+ private readonly List _list;
+
+ public TrimmedBuffer(int maxSize)
+ : this((IEnumerable)null, default(T), maxSize)
+ {
+ }
+
+ public TrimmedBuffer(IEnumerable collection, int maxSize)
+ : this(collection, default(T), maxSize)
+ {
+ }
+
+ public TrimmedBuffer(string collection, int maxSize)
+ : this(collection.NullEmpty()?.Convert>()?.Distinct(), default(T), maxSize)
+ {
+ }
+
+ public TrimmedBuffer(string collection, T newItem, int maxSize)
+ : this(collection.NullEmpty()?.Convert>()?.Distinct(), newItem, maxSize)
+ {
+ }
+
+ public TrimmedBuffer(IEnumerable collection, T newItem, int maxSize)
+ {
+ Guard.IsTrue(maxSize >= 0, nameof(maxSize));
+
+ _maxSize = maxSize;
+ _list = new List(collection ?? Enumerable.Empty());
+
+ if (newItem != null)
+ {
+ Add(newItem);
+ }
+
+ Trim();
+ }
+
+ private void Trim()
+ {
+ if (_maxSize >= 0)
+ {
+ while (_list.Count > _maxSize)
+ _list.RemoveAt(_list.Count - 1);
+ }
+ }
+
+ public T this[int index]
+ {
+ get
+ {
+ return _list[index];
+ }
+ set
+ {
+ _list[index] = value;
+ }
+ }
+
+ public void Add(T item)
+ {
+ var i = _list.IndexOf(item);
+ if (i > -1)
+ _list.RemoveAt(i);
+
+ _list.Insert(0, item);
+
+ Trim();
+ }
+
+ public void Clear() => _list.Clear();
+
+ public bool Contains(T item) => _list.Contains(item);
+
+ public void CopyTo(T[] array, int arrayIndex) => _list.CopyTo(array, arrayIndex);
+
+ public bool Remove(T item) => _list.Remove(item);
+
+ public int Count => _list.Count;
+
+ public bool IsReadOnly => false;
+
+ public IEnumerator GetEnumerator()
+ {
+ return _list.GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return _list.GetEnumerator();
+ }
+
+ public override string ToString()
+ {
+ return string.Join(";", _list);
+ }
+ }
+}
diff --git a/src/Libraries/SmartStore.Core/ComponentModel/FastActivator.cs b/src/Libraries/SmartStore.Core/ComponentModel/FastActivator.cs
index d37ec07015..85419958d3 100644
--- a/src/Libraries/SmartStore.Core/ComponentModel/FastActivator.cs
+++ b/src/Libraries/SmartStore.Core/ComponentModel/FastActivator.cs
@@ -44,6 +44,8 @@ public object Activate(params object[] parameters)
return Invoker(parameters);
}
+ #region Static
+
///
/// Creates a single fast constructor invoker. The result is not cached.
///
@@ -78,8 +80,6 @@ public static Func MakeFastInvoker(ConstructorInfo constructor
return lambda.Compile();
}
- #region Static
-
///
/// Creates and caches fast constructor invokers
///
@@ -93,8 +93,7 @@ public static FastActivator[] GetActivators(Type type)
private static FastActivator[] GetActivatorsCore(Type type)
{
- FastActivator[] activators;
- if (!_activatorsCache.TryGetValue(type, out activators))
+ if (!_activatorsCache.TryGetValue(type, out FastActivator[] activators))
{
var candidates = GetCandidateConstructors(type);
activators = candidates.Select(c => new FastActivator(c)).ToArray();
diff --git a/src/Libraries/SmartStore.Core/ComponentModel/FastInvoker.cs b/src/Libraries/SmartStore.Core/ComponentModel/FastInvoker.cs
new file mode 100644
index 0000000000..ba43358e1a
--- /dev/null
+++ b/src/Libraries/SmartStore.Core/ComponentModel/FastInvoker.cs
@@ -0,0 +1,293 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Linq.Expressions;
+using System.Collections.Concurrent;
+using SmartStore.Utilities;
+
+namespace SmartStore.ComponentModel
+{
+ public class FastInvoker
+ {
+ private static readonly ConcurrentDictionary _invokersCache = new ConcurrentDictionary();
+
+ public FastInvoker(MethodInfo methodInfo)
+ {
+ Guard.NotNull(methodInfo, nameof(methodInfo));
+
+ Method = methodInfo;
+ Invoker = MakeFastInvoker(methodInfo);
+ ParameterTypes = methodInfo.GetParameters().Select(p => p.ParameterType).ToArray();
+ }
+
+ ///
+ /// Gets the backing .
+ ///
+ public MethodInfo Method { get; private set; }
+
+ ///
+ /// Gets the parameter types from the backing
+ ///
+ public Type[] ParameterTypes { get; private set; }
+
+ ///
+ /// Gets the method invoker.
+ ///
+ public Func Invoker { get; private set; }
+
+ ///
+ /// Invokes the method using the specified parameters.
+ ///
+ /// The method invocation result.
+ public object Invoke(object obj, params object[] parameters)
+ {
+ return Invoker(obj, parameters);
+ }
+
+ #region Static
+
+ ///
+ /// Creates a single fast method invoker. The result is not cached.
+ ///
+ /// Method to create invoker for.
+ /// The fast method invoker delegate.
+ public static Func MakeFastInvoker(MethodInfo method)
+ {
+ Guard.NotNull(method, nameof(method));
+
+ var instanceParameterExpression = Expression.Parameter(typeof(object), "instance");
+ var argumentsParameterExpression = Expression.Parameter(typeof(object[]), "args");
+ var index = 0;
+
+ var argumentExtractionExpressions =
+ method
+ .GetParameters()
+ .Select(parameter =>
+ Expression.Convert(
+ Expression.ArrayAccess(
+ argumentsParameterExpression,
+ Expression.Constant(index++)
+ ),
+ parameter.ParameterType
+ )
+ ).ToList();
+
+ var callExpression = method.IsStatic
+ ? Expression.Call(method, argumentExtractionExpressions)
+ : Expression.Call(
+ Expression.Convert(
+ instanceParameterExpression,
+ method.DeclaringType
+ ),
+ method,
+ argumentExtractionExpressions
+ );
+
+ var endLabel = Expression.Label(typeof(object));
+
+ var finalExpression = method.ReturnType == typeof(void)
+ ? (Expression)Expression.Block(
+ callExpression,
+ Expression.Return(endLabel, Expression.Constant(null)),
+ Expression.Label(endLabel, Expression.Constant(null))
+ )
+ : Expression.Convert(callExpression, typeof(object));
+
+ var lambdaExpression = Expression.Lambda>(
+ finalExpression,
+ instanceParameterExpression,
+ argumentsParameterExpression
+ );
+
+ var lamdba = lambdaExpression.Compile();
+ return lamdba;
+ }
+
+ ///
+ /// Invokes a method using the specified object and parameter instances.
+ ///
+ /// The objectinstance
+ /// Method name
+ /// Argument types of the matching method overload (in exact order)
+ /// Parameter instances to pass to invocation
+ /// The method invocation result.
+ public static object Invoke(object obj, string methodName, Type[] parameterTypes, object[] parameters)
+ {
+ Guard.NotNull(obj, nameof(obj));
+
+ FastInvoker invoker;
+
+ if (parameterTypes == null || parameterTypes.Length == 0)
+ {
+ invoker = GetInvoker(obj.GetType(), methodName);
+ }
+ else
+ {
+ invoker = GetInvoker(obj.GetType(), methodName, parameterTypes);
+ }
+
+ //var hasAnyNullParam = parameters.Any(x => x == null);
+ //if (hasAnyNullParam)
+ //{
+ // throw new ArgumentException("When invoking a method with parameter instances, no instance must be null.", nameof(parameters));
+ //}
+
+ return invoker.Invoke(obj, parameters ?? Array.Empty());
+ }
+
+ ///
+ /// Creates and caches a fast method invoker.
+ ///
+ /// Name of method to create an invoker for.
+ /// Argument types of method to create an invoker for.
+ /// The fast method invoker.
+ public static FastInvoker GetInvoker(string methodName, params Type[] argTypes)
+ {
+ return GetInvoker(typeof(T), methodName, argTypes);
+ }
+
+ ///
+ /// Creates and caches a fast method invoker.
+ ///
+ /// The type to extract fast method invoker for.
+ /// Name of method to create an invoker for.
+ /// Argument types of method to create an invoker for.
+ /// The fast method invoker.
+ public static FastInvoker GetInvoker(Type type, string methodName, params Type[] argTypes)
+ {
+ Guard.NotNull(type, nameof(type));
+ Guard.NotEmpty(methodName, nameof(methodName));
+
+ var cacheKey = MethodKey.Create(type, methodName, argTypes);
+
+ if (!_invokersCache.TryGetValue(cacheKey, out var invoker))
+ {
+ var method = FindMatchingMethod(type, methodName, argTypes);
+
+ if (method == null)
+ {
+ throw new MethodAccessException("Could not find a matching method '{0}' in type {1}.".FormatInvariant(methodName, type));
+ }
+
+ invoker = new FastInvoker(method);
+ _invokersCache.TryAdd(cacheKey, invoker);
+ }
+
+ return invoker;
+ }
+
+ ///
+ /// Creates and caches a fast method invoker.
+ ///
+ /// Method info instance to create an invoker for.
+ /// The fast method invoker.
+ public static FastInvoker GetInvoker(MethodInfo method)
+ {
+ Guard.NotNull(method, nameof(method));
+
+ var cacheKey = MethodKey.Create(method);
+
+ if (!_invokersCache.TryGetValue(cacheKey, out var invoker))
+ {
+ invoker = new FastInvoker(method);
+ _invokersCache.TryAdd(cacheKey, invoker);
+ }
+
+ return invoker;
+ }
+
+ private static MethodInfo FindMatchingMethod(Type type, string methodName, Type[] argTypes)
+ {
+ var method = argTypes == null || argTypes.Length == 0
+ ? type.GetMethod(methodName)
+ : type.GetMethod(
+ methodName,
+ BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly,
+ null,
+ argTypes ?? new Type[0],
+ null);
+
+ return method;
+ }
+
+ #endregion
+
+ abstract class MethodKey
+ {
+ public override bool Equals(object obj) =>
+ throw new NotImplementedException();
+
+ public override int GetHashCode() =>
+ throw new NotImplementedException();
+
+ public static bool operator ==(MethodKey left, MethodKey right) =>
+ object.Equals(left, right);
+
+ public static bool operator !=(MethodKey left, MethodKey right) =>
+ !(left == right);
+
+ internal static MethodKey Create(Type type, string methodName, IEnumerable parameterTypes)
+ {
+ return new HashMethodKey(type, methodName, parameterTypes);
+ }
+
+ internal static MethodKey Create(MethodInfo method)
+ {
+ return new MethodInfoKey(method);
+ }
+ }
+
+ class HashMethodKey : MethodKey, IEquatable
+ {
+ private readonly int _hash;
+
+ public HashMethodKey(Type type, string methodName, IEnumerable parameterTypes)
+ {
+ _hash = HashCodeCombiner.Start().Add(type).Add(methodName).Add(parameterTypes).CombinedHash;
+ }
+
+ public override bool Equals(object obj) =>
+ this.Equals(obj as HashMethodKey);
+
+ public bool Equals(HashMethodKey other)
+ {
+ if (other == null)
+ return false;
+
+ return this._hash == other._hash;
+ }
+
+ public override int GetHashCode()
+ {
+ return _hash;
+ }
+ }
+
+ class MethodInfoKey : MethodKey, IEquatable
+ {
+ private readonly MethodInfo _method;
+
+ public MethodInfoKey(MethodInfo method)
+ {
+ _method = method;
+ }
+
+ public override bool Equals(object obj) =>
+ this.Equals(obj as MethodInfoKey);
+
+ public bool Equals(MethodInfoKey other)
+ {
+ if (other == null)
+ return false;
+
+ return this._method == other._method;
+ }
+
+ public override int GetHashCode()
+ {
+ return _method.GetHashCode();
+ }
+ }
+ }
+}
diff --git a/src/Libraries/SmartStore.Core/ComponentModel/FastProperty.cs b/src/Libraries/SmartStore.Core/ComponentModel/FastProperty.cs
index 4ad5aebee4..7aafa809cd 100644
--- a/src/Libraries/SmartStore.Core/ComponentModel/FastProperty.cs
+++ b/src/Libraries/SmartStore.Core/ComponentModel/FastProperty.cs
@@ -543,7 +543,7 @@ protected static IDictionary GetVisibleProperties(
{
return result;
}
-
+
// The simple and common case, this is normal POCO object - no need to allocate.
var allPropertiesDefinedOnType = true;
var allProperties = GetProperties(type, createPropertyHelper, allPropertiesCache);
diff --git a/src/Libraries/SmartStore.Core/ComponentModel/HybridExpando.cs b/src/Libraries/SmartStore.Core/ComponentModel/HybridExpando.cs
index 010ef62aae..17d44dbbac 100644
--- a/src/Libraries/SmartStore.Core/ComponentModel/HybridExpando.cs
+++ b/src/Libraries/SmartStore.Core/ComponentModel/HybridExpando.cs
@@ -234,7 +234,7 @@ public override bool TrySetMember(SetMemberBinder binder, object value)
protected virtual bool TrySetMemberCore(string name, object value)
{
- // first check to see if there's a native property to set
+ // first check to see if there's a dictionary entry to set
if (Properties.ContainsKey(name))
{
Properties[name] = value;
diff --git a/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/EnumerableConverter.cs b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/EnumerableConverter.cs
index 5687317362..ae60b58488 100644
--- a/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/EnumerableConverter.cs
+++ b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/EnumerableConverter.cs
@@ -125,7 +125,7 @@ public override object ConvertTo(CultureInfo culture, string format, object valu
if (value is IEnumerable enumerable)
{
- // we don't use string.Join() because it doesn't support invariant culture
+ // We don't use string.Join() because it doesn't support invariant culture
foreach (var token in enumerable)
{
var str = _elementTypeConverter.ConvertTo(culture, format, token, typeof(string));
diff --git a/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/TypeConverterFactory.cs b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/TypeConverterFactory.cs
index 299ab07a07..ee6c50e474 100644
--- a/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/TypeConverterFactory.cs
+++ b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/TypeConverterFactory.cs
@@ -86,7 +86,8 @@ public static ITypeConverter GetConverter(object component)
public static ITypeConverter GetConverter(Type type)
{
- Guard.NotNull(type, nameof(type));
+ if (type == null)
+ throw new ArgumentNullException(nameof(type));
if (_typeConverters.TryGetValue(type, out var converter))
{
diff --git a/src/Libraries/SmartStore.Core/Configuration/JsonPersistAttribute.cs b/src/Libraries/SmartStore.Core/Configuration/JsonPersistAttribute.cs
index d0ed7563d2..00d96fe9ba 100644
--- a/src/Libraries/SmartStore.Core/Configuration/JsonPersistAttribute.cs
+++ b/src/Libraries/SmartStore.Core/Configuration/JsonPersistAttribute.cs
@@ -4,17 +4,15 @@
using System.Text;
namespace SmartStore.Core.Configuration
-{
-
- // codehint: sm-add
+{
///
/// Marker attribute. Indicates that the settings should
/// be persisted as a JSON string rather than splitted
/// into single properties.
///
[AttributeUsage(AttributeTargets.Class)]
+ [Obsolete]
public class JsonPersistAttribute : Attribute
{
}
-
}
diff --git a/src/Libraries/SmartStore.Core/Data/DbContextScope.cs b/src/Libraries/SmartStore.Core/Data/DbContextScope.cs
index 93a7552c50..7d3b433e8e 100644
--- a/src/Libraries/SmartStore.Core/Data/DbContextScope.cs
+++ b/src/Libraries/SmartStore.Core/Data/DbContextScope.cs
@@ -3,6 +3,7 @@
using System.Linq.Expressions;
using System.Collections.Generic;
using System.Linq;
+using System.Threading.Tasks;
namespace SmartStore.Core.Data
{
@@ -89,7 +90,12 @@ public int Commit()
return _ctx.SaveChanges();
}
- public void Dispose()
+ public async Task CommitAsync()
+ {
+ return await _ctx.SaveChangesAsync();
+ }
+
+ public void Dispose()
{
_ctx.AutoDetectChangesEnabled = _autoDetectChangesEnabled;
_ctx.ProxyCreationEnabled = _proxyCreationEnabled;
diff --git a/src/Libraries/SmartStore.Core/Data/DbQuerySettings.cs b/src/Libraries/SmartStore.Core/Data/DbQuerySettings.cs
index d1e22d6965..3e2318e4f0 100644
--- a/src/Libraries/SmartStore.Core/Data/DbQuerySettings.cs
+++ b/src/Libraries/SmartStore.Core/Data/DbQuerySettings.cs
@@ -4,8 +4,6 @@ namespace SmartStore.Core.Data
{
public class DbQuerySettings
{
- private readonly static DbQuerySettings s_default = new DbQuerySettings(false, false);
-
public DbQuerySettings(bool ignoreAcl, bool ignoreMultiStore)
{
this.IgnoreAcl = ignoreAcl;
@@ -15,9 +13,6 @@ public DbQuerySettings(bool ignoreAcl, bool ignoreMultiStore)
public bool IgnoreAcl { get; private set; }
public bool IgnoreMultiStore { get; private set; }
- public static DbQuerySettings Default
- {
- get { return s_default; }
- }
+ public static DbQuerySettings Default { get; } = new DbQuerySettings(false, false);
}
}
diff --git a/src/Libraries/SmartStore.Core/Data/Hooks/DbLoadHook.cs b/src/Libraries/SmartStore.Core/Data/Hooks/DbLoadHook.cs
deleted file mode 100644
index b2e9d20731..0000000000
--- a/src/Libraries/SmartStore.Core/Data/Hooks/DbLoadHook.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-using System;
-
-namespace SmartStore.Core.Data.Hooks
-{
- public abstract class DbLoadHook : IDbLoadHook
- {
- public virtual void OnLoaded(BaseEntity entity)
- {
- throw new NotImplementedException();
- }
- }
-}
diff --git a/src/Libraries/SmartStore.Core/Data/Hooks/DbSaveHook.cs b/src/Libraries/SmartStore.Core/Data/Hooks/DbSaveHook.cs
index 656ebc60be..09b74716ac 100644
--- a/src/Libraries/SmartStore.Core/Data/Hooks/DbSaveHook.cs
+++ b/src/Libraries/SmartStore.Core/Data/Hooks/DbSaveHook.cs
@@ -40,7 +40,6 @@ public virtual void OnBeforeSaveCompleted()
{
}
-
public virtual void OnAfterSave(IHookedEntity entry)
{
var entity = entry.Entity as TEntity;
diff --git a/src/Libraries/SmartStore.Core/Data/Hooks/DefaultDbHookHandler.cs b/src/Libraries/SmartStore.Core/Data/Hooks/DefaultDbHookHandler.cs
index 8152d3fd3e..39126f48f8 100644
--- a/src/Libraries/SmartStore.Core/Data/Hooks/DefaultDbHookHandler.cs
+++ b/src/Libraries/SmartStore.Core/Data/Hooks/DefaultDbHookHandler.cs
@@ -8,8 +8,6 @@ namespace SmartStore.Core.Data.Hooks
{
public class DefaultDbHookHandler : IDbHookHandler
{
- private readonly IEnumerable> _hooks;
- private readonly IList> _loadHooks;
private readonly IList> _saveHooks;
private readonly Multimap _hooksRequestCache = new Multimap();
@@ -17,7 +15,6 @@ public class DefaultDbHookHandler : IDbHookHandler
// Prevents repetitive hooking of the same entity/state/[pre|post] combination within a single request
private readonly HashSet _hookedEntities = new HashSet();
- private static HashSet _importantLoadHookTypes;
private static HashSet _importantSaveHookTypes;
private readonly static object _lock = new object();
@@ -29,8 +26,6 @@ public class DefaultDbHookHandler : IDbHookHandler
public DefaultDbHookHandler(IEnumerable> hooks)
{
- _hooks = hooks;
- _loadHooks = hooks.Where(x => x.Metadata.IsLoadHook == true).ToList();
_saveHooks = hooks.Where(x => x.Metadata.IsLoadHook == false).ToList();
}
@@ -38,24 +33,7 @@ public ILogger Logger
{
get;
set;
- }
-
- public bool HasImportantLoadHooks()
- {
- if (_importantLoadHookTypes == null)
- {
- lock (_lock)
- {
- if (_importantLoadHookTypes == null)
- {
- _importantLoadHookTypes = new HashSet();
- _importantLoadHookTypes.AddRange(_loadHooks.Where(x => x.Metadata.Important).Select(x => x.Metadata.ImplType));
- }
- }
- }
-
- return _importantLoadHookTypes.Any();
- }
+ } = NullLogger.Instance;
public bool HasImportantSaveHooks()
{
@@ -74,41 +52,6 @@ public bool HasImportantSaveHooks()
return _importantSaveHookTypes.Any();
}
- public IEnumerable TriggerLoadHooks(BaseEntity entity, bool importantHooksOnly)
- {
- Guard.NotNull(entity, nameof(entity));
-
- var processedHooks = new HashSet();
-
- if (!_loadHooks.Any() || (importantHooksOnly && !this.HasImportantLoadHooks()))
- {
- return processedHooks;
- }
-
- var entityType = entity.GetUnproxiedType();
-
- var hooks = GetLoadHookInstancesFor(entityType, importantHooksOnly);
- foreach (var hook in hooks)
- {
- // call hook
- try
- {
- hook.OnLoaded(entity);
- processedHooks.Add(hook);
- }
- catch (Exception ex) when (ex is NotImplementedException || ex is NotSupportedException)
- {
- RegisterVoidHook(hook, entityType, EntityState.Unchanged, HookStage.Load);
- }
- catch (Exception ex)
- {
- Logger.ErrorFormat(ex, "LoadHook exception ({0})", hook.GetType().FullName);
- }
- }
-
- return processedHooks;
- }
-
public IEnumerable TriggerPreSaveHooks(IEnumerable entries, bool importantHooksOnly, out bool anyStateChanged)
{
Guard.NotNull(entries, nameof(entries));
@@ -210,17 +153,6 @@ public IEnumerable TriggerPostSaveHooks(IEnumerable
return processedHooks;
}
- private IEnumerable GetLoadHookInstancesFor(Type entityType, bool importantOnly)
- {
- return GetHookInstancesFor(
- entityType,
- EntityState.Unchanged,
- HookStage.Load,
- importantOnly,
- _loadHooks,
- _importantLoadHookTypes);
- }
-
private IEnumerable GetSaveHookInstancesFor(IHookedEntity entry, HookStage stage, bool importantOnly)
{
return GetHookInstancesFor(
diff --git a/src/Libraries/SmartStore.Core/Data/Hooks/HookMetadata.cs b/src/Libraries/SmartStore.Core/Data/Hooks/HookMetadata.cs
index 2c07d9e699..f1f81574a2 100644
--- a/src/Libraries/SmartStore.Core/Data/Hooks/HookMetadata.cs
+++ b/src/Libraries/SmartStore.Core/Data/Hooks/HookMetadata.cs
@@ -29,8 +29,7 @@ public static HookMetadata Create(Type hookedType, bool important = false
{
ImplType = typeof(THook),
HookedType = hookedType,
- Important = important,
- IsLoadHook = typeof(IDbLoadHook).IsAssignableFrom(hookedType)
+ Important = important
};
}
}
diff --git a/src/Libraries/SmartStore.Core/Data/Hooks/IDbHookHandler.cs b/src/Libraries/SmartStore.Core/Data/Hooks/IDbHookHandler.cs
index ed010a5313..9e46634c86 100644
--- a/src/Libraries/SmartStore.Core/Data/Hooks/IDbHookHandler.cs
+++ b/src/Libraries/SmartStore.Core/Data/Hooks/IDbHookHandler.cs
@@ -6,17 +6,8 @@ namespace SmartStore.Core.Data.Hooks
{
public interface IDbHookHandler
{
- bool HasImportantLoadHooks();
bool HasImportantSaveHooks();
- ///
- /// Triggers all load hooks for a single entity
- ///
- /// Whether to trigger only hooks marked with the attribute
- /// The loaded entity
- /// The list of actually processed hook instances
- IEnumerable TriggerLoadHooks(BaseEntity entity, bool importantHooksOnly);
-
///
/// Triggers all pre action hooks
///
@@ -44,21 +35,11 @@ public static IDbHookHandler Instance
get { return s_instance; }
}
- public bool HasImportantLoadHooks()
- {
- return false;
- }
-
public bool HasImportantSaveHooks()
{
return false;
}
- public IEnumerable TriggerLoadHooks(BaseEntity entity, bool importantHooksOnly)
- {
- return Enumerable.Empty();
- }
-
public IEnumerable TriggerPreSaveHooks(IEnumerable entries, bool importantHooksOnly, out bool anyStateChanged)
{
anyStateChanged = false;
diff --git a/src/Libraries/SmartStore.Core/Data/Hooks/IDbLoadHook.cs b/src/Libraries/SmartStore.Core/Data/Hooks/IDbLoadHook.cs
index 843fe689cc..669ffc9dec 100644
--- a/src/Libraries/SmartStore.Core/Data/Hooks/IDbLoadHook.cs
+++ b/src/Libraries/SmartStore.Core/Data/Hooks/IDbLoadHook.cs
@@ -5,6 +5,7 @@ namespace SmartStore.Core.Data.Hooks
///
/// A hook that is executed right after an entity has been loaded from database and materialized.
///
+ [Obsolete("Not called anymore for performance reasons.")]
public interface IDbLoadHook : IDbHook
{
void OnLoaded(BaseEntity entity);
diff --git a/src/Libraries/SmartStore.Core/Data/IDbContextExtensions.cs b/src/Libraries/SmartStore.Core/Data/IDbContextExtensions.cs
index 7ee2b43a39..0d56838cce 100644
--- a/src/Libraries/SmartStore.Core/Data/IDbContextExtensions.cs
+++ b/src/Libraries/SmartStore.Core/Data/IDbContextExtensions.cs
@@ -107,6 +107,12 @@ public static void LoadCollection(
var entry = dbContext.Entry(entity);
var collection = entry.Collection(navigationProperty);
+ // Avoid System.InvalidOperationException: Member 'IsLoaded' cannot be called for property...
+ if (entry.State == System.Data.Entity.EntityState.Detached)
+ {
+ ctx.Attach(entity);
+ }
+
if (force)
{
collection.IsLoaded = false;
@@ -156,6 +162,12 @@ public static void LoadReference(
var entry = dbContext.Entry(entity);
var reference = entry.Reference(navigationProperty);
+ // Avoid System.InvalidOperationException: Member 'IsLoaded' cannot be called for property...
+ if (entry.State == System.Data.Entity.EntityState.Detached)
+ {
+ ctx.Attach(entity);
+ }
+
if (force)
{
reference.IsLoaded = false;
diff --git a/src/Libraries/SmartStore.Core/Data/IQueryableExtensions.cs b/src/Libraries/SmartStore.Core/Data/IQueryableExtensions.cs
index b4426b1ec8..d9534c06ea 100644
--- a/src/Libraries/SmartStore.Core/Data/IQueryableExtensions.cs
+++ b/src/Libraries/SmartStore.Core/Data/IQueryableExtensions.cs
@@ -9,7 +9,6 @@ namespace SmartStore
{
public static class IQueryableExtensions
{
-
///
/// Instructs the repository to eager load entities that may be in the type's association path.
///
@@ -40,6 +39,5 @@ public static IQueryable Expand(this IQueryable query, Expre
return query.Include(path);
}
-
}
}
diff --git a/src/Libraries/SmartStore.Core/Data/RepositoryExtensions.cs b/src/Libraries/SmartStore.Core/Data/RepositoryExtensions.cs
index a3d54c9f75..5041bebcdf 100644
--- a/src/Libraries/SmartStore.Core/Data/RepositoryExtensions.cs
+++ b/src/Libraries/SmartStore.Core/Data/RepositoryExtensions.cs
@@ -4,6 +4,7 @@
using System.Linq;
using System.Linq.Expressions;
using System.Text;
+using System.Threading.Tasks;
namespace SmartStore.Core.Data
{
@@ -95,7 +96,57 @@ public static int DeleteAll(this IRepository rs, Expression>
return count;
}
- public static IQueryable Get(
+ ///