diff --git a/.gitignore b/.gitignore
index 85bd82b7ff..d8cacc6b13 100644
--- a/.gitignore
+++ b/.gitignore
@@ -322,3 +322,6 @@ Kopie von*
src/SmartStoreNET.Packager.sln
Log.txt
src/Presentation/SmartStore.Web/Themes/FlexMuseo/
+
+# Used by SSL certificates management tools (e.g. Certify The Web).
+/src/Presentation/SmartStore.Web/.well-known
diff --git a/CREDITS.txt b/CREDITS.txt
index 5ea4c4a6fb..ff0d418111 100644
--- a/CREDITS.txt
+++ b/CREDITS.txt
@@ -135,11 +135,11 @@ Copyright: Copyright (c) 2007 James Newton-King
License: MIT
-GeoLite Free by MaxMind
+DB-IP Country Lite by Eris Networks S.A.S
---------------------------------------------
-Website: http://www.maxmind.com
-Copyright: Copyright (c) 2014 Maxmind
-License: Creative Commons Attribution-ShareAlike 3.0 Unported License
+https://db-ip.com/
+Copyright: Copyright (c) 2020 db-ip.com
+License: Creative Commons Attribution 4.0 International License
Linearicons by Perxis
diff --git a/README.md b/README.md
index f88a9608ed..51e24bdf95 100644
--- a/README.md
+++ b/README.md
@@ -1,136 +1,25 @@
-
-
-
-
-
-
-
-
-
-
-
-
-Ready. Sell. Grow.
-
-
-
-
-
-
-
-
-
-
-
- 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.
+# Deprecation warning :warning:
-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.
+This repository is outdated and no longer being actively maintained by the Smartstore devs.
+Users & developers are encouraged to check out the successor built on **ASP.NET Core**:
+**Smartstore 5**.
-
-
-
+
+
+
+
-**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)
-
-
-
-## Highlights
-
-### Technology & Design
-
-* State of the art architecture thanks to `ASP.NET 4.5`, `ASP.NET MVC 5`, `Entity Framework 6` and Domain Driven Design
-* Easy to extend and extremely flexible thanks to modular design
-* Highly scalable thanks to full page caching and web farm support
-* A powerful theming engine lets you create themes & skins with minimum effort thanks to theme inheritance
-* Point&Click Theme configuration
-* Highly professional search framework based on Lucene.NET, delivering ultra fast faceted search results
-* Consistent and sophisticated use of modern components such as `jQuery`, `Bootstrap 4`, `Sass` & more in the front and back end.
-* Easy shop management thanks to modern and clean UI
-
-### Features
-
-* NEW: CMS Page Builder
-* NEW: CMS Menu Builder
-* Multi-Store support
-* Unlimited number of products and categories
-* Product Bundles
-* RESTful WebApi
-* 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.
-* Trusted Shops precertification
-* 100% compliant with German jurisdiction
-* Sales-, Customer- & Inventory-management
-* Comprehensive CRM features
-* Powerful Discount System
-* Powerful layered navigation in the shop
-* Numerous Payment and Shipping Providers and options
-* Sophisticated Marketing & Promotion capabilities (Gift cards, Reward Points, discounts of any type and more)
-* Reviews & Ratings
-* CMS (Blog, Forum, custom pages & HTML content etc.)
-* and many more...
-
-
-
-## Project Status
-
-Smartstore.NET V3.2.0 has been released on 10 May, 2019. The highlights are:
-
-* (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.
-
-* [**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
-* Setup a website in IIS and point the file directory to your unzipped folder
-* 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.
-
### System requirements
* IIS 7+
* ASP.NET 4.5+
* MS SQL Server 2008 Express (or higher) OR MS SQL Server Compact 4
-* Visual C++ Redistributable für Visual Studio 2015 ([Download](https://www.microsoft.com/en-US/download/details.aspx?id=52685))
+* Visual C++ Redistributable für Visual Studio 2015-2019 ([Download](https://support.microsoft.com/en-us/help/2977003/the-latest-supported-visual-c-downloads))
* Full Trust
## License
-Smartstore.NET Community Edition is released under the [GPLv3 license](http://www.gnu.org/licenses/gpl-3.0.txt).
+Smartstore 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 080c78d497..f67aba4987 100644
--- a/SmartStoreNET.Tasks.Targets
+++ b/SmartStoreNET.Tasks.Targets
@@ -39,7 +39,7 @@
x86
$(BUILD_NUMBER)
- 3.2.2
+ 4.2.0
$(StageFolder)
.$(Version)
@@ -97,13 +97,15 @@
-
-
-
-
+
+
+
+
+
+
-
+
diff --git a/assets/ready.set.go.jpg b/assets/ready.set.go.jpg
new file mode 100644
index 0000000000..e69e37758c
Binary files /dev/null and b/assets/ready.set.go.jpg differ
diff --git a/assets/smartstore-text.png b/assets/smartstore-text.png
index 25bf9558fb..0d8cfe6bb8 100644
Binary files a/assets/smartstore-text.png and b/assets/smartstore-text.png differ
diff --git a/changelog.md b/changelog.md
index 9e52619298..8ba63a78f8 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,27 +1,241 @@
# Release Notes
-## SmartStore.NET 4.0.0
+## Smartstore 4.2.0
+
+### New Features
+**Dimension Pricing** (commercial plugin)
+
+* Freely configurable dimension templates
+* Price calculation based on user input for predefined product dimensions
+* Shipping rates based on dimension and weight calculation
+* Freely configurable shipping formulas for irregular shapes
+
+**File Manager** (commercial plugin)
+
+* Freely configurable tabs for product detail pages
+* Unlimited upload of documents for each product tab
+* CSV Import
+
+**Digital Product Advisor** (commercial plugin)
+
+* Product presentation based on configurable questions and predefined answers
+* Product filter based on rule builder rules
+
+### Improvements
+* Web API: added RewardPointsHistory entity
+* Added canonical HTML link for blog, forum and news page
+* Better semaphore handling
+* Several OpenTrans improvements
+* BMEcat: use stream writer to write large xml files & fixed catalog structure
+
+### Bugfixes
+
+* #379 & #264 Login failed if login type is user and email and if both username and email are an email address and are not identical.
+* #2197 Web API error "The specified type member 'MergedDataValues' is not supported in LINQ to Entities..."
+* Web API: fixes OData Connected Service error when accessing MediaFiles and MediaFolders lists
+* PayPal Express: fixed order may have been placed even if DoExpressCheckoutPayment failed
+* Roxy Filemanager: icons were not displayed anymore
+* Azure CloudBlockBlob stream throws on Length access. Fetch Position instead.
+* Fixed the uploaded file of a product attribute is lost as soon as another attribute is selected
+* Fixes display problem with uploaded media files which contain a + character
+
+## Smartstore 4.1.1
+### New Features
+* **Page Builder**
+ * Added newsletter block to subscribe/unsubscribe to the newsletter.
+* Rule Builder: added CartItemQuantity rule to specify conditions for the quantity per cart product.
+* Added an option to display blog posts and news only for a certain language.
+* #2171 Created SEO setting for ExtraAllows.
+
+### Improvements
+* WebDAVModule is removed in Web.config because it disables PUT and DELETE requests (required by Web API).
+* Newsletter subscriptions: added filter for customer roles in export profile and backend list.
+* #2167 Added an activity log for order editing.
+* Inherit ACL and store mappings into all sub-categories, not only into direct sub-categories.
+* #2186 MegaMenu: link for background image had no effect.
+* Added honeypot protection to blog post comments.
+
+### Bugfixes
+* #2162 A plugin could not be uninstalled if it's the only one installed.
+* Sometimes no picture was displayed when navigating in the product details' image gallery.
+* Deactivated slugs were falsely always redirected without SSL.
+* Selected products could not be deleted in the backend product list.
+* Web-API: fixed "failed to serialize the response body" when uploading product images and missing product media file mapping if an equal image was found in catalog folder.
+* Categories: fixed missing cache invalidation when inheriting ACL or stores into child categories and products.
+* Link Builder: the selected media file was not applied.
+* Selecting an attribute of type date caused an error on the product detail page.
+* AmazonPay: fixed rare "client ID is a required parameter and is not set" when using the login function.
+* Various fixes for anti forgery request handling.
+* Used mediasettings value for picture rendering in MegaMenu (pictures weren't displayed in MegaMenu dropdowns).
+* Mediamanager uploads were marked as being transient.
+
+## Smartstore 4.1.0
### Highlights
-* (NEW) **Granular permissions**: Extensive permission management based on customer groups via tree view. Supports inheritance to allow or deny entire permission ranges.
-* (NEW) **Granular permissions**: All permissions of a customer directly visible on the customer edit page.
+* **Web API:** now supports the widely extended version 4 of OData. New endpoints for uploading and managing media files have been added to the API.
+* (NEW) **Delivery dates:** product info now shows estimated delivery date (exact date, range, *Not before*, *Not later than*). Merchant can enter optional Min/Max delivery days and also specify business hours.
+* (DEV) [VS Extension](https://marketplace.visualstudio.com/items?itemName=SmartStoreAG.Smartstore) to create plugins. Helps developing plugins for Smartstore by creating the basic structure of a Smartstore plugin with all the required files.
+* **OCI** provides connection to e-procurement systems via industry standard OCI (commercial plugin)
+* **cXmlPunchout** provides connection to e-procurement systems via industry standard cXmlPunchout (commercial plugin)
+* **Video** support in product media gallery.
+* **Media Gallery** displays a multitude of media files in various styles as Page Builder block.
+* (PERF) **~10% faster page rendering** speed thanks to better eager loading & caching strategies
### Breaking changes
-* *Install\UninstallPermissions* has been removed from *IPermissionService*. No longer needs to be called by plugins. Permissions are now automatically installed\uninstalled.
-* *IDiscountRequirementRule* and *IShippingMethodFilter* has been removed. Discounts and shipping methods can now be filtered by rule sets.
-* Customer navigation property *CustomerRoles* has been replaced by *CustomerRoleMappings*, a new entity for role mappings.
+* The **Glimpse** plugin is obsolete and has been removed! Does not work with .NET 4.7+ and hasn't been maintained for 7 years now.
+* (DEV) **Web API**: porting to OData v4 has minor incompatibilities with v3. See this [blog post](https://smartstore.com/en/web-api-supports-odata-v4-as-from-smartstore-4-1) for details.
+* **Blogs\News**: the direct language association has been removed. Localized content is now entered using the localization editor.
+Merging the contents of different languages must be done manually (migration is not possible).
+* **PostFinance**: plugin has been extended and renamed. Users of this payment method must activate PostFinance e-Commerce in backend and enter the API access data again.
+
+### New Features
+* Theming: responsive font-sizing and spacing
+* **Page Builder**
+ * #2042 Added gallery block to display a multitude of media files in various styles.
+ * #2036 Added block to display content slider.
+ * #2035 Added block to display news.
+ * #1729 Added option for title and intro inline background color.
+* **PostFinance**: added new payment methods "Checkout All-in-one" and "Checkout Flex".
+* #1644 Graying out unavailable attribute combinations.
+* #1744 Added 'Orders' tab to the product editor to display all orders in which the product has been purchased.
+* #1735 Added a setting to open a product directly if the search term matches a SKU, MPN or GTIN.
+* #1239 Added configurability for password strength.
+* #1678 Determine initial working currency based on IP/country.
+* #1697 Added picture and color options to checkout attribute values.
+* #1788 Added setting to restrict order amount in general and for customer roles in specific.
+* #1978 Added minimum and maximum days to delivery times to show a specific delivery date.
+* #1715 Added product attributes selected by customer to RFQ and ask question message.
+* (DEV) Full localization support for properties of setting classes.
+* #2012 Added paging and sorting for bestsellers report.
+* #2014 Added paging and sorting for top customers report.
+* #2092 Added meta properties for social media sharing to category, manufacturer, news and blog pages.
+* Added option for SameSite mode property of cookies
+* #1307 Add 'apple-touch-icon' meta with all common sizes
+* #213 Allow special title and meta tags for home
+
+### Improvements
+* (PERF) 10% faster page rendering speed thanks to better eager loading & caching strategies
+* #1981 Added grouping for cart rules.
+* #1407 Added menu to set order and payment status for orders checked in backend grid.
+* #1778 Back button closes offcanvas menu/cart.
+* #2069 MegaSearch: product order is applied to all categories in which a product appears.
+* #2052 When editing a product, the stock quantity is only updated if it has not changed since the page was loaded.
+* #213 Added options for special title and meta tags on home page.
+* #1977 CMS Menu Builder: added new builder types for blogs and news.
+* Search Log: added option to hide top search terms in instant search.
+* CookieManager: Admin area for user defined cookie information
+* CookieManager: More compact frontend layout for mobile devices
+* Seo optimization for News, Blog & Boards
+* (Admin) More filter options for news & blog items
+* #1901 News: Made CreatedOn configurable
+* #2078 Hidden news items should be rendered opaque if current user is in admin role.
+* #1902 Form registration clears values when switching between site languages
+* #1984 Render breadcrumb for pages that are part of the main menu.
+* #2121 Media Manager: implement direct file "replace" support.
+* #1338 Display 'PanGV' in OffCanvas cart
+* #1988 Preconnect all external resources and load them async
+* #1968 Dropzone: upload the folder structure when a folder is dropped.
+* #1996 Page Builder: Added 'Loading behaviour' option to picture block
+* #2100 Improved schema for news, blogs and products
+
+### Bugfixes
+* **Media Manager**
+ * The file date should be displayed in local time and not in UTC.
+* **Page Builder**
+ * Reveal effect on story block conflicted with nested block reveal effects.
+ * #2068 Fixed RTL display issues with dropdown menus and slider.
+* **Export**
+ * An empty export file was created at the end of an export when using the batch-size option.
+ * The offset option skipped one record too much.
+ * The XML export of CountryOfOrigin caused an error.
+* Message template attachments were not attached to the email.
+* Shipment templates showed all products of the order instead of only those of the shipment.
+* EasyCredit: fixed "Could not convert setting 'EasyCreditSettings.LastPurchaseLimitUpdatedOn' to type 'DateTime'".
+* #2076 Attributes were not pre-selected for all associated products of a grouped product.
+* Customer tab on customer role edit page showed deleted customers.
+* #2041 Product with call for price did show as free in product list and grid view.
+* #2079 Order item editing: order total was not updated when both unit and item price were changed.
+* RSS feeds: relative URLs of objects embedded in HTML are replaced by absolute URLs.
+* Rules: automatic category assignments of products were not taken into account in a search index updates.
+* Campaign preview did not work if there are no newsletter subscribers.
+* Many alt & title attribute fixes for pictures
+* #2000 Escape toxic chars in meta attributes
+* Google Analytics: Fixed script error in conjunction with CookieManager
+* #2085 RTL: searchbar button wasn't aligned correctly
+* #2068 RTL: Fixed Page Builder slider positioning, dropdown and other alignments
+* #2019 OffCancas compare: hide horizontal scrollbar
+* #2146 LimitedToStores property was not set in a rare case.
+
+## Smartstore 4.0.1
+### New Features
+* Check on app startup whether any plugin has changed and refresh all plugin locale resources.
+
+### Improvements
+* Theming: minimal but effective tweaks (rounding, shadows, typo)
+* (SEO) (PERF) Preload logo to prevent flashing and web fonts to avoid FOUT on slow internet connection
+* **Rule Builder**:
+ * #1969 The preview grid is now displayed in a modal dialog.
+ * Added more cart rules: weekday, total quantity of products in cart.
+ * Added list with example values for browser, operating system and device family rule.
+* #1995 The localized ALT text of media files is now respected for all frontend images.
+* SEO: most images in frontend now carry `loading="lazy"` attribute which loads images only when they enter visible viewport.
+* **Media Manager**:
+ * Improved image lazy loading behavior.
+ * The reference table of a file now remains open if its data has been loaded.
+ * Migration: detect duplicate file names.
+* Improved handling of customer roles, e.g. paging for customer role grid.
+* (PERF) Use direct SQL to delete guest customers.
+* Blog & News items: Unpublished items are visible for shop admins now
+
+
+### Bugfixes
+* Order placement validation now also checks whether the selected payment method has been filtered out.
+* #1976 The button to cancel recurring payments in my-account was out of function.
+* (pb) Flat Box did not invert text color if concrete colors were specified for text parts
+* (mm) Store logo is handled by "content" album, not "file"
+* (mm) Tracker tracks only the last media file property
+* (mm) Migration: `Download` entity was teared down too early in the migration process.
+* (perf) Plugin hash calculation was broken in a way that every plugin was redeployed on each app startup
+* A cart rule was not applied if more than one cart rule was assigned to an object.
+* MegaSearch: fixed the incorrect featured sorting.
+* Added missing paging for shipment list.
+* #2009 "Show all" link in top brands list is always rendered in new line but should be last item in last row.
+
+
+## Smartstore 4.0.0
+### Highlights
+* (NEW) **Media Manager**: Powerful and lightning-fast explorer/manager for media files (commercial plugin exclusively bundled with Pro Edition)
+* (NEW) **Rule Builder**: Powerful rule system for visual business rule creation
+ * Dozens of predefined rules out-of-the-box
+ * Supports logical operators (AND/OR) and unlimited grouping/nesting
+ * Cart rules: can be applied to discounts, shipping and payment methods
+ * Customer rules: used to auto-assign customers to customer groups for 1-to-1 content targeting
+ * Product rules: used to auto-assign products to categories based on filter definitions (e.g. price, brand, color, stock etc.)
+* (NEW) **New permission (ACL) system**: Granular tree-based permission management based on customer groups. Supports inheritance to allow or deny entire permission ranges
+* (NEW) **Personalized product recommendations**: Replaces homepage products dynamically with personalized product recommendations based on customer interests. (commercial plugin)
+* (NEW) **Geo Blocker**: Restricts shop access based on visitor country or IP address ranges (commercial plugin)
+* (NEW) **Dashboard with charts**: New dashboard widgets display key data like orders, customers, bestsellers etc. in nice graphical charts.
+
+### Breaking changes
+* (Dev) *Install\UninstallPermissions* has been removed from *IPermissionService*. No longer needs to be called by plugins. Permissions are now automatically installed\uninstalled.
+* (Dev) *IDiscountRequirementRule* and *IShippingMethodFilter* has been removed. Discounts and shipping methods can now be filtered by rule sets.
+* (Dev) Customer navigation property *CustomerRoles* has been replaced by *CustomerRoleMappings*, a new entity for role mappings.
* The search index must be rebuilt due to various enhancements.
-* The wrong name of API endpoint *LocalizedPropertys* has been corrected and renamed to *LocalizedProperties*.
+* (Dev) The wrong name of API endpoint *LocalizedPropertys* has been corrected and renamed to *LocalizedProperties*.
### New Features
+* **Granular permissions**: All permissions of a customer directly visible on the customer edit page.
+* DEV: Added Visual Studio extension to create Smartstore Plugins
+* GDPR conform Cookie Consent Manager
+* SEO: XML Sitemap now includes blog, news and forum
+* SEO: do 301 redirect to URL without trailing slash
+* SVG support for image uploads.
+* Schedule tasks: new property "Priority" runs tasks with higher priority first when multiple tasks are pending.
+* #1805 Added XML Sitemap settings to backend UI.
* #1598 Add published property to ProductTag entity.
* #1669 Apply percentage discounts also on tier prices.
* #1618 Implement ACL and multistore capability on menu item level.
* #1683 Menu Builder items: implement support for icon (brand) color.
* #1584 Show bundle item images in order details like in shopping cart details.
-* SEO: XML Sitemap now includes blog, news and forum
-* SEO: do 301 redirect to URL without trailing slash
-* SVG support for image uploads.
-* Schedule tasks: new property "Priority" runs tasks with higher priority first when multiple tasks are pending.
* **MegaSearch**:
* Added more text analysis options (e.g. Lucene.Net classic analyzer).
* Added tool to display internal information about a search, such as Lucene.Net terms.
@@ -30,8 +244,8 @@
* Perf. Added option to ignore attribute filtering property on product level.
* **Web-API**:
* #1809 Added a parameter to start an import after uploading import files.
- * #1801 Added endpoints for ProductPictures, ProductCategories and ProductManufacturers to allow to update DisplayOrder.
- * #1858 Added endpoints for NewsLetterSubscription.
+ * #1801 Added endpoints for *ProductPictures*, *ProductCategories* and *ProductManufacturers* to allow updating *DisplayOrder*.
+ * #1858 Added endpoints for *NewsLetterSubscription*.
* #1714 Direct links to variations on a product.
* #1733 Add ability to hide products from catalog, but not search.
* #1754 Implement hidden boolean setting that controls which catalog search engine implementation should be used in backend.
@@ -40,9 +254,10 @@
* #1116 Add new field for product condition.
* #1790 ACL support for manufacturers.
* #1616 Add new field for tracking URL to shipment entity to better track shipments.
+* (Page Builder) Added a Page Builder block to display blog posts.
### Improvements
-* #1663 Make MeasureDimension and MeasureWeight localizable.
+* #1663 Make *MeasureDimension* and *MeasureWeight* localizable.
* #1600 Show an example currency value for custom formatting value changes.
* Card desk instead of a grid for customer addresses on customer edit page.
* Display "price from" in product lists if any attribute combination price exists.
@@ -58,6 +273,13 @@
* #1848 Send the billing address when redirecting to PayPal.
* Hide cart payment button for payment methods without match of applied rule sets.
* #1839 MegaSearch: Support exact value match for numeric range filters.
+* #1920 Hide option prizes if "Call for price" is enabled.
+* Debitoor: added an option to force a price type on invoices.
+* BeezUp: export product costs.
+* #1915 RTL: fixed alignment of product art badge.
+* #1738 Sending of mails to customers uses generic message template now.
+* #1805 UI: Added XmlSitemap settings to backend
+* And many more other minor improvements
### Bugfixes
* Export: Fixed KeyNotFoundException when batch size was 1.
@@ -72,20 +294,25 @@
* **Page Builder**:
* #1689 Block with z-index < 0 can not be selected in story view - Block tools can reduce z-index below 0.
* #1691 Manually editing/removing block cols/rows does not refresh grid state correctly.
+ * #1684 Edit Mode rendering bug in Mac Safari
+ * #1836 Added "Order" property to page Builder block to control rendering order of blocks in HTML output
* **PayPal**:
* Fixed rare exception "Unsupported Media Type" (415) in PayPal PLUS.
* Only process a partially refund IPN when the order refunded amount is zero. Otherwise the order refunded amount will be wrong.
* **Customer import**:
* VatNumber and other fields were ignored.
* Customer roles sometimes inserted several times.
- * Changed billing\shipping address was added instead of updating the existing one.
+ * Changed billing/shipping address was added instead of updating the existing one.
* Region assigned to an address was not updated.
* **Product tags**:
* #1730 Product tag count should filter also based on Visibility.
* Product tag count sometimes not up-to-date due to missing cache clearance.
+* **Debitoor**:
+ * Avoid errors due to invalid quantity unit Id of 0.
+ * #1952 rework quantity units.
+ * Sometimes the SKU was missing on invoices.
* Multistore mapping was ignored for manufacturers in sitemap.
* Categories limited to stores were not displayed in tree view.
-* Debitoor: avoid errors due to invalid product unitId of 0.
* Fixed InvalidOperationException in CreatePdfInvoiceAttachment when an order is placed by a guest.
* The ShowDescriptionInSubPages setting should also be applied to the bottom category description.
* #1774 Recaptcha: doesn't work for product reviews, blog and news comments if hidden captcha is activated.
@@ -104,6 +331,14 @@
* #1835 Removing gift card issue.
* #1875 A delivery time cannot be deleted if it is assigned to a variant combination of a deleted product.
* BeezUp: fixes ArgumentNullException, parameter name "source".
+* #1743 Avoid redirecting to the account activation page when trying to log in for the first time.
+* #1895 The checkout button disappears when moving a product from the shopping cart to the wishlist.
+* Menu builder: in case of system menus the template can changes accidentally when saving.
+* Structured data: Replace length with depth property.
+* #1701 'View All' button from the offcanvas manufacturer menu causes 404 on mobile.
+* Added missing sitemap task.
+* #1696 ESD: browser freezes when editing file changelog
+* And many more other minor fixes...
## SmartStore.NET 3.2.2
diff --git a/lib/SmartStore.Licensing/SmartStore.Licensing.dll b/lib/SmartStore.Licensing/SmartStore.Licensing.dll
index 17bf4e2ad9..61354aae6a 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 b58492422f..92179ee59a 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 2019")]
+[assembly: AssemblyCopyright("Copyright © SmartStore AG 2020")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
diff --git a/src/AssemblyVersionInfo.cs b/src/AssemblyVersionInfo.cs
index 719cd1d6ff..9ca54df467 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.2.2.0")]
+[assembly: AssemblyVersion("4.2.0.0")]
-[assembly: AssemblyFileVersion("3.2.2.0")]
-[assembly: AssemblyInformationalVersion("3.2.2.0")]
+[assembly: AssemblyFileVersion("4.2.0.0")]
+[assembly: AssemblyInformationalVersion("4.2.0.0")]
diff --git a/src/Libraries/SmartStore.Core/Async/AsyncRunner.cs b/src/Libraries/SmartStore.Core/Async/AsyncRunner.cs
index 8327c7b40d..91ffb0db95 100644
--- a/src/Libraries/SmartStore.Core/Async/AsyncRunner.cs
+++ b/src/Libraries/SmartStore.Core/Async/AsyncRunner.cs
@@ -1,6 +1,5 @@
using System;
using System.Collections.Concurrent;
-using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using System.Web.Hosting;
@@ -9,423 +8,417 @@
namespace SmartStore.Core.Async
{
- using EventQueue = ConcurrentQueue>;
-
- public class AsyncRunner : IDisposable
- {
- #region Instance members
-
- private ExclusiveSynchronizationContext CurrentContext;
- private SynchronizationContext OldContext;
- private int TaskCount;
-
- private AsyncRunner()
- {
- OldContext = SynchronizationContext.Current;
- CurrentContext = new ExclusiveSynchronizationContext(OldContext);
- SynchronizationContext.SetSynchronizationContext(CurrentContext);
- }
-
- public void Dispose()
- {
- 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 execute
- public void Run(Task task, Action continuation = null)
- {
- CurrentContext.Post(async _ =>
- {
- try
- {
- Increment();
- await task;
-
- continuation?.Invoke(task);
- }
- catch (Exception e)
- {
- CurrentContext.InnerException = e;
- }
- finally
- {
- Decrement();
- }
- }, null);
+ using EventQueue = ConcurrentQueue>;
+
+ public class AsyncRunner : IDisposable
+ {
+ #region Instance members
+
+ private readonly ExclusiveSynchronizationContext CurrentContext;
+ private readonly SynchronizationContext OldContext;
+ private int TaskCount;
+
+ private AsyncRunner()
+ {
+ OldContext = SynchronizationContext.Current;
+ CurrentContext = new ExclusiveSynchronizationContext(OldContext);
+ SynchronizationContext.SetSynchronizationContext(CurrentContext);
+ }
+
+ public void Dispose()
+ {
+ 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 execute
+ public void Run(Task task, Action continuation = null)
+ {
+ CurrentContext.Post(async _ =>
+ {
+ try
+ {
+ Increment();
+ await task;
+
+ continuation?.Invoke(task);
+ }
+ catch (Exception e)
+ {
+ CurrentContext.InnerException = e;
+ }
+ finally
+ {
+ Decrement();
+ }
+ }, null);
CurrentContext.BeginMessageLoop();
}
- ///
- /// Executes an async Task method which has a TResult return type synchronously
- ///
- /// Return Type
- /// Task to execute
- public TResult Run(Task task, Action> continuation = null)
- {
- var result = default(TResult);
+ ///
+ /// Executes an async Task method which has a TResult return type synchronously
+ ///
+ /// Return Type
+ /// Task to execute
+ public TResult Run(Task task, Action> continuation = null)
+ {
+ var result = default(TResult);
var curContext = CurrentContext;
curContext.Post(async _ =>
- {
- try
- {
- Increment();
- result = await task;
-
- continuation?.Invoke(task);
- }
- catch (Exception e)
- {
- CurrentContext.InnerException = e;
- }
- finally
- {
- Decrement();
- }
- }, null);
+ {
+ try
+ {
+ Increment();
+ result = await task;
+
+ continuation?.Invoke(task);
+ }
+ catch (Exception e)
+ {
+ CurrentContext.InnerException = e;
+ }
+ finally
+ {
+ Decrement();
+ }
+ }, null);
curContext.BeginMessageLoop();
return result;
- }
-
- #endregion
-
- private static readonly BackgroundWorkHost _host = new BackgroundWorkHost();
-
- ///
- /// Gets the global cancellation token which signals the application shutdown
- ///
- public static CancellationToken AppShutdownCancellationToken
- {
- get { return _host.ShutdownCancellationTokenSource.Token; }
- }
-
- public static AsyncRunner Create()
- {
- return new AsyncRunner();
- }
-
- ///
- /// 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)
- {
- using (var runner = new AsyncRunner())
- {
- runner.Run(func(), continuation);
- }
- }
-
- ///
- /// 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)
- {
- var result = default(TResult);
-
- using (var runner = new AsyncRunner())
- {
- result = runner.Run(func(), continuation);
- }
-
- return result;
- }
-
- public static Task Run(
- Action action,
- CancellationToken cancellationToken = default(CancellationToken),
- TaskCreationOptions options = TaskCreationOptions.LongRunning,
- TaskScheduler scheduler = null)
- {
- Guard.NotNull(action, nameof(action));
-
- var ct = _host.CreateCompositeCancellationTokenSource(cancellationToken).Token;
-
- var t = Task.Factory.StartNew(() => {
- var accessor = EngineContext.Current.ContainerManager.ScopeAccessor;
- using (accessor.BeginContextAwareScope())
- {
- action(accessor.GetLifetimeScope(null), ct);
- }
- }, ct, options, scheduler ?? TaskScheduler.Default);
-
- _host.Register(t, ct);
-
- return t;
- }
-
- 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));
-
- var ct = _host.CreateCompositeCancellationTokenSource(cancellationToken).Token;
-
- var t = Task.Factory.StartNew((o) =>
- {
- var accessor = EngineContext.Current.ContainerManager.ScopeAccessor;
- using (accessor.BeginContextAwareScope())
- {
- action(accessor.GetLifetimeScope(null), ct, o);
- }
- }, state, ct, options, scheduler ?? TaskScheduler.Default);
-
- _host.Register(t, ct);
-
- return t;
- }
-
- public static Task Run(
- Func function,
- CancellationToken cancellationToken = default(CancellationToken),
- TaskCreationOptions options = TaskCreationOptions.LongRunning,
- TaskScheduler scheduler = null)
- {
- Guard.NotNull(function, nameof(function));
-
- var ct = _host.CreateCompositeCancellationTokenSource(cancellationToken).Token;
-
- var t = Task.Factory.StartNew(() =>
- {
- var accessor = EngineContext.Current.ContainerManager.ScopeAccessor;
- using (accessor.BeginContextAwareScope())
- {
- return function(accessor.GetLifetimeScope(null), ct);
- }
- }, ct, options, scheduler ?? TaskScheduler.Default);
-
- _host.Register(t, ct);
-
- return t;
- }
-
- 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));
-
- var ct = _host.CreateCompositeCancellationTokenSource(cancellationToken).Token;
-
- var t = Task.Factory.StartNew((o) =>
- {
- var accessor = EngineContext.Current.ContainerManager.ScopeAccessor;
- using (accessor.BeginContextAwareScope())
- {
- return function(accessor.GetLifetimeScope(null), ct, o);
- }
- }, state, ct, options, scheduler ?? TaskScheduler.Default);
-
- _host.Register(t, ct);
-
- return t;
- }
-
- 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;
- 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; }
-
- public override void Send(SendOrPostCallback d, object state)
- {
- throw new NotSupportedException("We cannot send to the same thread");
- }
-
- public override void Post(SendOrPostCallback d, object state)
- {
- _items.Enqueue(Tuple.Create(d, state));
- _workItemsWaiting.Set();
- }
-
- public void EndMessageLoop()
- {
- Post(_ => _done = true, null);
- }
-
- public void BeginMessageLoop()
- {
- while (!_done)
- {
- if (!_items.TryDequeue(out var task))
- {
- _workItemsWaiting.WaitOne();
- }
- else
- {
- task.Item1(task.Item2);
-
- if (InnerException != null) // method threw an exeption
- {
- throw new AggregateException("AsyncHelpers.Run method threw an exception.", InnerException);
- }
- }
- }
- }
-
- public override SynchronizationContext CreateCopy()
- {
- return this;
- }
- }
-
- }
-
- internal class BackgroundWorkHost : IRegisteredObject
- {
- private readonly CancellationTokenSource _shutdownCancellationTokenSource = new CancellationTokenSource();
- private int _numRunningWorkItems;
-
- public BackgroundWorkHost()
- {
- HostingEnvironment.RegisterObject(this);
- }
-
- public CancellationTokenSource ShutdownCancellationTokenSource
- {
- get { return _shutdownCancellationTokenSource; }
- }
-
- public void Stop(bool immediate)
- {
- int num;
- lock (this)
- {
- _shutdownCancellationTokenSource.Cancel();
- num = _numRunningWorkItems;
- }
- if (num == 0)
- {
- FinalShutdown();
- }
- }
-
- public CancellationTokenSource CreateCompositeCancellationTokenSource(CancellationToken userCancellationToken)
- {
- if (userCancellationToken == CancellationToken.None)
- {
- return _shutdownCancellationTokenSource;
- }
- return CancellationTokenSource.CreateLinkedTokenSource(_shutdownCancellationTokenSource.Token, userCancellationToken);
- }
-
- public void Register(Task work, CancellationToken cancellationToken)
- {
- if (!cancellationToken.IsCancellationRequested)
- {
- lock (this)
- {
- if (cancellationToken.IsCancellationRequested)
- {
- return;
- }
- _numRunningWorkItems++;
- }
-
- work.ContinueWith(
- WorkItemComplete,
- CancellationToken.None,
- TaskContinuationOptions.ExecuteSynchronously,
- TaskScheduler.Default);
- }
- }
-
- private void WorkItemComplete(Task work)
- {
- int num;
- bool isCancellationRequested;
- lock (this)
- {
- num = --_numRunningWorkItems;
- isCancellationRequested = _shutdownCancellationTokenSource.IsCancellationRequested;
- }
- if (num == 0 && isCancellationRequested)
- {
- FinalShutdown();
- }
- }
-
- private void FinalShutdown()
- {
- HostingEnvironment.UnregisterObject(this);
- }
-
- }
-}
+ }
+
+ #endregion
+
+ private static readonly BackgroundWorkHost _host = new BackgroundWorkHost();
+
+ ///
+ /// Gets the global cancellation token which signals the application shutdown
+ ///
+ public static CancellationToken AppShutdownCancellationToken => _host.ShutdownCancellationTokenSource.Token;
+
+ public static AsyncRunner Create()
+ {
+ return new AsyncRunner();
+ }
+
+ ///
+ /// 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)
+ {
+ using (var runner = new AsyncRunner())
+ {
+ runner.Run(func(), continuation);
+ }
+ }
+
+ ///
+ /// 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)
+ {
+ var result = default(TResult);
+
+ using (var runner = new AsyncRunner())
+ {
+ result = runner.Run(func(), continuation);
+ }
+
+ return result;
+ }
+
+ public static Task Run(
+ Action action,
+ CancellationToken cancellationToken = default(CancellationToken),
+ TaskCreationOptions options = TaskCreationOptions.LongRunning,
+ TaskScheduler scheduler = null)
+ {
+ Guard.NotNull(action, nameof(action));
+
+ var ct = _host.CreateCompositeCancellationTokenSource(cancellationToken).Token;
+
+ var t = Task.Factory.StartNew(() =>
+ {
+ var accessor = EngineContext.Current.ContainerManager.ScopeAccessor;
+ using (accessor.BeginContextAwareScope())
+ {
+ action(accessor.GetLifetimeScope(null), ct);
+ }
+ }, ct, options, scheduler ?? TaskScheduler.Default);
+
+ _host.Register(t, ct);
+
+ return t;
+ }
+
+ 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));
+
+ var ct = _host.CreateCompositeCancellationTokenSource(cancellationToken).Token;
+
+ var t = Task.Factory.StartNew((o) =>
+ {
+ var accessor = EngineContext.Current.ContainerManager.ScopeAccessor;
+ using (accessor.BeginContextAwareScope())
+ {
+ action(accessor.GetLifetimeScope(null), ct, o);
+ }
+ }, state, ct, options, scheduler ?? TaskScheduler.Default);
+
+ _host.Register(t, ct);
+
+ return t;
+ }
+
+ public static Task Run(
+ Func function,
+ CancellationToken cancellationToken = default(CancellationToken),
+ TaskCreationOptions options = TaskCreationOptions.LongRunning,
+ TaskScheduler scheduler = null)
+ {
+ Guard.NotNull(function, nameof(function));
+
+ var ct = _host.CreateCompositeCancellationTokenSource(cancellationToken).Token;
+
+ var t = Task.Factory.StartNew(() =>
+ {
+ var accessor = EngineContext.Current.ContainerManager.ScopeAccessor;
+ using (accessor.BeginContextAwareScope())
+ {
+ return function(accessor.GetLifetimeScope(null), ct);
+ }
+ }, ct, options, scheduler ?? TaskScheduler.Default);
+
+ _host.Register(t, ct);
+
+ return t;
+ }
+
+ 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));
+
+ var ct = _host.CreateCompositeCancellationTokenSource(cancellationToken).Token;
+
+ var t = Task.Factory.StartNew((o) =>
+ {
+ var accessor = EngineContext.Current.ContainerManager.ScopeAccessor;
+ using (accessor.BeginContextAwareScope())
+ {
+ return function(accessor.GetLifetimeScope(null), ct, o);
+ }
+ }, state, ct, options, scheduler ?? TaskScheduler.Default);
+
+ _host.Register(t, ct);
+
+ return t;
+ }
+
+ 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;
+ 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; }
+
+ public override void Send(SendOrPostCallback d, object state)
+ {
+ throw new NotSupportedException("We cannot send to the same thread");
+ }
+
+ public override void Post(SendOrPostCallback d, object state)
+ {
+ _items.Enqueue(Tuple.Create(d, state));
+ _workItemsWaiting.Set();
+ }
+
+ public void EndMessageLoop()
+ {
+ Post(_ => _done = true, null);
+ }
+
+ public void BeginMessageLoop()
+ {
+ while (!_done)
+ {
+ if (!_items.TryDequeue(out var task))
+ {
+ _workItemsWaiting.WaitOne();
+ }
+ else
+ {
+ task.Item1(task.Item2);
+
+ if (InnerException != null) // method threw an exeption
+ {
+ throw new AggregateException("AsyncHelpers.Run method threw an exception.", InnerException);
+ }
+ }
+ }
+ }
+
+ public override SynchronizationContext CreateCopy()
+ {
+ return this;
+ }
+ }
+ }
+
+ internal class BackgroundWorkHost : IRegisteredObject
+ {
+ private readonly CancellationTokenSource _shutdownCancellationTokenSource = new CancellationTokenSource();
+ private int _numRunningWorkItems;
+
+ public BackgroundWorkHost()
+ {
+ HostingEnvironment.RegisterObject(this);
+ }
+
+ public CancellationTokenSource ShutdownCancellationTokenSource => _shutdownCancellationTokenSource;
+
+ public void Stop(bool immediate)
+ {
+ int num;
+ lock (this)
+ {
+ _shutdownCancellationTokenSource.Cancel();
+ num = _numRunningWorkItems;
+ }
+ if (num == 0)
+ {
+ FinalShutdown();
+ }
+ }
+
+ public CancellationTokenSource CreateCompositeCancellationTokenSource(CancellationToken userCancellationToken)
+ {
+ if (userCancellationToken == CancellationToken.None)
+ {
+ return _shutdownCancellationTokenSource;
+ }
+ return CancellationTokenSource.CreateLinkedTokenSource(_shutdownCancellationTokenSource.Token, userCancellationToken);
+ }
+
+ public void Register(Task work, CancellationToken cancellationToken)
+ {
+ if (!cancellationToken.IsCancellationRequested)
+ {
+ lock (this)
+ {
+ if (cancellationToken.IsCancellationRequested)
+ {
+ return;
+ }
+ _numRunningWorkItems++;
+ }
+
+ work.ContinueWith(
+ WorkItemComplete,
+ CancellationToken.None,
+ TaskContinuationOptions.ExecuteSynchronously,
+ TaskScheduler.Default);
+ }
+ }
+
+ private void WorkItemComplete(Task work)
+ {
+ int num;
+ bool isCancellationRequested;
+ lock (this)
+ {
+ num = --_numRunningWorkItems;
+ isCancellationRequested = _shutdownCancellationTokenSource.IsCancellationRequested;
+ }
+ if (num == 0 && isCancellationRequested)
+ {
+ FinalShutdown();
+ }
+ }
+
+ private void FinalShutdown()
+ {
+ HostingEnvironment.UnregisterObject(this);
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/src/Libraries/SmartStore.Core/Async/AsyncStateInfo.cs b/src/Libraries/SmartStore.Core/Async/AsyncStateInfo.cs
index 8bc3898736..d4f9493575 100644
--- a/src/Libraries/SmartStore.Core/Async/AsyncStateInfo.cs
+++ b/src/Libraries/SmartStore.Core/Async/AsyncStateInfo.cs
@@ -2,34 +2,34 @@
namespace SmartStore.Core.Async
{
- [Serializable]
- public class AsyncStateInfo
- {
- // used for serialization compatibility
- public static readonly string Version = "1";
+ [Serializable]
+ public class AsyncStateInfo
+ {
+ // used for serialization compatibility
+ public static readonly string Version = "1";
- public object Progress
- {
- get;
- set;
- }
+ public object Progress
+ {
+ get;
+ set;
+ }
- public DateTime CreatedOnUtc
- {
- get;
- set;
- }
+ public DateTime CreatedOnUtc
+ {
+ get;
+ set;
+ }
- public DateTime LastAccessUtc
- {
- get;
- set;
- }
+ public DateTime LastAccessUtc
+ {
+ get;
+ set;
+ }
- public TimeSpan Duration
- {
- get;
- set;
- }
- }
+ public TimeSpan Duration
+ {
+ get;
+ set;
+ }
+ }
}
diff --git a/src/Libraries/SmartStore.Core/Async/IAsyncState.cs b/src/Libraries/SmartStore.Core/Async/IAsyncState.cs
index 3c4033d951..232ef299d3 100644
--- a/src/Libraries/SmartStore.Core/Async/IAsyncState.cs
+++ b/src/Libraries/SmartStore.Core/Async/IAsyncState.cs
@@ -4,12 +4,12 @@
namespace SmartStore.Core.Async
{
- ///
+ ///
/// Stores status information about long-running processes.
///
public interface IAsyncState
- {
- ///
+ {
+ ///
/// Checks whether a status object exists.
///
/// The type of status to check for.
@@ -73,5 +73,5 @@ public interface IAsyncState
bool Cancel(string name = null);
- }
+ }
}
diff --git a/src/Libraries/SmartStore.Core/Async/LocalAsyncState.cs b/src/Libraries/SmartStore.Core/Async/LocalAsyncState.cs
index e34800f3d2..ba66c15bf0 100644
--- a/src/Libraries/SmartStore.Core/Async/LocalAsyncState.cs
+++ b/src/Libraries/SmartStore.Core/Async/LocalAsyncState.cs
@@ -2,98 +2,96 @@
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Caching;
-using System.Text;
using System.Threading;
-using System.Threading.Tasks;
namespace SmartStore.Core.Async
{
- public partial class LocalAsyncState : IAsyncState
- {
- private readonly MemoryCache _states = new MemoryCache("SmartStore.AsyncState.Progress");
- private readonly MemoryCache _cancelTokens = new MemoryCache("SmartStore.AsyncState.CancelTokenSources");
-
- public virtual bool Exists(string name = null)
- {
- var value = GetStateInfo(name);
- return value != null && !object.Equals(value.Progress, default(T));
-
- }
-
- public virtual T Get(string name = null)
- {
- var value = GetStateInfo(name);
-
- if (value != null)
- {
- return (T)value.Progress;
- }
-
- return default(T);
- }
-
- public virtual IEnumerable GetAll()
- {
- var keyPrefix = BuildKey(null);
- return _states
- .Where(x => x.Key.StartsWith(keyPrefix))
- .Select(x => x.Value)
- .OfType()
- .Select(x => x.Progress)
- .OfType();
- }
-
-
- public virtual void Set(T state, string name = null, bool neverExpires = false)
- {
- Guard.NotNull(state, nameof(state));
-
- var value = GetStateInfo(name);
-
- if (value != null)
- {
- // exists already, so update
- if (state != null)
- {
- value.Progress = state;
- }
- }
- else
- {
- // add new entry
- var duration = neverExpires ? TimeSpan.Zero : TimeSpan.FromMinutes(15);
- var policy = new CacheItemPolicy
- {
+ public partial class LocalAsyncState : IAsyncState
+ {
+ private readonly MemoryCache _states = new MemoryCache("SmartStore.AsyncState.Progress");
+ private readonly MemoryCache _cancelTokens = new MemoryCache("SmartStore.AsyncState.CancelTokenSources");
+
+ public virtual bool Exists(string name = null)
+ {
+ var value = GetStateInfo(name);
+ return value != null && !object.Equals(value.Progress, default(T));
+
+ }
+
+ public virtual T Get(string name = null)
+ {
+ var value = GetStateInfo(name);
+
+ if (value != null)
+ {
+ return (T)value.Progress;
+ }
+
+ return default(T);
+ }
+
+ public virtual IEnumerable GetAll()
+ {
+ var keyPrefix = BuildKey(null);
+ return _states
+ .Where(x => x.Key.StartsWith(keyPrefix))
+ .Select(x => x.Value)
+ .OfType()
+ .Select(x => x.Progress)
+ .OfType();
+ }
+
+
+ public virtual void Set(T state, string name = null, bool neverExpires = false)
+ {
+ Guard.NotNull(state, nameof(state));
+
+ var value = GetStateInfo(name);
+
+ if (value != null)
+ {
+ // exists already, so update
+ if (state != null)
+ {
+ value.Progress = state;
+ }
+ }
+ else
+ {
+ // add new entry
+ var duration = neverExpires ? TimeSpan.Zero : TimeSpan.FromMinutes(15);
+ var policy = new CacheItemPolicy
+ {
SlidingExpiration = duration,
Priority = CacheItemPriority.NotRemovable
};
- var key = BuildKey(name);
+ var key = BuildKey(name);
// On expiration or removal: remove corresponding cancel token also.
policy.RemovedCallback = (x) => OnRemoveCancelTokenSource(key);
_states.Set(key, new AsyncStateInfo { Progress = state, Duration = duration }, policy);
- }
- }
-
- public virtual void Update(Action update, string name = null)
- {
- Guard.NotNull(update, nameof(update));
-
- var value = GetStateInfo(name);
-
- if (value != null)
- {
- var state = (T)value.Progress;
- if (state != null)
- {
- update(state);
- }
- }
- }
-
- public virtual bool Remove(string name = null)
- {
+ }
+ }
+
+ public virtual void Update(Action update, string name = null)
+ {
+ Guard.NotNull(update, nameof(update));
+
+ var value = GetStateInfo(name);
+
+ if (value != null)
+ {
+ var state = (T)value.Progress;
+ if (state != null)
+ {
+ update(state);
+ }
+ }
+ }
+
+ public virtual bool Remove(string name = null)
+ {
var key = BuildKey(name);
if (!OnRemoveStateInfo(key))
@@ -104,14 +102,14 @@ public virtual bool Remove(string name = null)
OnRemoveCancelTokenSource(key);
return false;
}
-
+
return true;
- }
+ }
- protected virtual bool OnRemoveStateInfo(string key)
- {
- return _states.Remove(key) != null;
- }
+ protected virtual bool OnRemoveStateInfo(string key)
+ {
+ return _states.Remove(key) != null;
+ }
public virtual bool RemoveCancelTokenSource(string name = null)
{
@@ -119,83 +117,83 @@ public virtual bool RemoveCancelTokenSource(string name = null)
}
protected virtual bool OnRemoveCancelTokenSource(string key, bool successive = false)
- {
- Guard.NotEmpty(key, nameof(key));
+ {
+ Guard.NotEmpty(key, nameof(key));
- var token = _cancelTokens.Remove(key) as CancellationTokenSource;
+ var token = _cancelTokens.Remove(key) as CancellationTokenSource;
- if (token != null)
- {
- token.Dispose();
- return true;
- }
+ if (token != null)
+ {
+ token.Dispose();
+ return true;
+ }
- return false;
- }
+ return false;
+ }
- public CancellationTokenSource GetCancelTokenSource(string name = null)
- {
- return OnGetCancelTokenSource(BuildKey(name));
- }
+ public CancellationTokenSource GetCancelTokenSource(string name = null)
+ {
+ return OnGetCancelTokenSource(BuildKey(name));
+ }
- protected virtual CancellationTokenSource OnGetCancelTokenSource(string key, bool successive = false)
- {
- Guard.NotEmpty(key, nameof(key));
+ protected virtual CancellationTokenSource OnGetCancelTokenSource(string key, bool successive = false)
+ {
+ Guard.NotEmpty(key, nameof(key));
- return _cancelTokens.Get(key) as CancellationTokenSource;
- }
+ return _cancelTokens.Get(key) as CancellationTokenSource;
+ }
- public virtual void SetCancelTokenSource(CancellationTokenSource cancelTokenSource, string name = null)
- {
- Guard.NotNull(cancelTokenSource, nameof(cancelTokenSource));
+ public virtual void SetCancelTokenSource(CancellationTokenSource cancelTokenSource, string name = null)
+ {
+ Guard.NotNull(cancelTokenSource, nameof(cancelTokenSource));
- var key = BuildKey(name);
+ var key = BuildKey(name);
- if (Exists(name))
- {
- OnRemoveCancelTokenSource(key);
- }
+ if (Exists(name))
+ {
+ OnRemoveCancelTokenSource(key);
+ }
- var policy = new CacheItemPolicy { Priority = CacheItemPriority.NotRemovable };
+ var policy = new CacheItemPolicy { Priority = CacheItemPriority.NotRemovable };
- _cancelTokens.Set(key, cancelTokenSource, policy);
- }
+ _cancelTokens.Set(key, cancelTokenSource, policy);
+ }
- public bool Cancel(string name = null)
- {
- return OnCancel(BuildKey(name));
- }
+ public bool Cancel(string name = null)
+ {
+ return OnCancel(BuildKey(name));
+ }
- protected virtual bool OnCancel(string key, bool successive = false)
- {
- Guard.NotEmpty(key, nameof(key));
+ protected virtual bool OnCancel(string key, bool successive = false)
+ {
+ Guard.NotEmpty(key, nameof(key));
- var cts = OnGetCancelTokenSource(key);
+ var cts = OnGetCancelTokenSource(key);
- if (cts != null)
- {
- cts.Cancel();
- return true;
- }
+ if (cts != null)
+ {
+ cts.Cancel();
+ return true;
+ }
- return false;
- }
+ return false;
+ }
- protected virtual AsyncStateInfo GetStateInfo(string name = null)
- {
- return _states.Get(BuildKey(name)) as AsyncStateInfo;
- }
+ protected virtual AsyncStateInfo GetStateInfo(string name = null)
+ {
+ return _states.Get(BuildKey(name)) as AsyncStateInfo;
+ }
- protected string BuildKey(string name)
- {
- return BuildKey(typeof(T), name);
- }
+ protected string BuildKey(string name)
+ {
+ return BuildKey(typeof(T), name);
+ }
- protected virtual string BuildKey(Type type, string name)
- {
- return "{0}{1}".FormatInvariant(type.FullName, name.HasValue() ? ":" + name : "");
- }
- }
+ protected virtual string BuildKey(Type type, string name)
+ {
+ return "{0}{1}".FormatInvariant(type.FullName, name.HasValue() ? ":" + name : "");
+ }
+ }
}
diff --git a/src/Libraries/SmartStore.Core/BaseEntity.cs b/src/Libraries/SmartStore.Core/BaseEntity.cs
index deed799cc7..e2882f9851 100644
--- a/src/Libraries/SmartStore.Core/BaseEntity.cs
+++ b/src/Libraries/SmartStore.Core/BaseEntity.cs
@@ -1,106 +1,104 @@
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.CompilerServices;
using System.Runtime.Serialization;
namespace SmartStore.Core
-{
+{
///
/// Base class for entities
///
[DataContract]
public abstract partial class BaseEntity : IEquatable
- {
- ///
+ {
+ ///
/// Gets or sets the entity identifier
///
- [DataMember]
- [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ [DataMember]
+ [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
- [SuppressMessage("ReSharper", "PossibleNullReferenceException")]
+ [SuppressMessage("ReSharper", "PossibleNullReferenceException")]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public virtual string GetEntityName()
- {
- return GetUnproxiedType().Name;
- }
+ {
+ return GetUnproxiedType().Name;
+ }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Type GetUnproxiedType()
{
- #region Old
- //var t = GetType();
- //if (t.AssemblyQualifiedName.StartsWith("System.Data.Entity."))
- //{
- // // it's a proxied type
- // t = t.BaseType;
- //}
-
- //return t;
- #endregion
-
- return ObjectContext.GetObjectType(GetType());
- }
-
- ///
- /// Transient objects are not associated with an item already in storage. For instance,
- /// a Product entity is transient if its Id is 0.
- ///
- public virtual bool IsTransientRecord()
- {
- return Id == 0;
- }
-
- public override bool Equals(object obj)
- {
- return this.Equals(obj as BaseEntity);
- }
-
- bool IEquatable.Equals(BaseEntity other)
- {
- return this.Equals(other);
- }
-
- protected virtual bool Equals(BaseEntity other)
- {
- if (other == null)
- return false;
-
- if (ReferenceEquals(this, other))
- return true;
-
- if (HasSameNonDefaultIds(other))
- {
- var otherType = other.GetUnproxiedType();
- var thisType = GetUnproxiedType();
- return thisType.Equals(otherType);
- }
-
- return false;
- }
-
- [SuppressMessage("ReSharper", "NonReadonlyMemberInGetHashCode")]
- public override int GetHashCode()
+ #region Old
+ //var t = GetType();
+ //if (t.AssemblyQualifiedName.StartsWith("System.Data.Entity."))
+ //{
+ // // it's a proxied type
+ // t = t.BaseType;
+ //}
+
+ //return t;
+ #endregion
+
+ return ObjectContext.GetObjectType(GetType());
+ }
+
+ ///
+ /// Transient objects are not associated with an item already in storage. For instance,
+ /// a Product entity is transient if its Id is 0.
+ ///
+ public virtual bool IsTransientRecord()
+ {
+ return Id == 0;
+ }
+
+ public override bool Equals(object obj)
+ {
+ return this.Equals(obj as BaseEntity);
+ }
+
+ bool IEquatable.Equals(BaseEntity other)
+ {
+ return this.Equals(other);
+ }
+
+ protected virtual bool Equals(BaseEntity other)
{
- if (IsTransientRecord())
- {
- return base.GetHashCode();
- }
- else
- {
- unchecked
- {
- // It's possible for two objects to return the same hash code based on
- // identically valued properties, even if they're of two different types,
- // so we include the object's type in the hash calculation
- var hashCode = GetUnproxiedType().GetHashCode();
- return (hashCode * 31) ^ Id.GetHashCode();
- }
- }
+ if (other == null)
+ return false;
+
+ if (ReferenceEquals(this, other))
+ return true;
+
+ if (HasSameNonDefaultIds(other))
+ {
+ var otherType = other.GetUnproxiedType();
+ var thisType = GetUnproxiedType();
+ return thisType.Equals(otherType);
+ }
+
+ return false;
+ }
+
+ [SuppressMessage("ReSharper", "NonReadonlyMemberInGetHashCode")]
+ public override int GetHashCode()
+ {
+ if (IsTransientRecord())
+ {
+ return base.GetHashCode();
+ }
+ else
+ {
+ unchecked
+ {
+ // It's possible for two objects to return the same hash code based on
+ // identically valued properties, even if they're of two different types,
+ // so we include the object's type in the hash calculation
+ var hashCode = GetUnproxiedType().GetHashCode();
+ return (hashCode * 31) ^ Id.GetHashCode();
+ }
+ }
}
public static bool operator ==(BaseEntity x, BaseEntity y)
@@ -113,9 +111,9 @@ public override int GetHashCode()
return !(x == y);
}
- private bool HasSameNonDefaultIds(BaseEntity other)
- {
- return !this.IsTransientRecord() && !other.IsTransientRecord() && this.Id == other.Id;
- }
- }
+ private bool HasSameNonDefaultIds(BaseEntity other)
+ {
+ return !this.IsTransientRecord() && !other.IsTransientRecord() && this.Id == other.Id;
+ }
+ }
}
diff --git a/src/Libraries/SmartStore.Core/Caching/CacheScope.cs b/src/Libraries/SmartStore.Core/Caching/CacheScope.cs
index b705ab10b6..b983a2a6cf 100644
--- a/src/Libraries/SmartStore.Core/Caching/CacheScope.cs
+++ b/src/Libraries/SmartStore.Core/Caching/CacheScope.cs
@@ -1,41 +1,34 @@
-using System;
-using System.Collections.Generic;
+using System.Collections.Generic;
using System.Linq;
namespace SmartStore.Core.Caching
{
- public class CacheScope
- {
- private HashSet _dependencies;
+ public class CacheScope
+ {
+ private HashSet _dependencies;
- public CacheScope(string key)
- {
- Key = key;
- }
+ public CacheScope(string key)
+ {
+ Key = key;
+ }
- public string Key { get; private set; }
+ public string Key { get; private set; }
- public void AddDependency(string key)
- {
- if (key == null)
- {
- return;
- }
+ public void AddDependency(string key)
+ {
+ if (key == null)
+ {
+ return;
+ }
- if (_dependencies == null)
- {
- _dependencies = new HashSet();
- }
+ if (_dependencies == null)
+ {
+ _dependencies = new HashSet();
+ }
- _dependencies.Add(key);
- }
+ _dependencies.Add(key);
+ }
- public IEnumerable Dependencies
- {
- get
- {
- return _dependencies ?? Enumerable.Empty();
- }
- }
- }
+ public IEnumerable Dependencies => _dependencies ?? Enumerable.Empty();
+ }
}
diff --git a/src/Libraries/SmartStore.Core/Caching/CacheScopeAccessor.cs b/src/Libraries/SmartStore.Core/Caching/CacheScopeAccessor.cs
index b04e0a7d45..b8e23759a9 100644
--- a/src/Libraries/SmartStore.Core/Caching/CacheScopeAccessor.cs
+++ b/src/Libraries/SmartStore.Core/Caching/CacheScopeAccessor.cs
@@ -5,58 +5,58 @@
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;
- }
- }
+ 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 3491021f32..43931b9edd 100644
--- a/src/Libraries/SmartStore.Core/Caching/ICacheManager.cs
+++ b/src/Libraries/SmartStore.Core/Caching/ICacheManager.cs
@@ -9,60 +9,60 @@ namespace SmartStore.Core.Caching
///
public interface ICacheManager
{
- ///
- /// Gets a cache item associated with the specified key
- ///
- /// 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, bool independent = false);
+ ///
+ /// Gets a cache item associated with the specified key
+ ///
+ /// 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, bool independent = false);
- ///
- /// Gets a cache item associated with the specified key or adds the item
- /// if it doesn't exist in the cache.
- ///
- /// The type of the item to get or add
- /// 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.
- /// When false, an exception will be thrown when the acquirer tries to acces the same cache item.
- /// Cached item value
- T Get(string key, Func acquirer, TimeSpan? duration = null, bool independent = false, bool allowRecursion = false);
+ ///
+ /// Gets a cache item associated with the specified key or adds the item
+ /// if it doesn't exist in the cache.
+ ///
+ /// The type of the item to get or add
+ /// 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.
+ /// When false, an exception will be thrown when the acquirer tries to acces the same cache item.
+ /// Cached item value
+ T Get(string key, Func acquirer, TimeSpan? duration = null, bool independent = false, bool allowRecursion = false);
- ///
- /// Gets a cache item associated with the specified key or - if it doesn't exist in the cache -
- /// adds the item obtained from the asynchronous acquirer .
- ///
- /// The type of the item to get or add
- /// 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.
- /// When false, an exception will be thrown when the acquirer tries to acces the same cache item.
- /// Cached item value
- Task GetAsync(string key, Func> acquirer, TimeSpan? duration = null, bool independent = false, bool allowRecursion = false);
+ ///
+ /// Gets a cache item associated with the specified key or - if it doesn't exist in the cache -
+ /// adds the item obtained from the asynchronous acquirer .
+ ///
+ /// The type of the item to get or add
+ /// 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.
+ /// When false, an exception will be thrown when the acquirer tries to acces the same cache item.
+ /// Cached item value
+ Task GetAsync(string key, Func> acquirer, TimeSpan? duration = null, bool independent = false, bool allowRecursion = 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, Func> acquirer = null);
+ ///
+ /// 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, Func> acquirer = null);
- ///
- /// Adds a cache item with the specified key
- ///
- /// Key
- /// Value
- /// Absolute expiration time
- ///
- /// 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);
+ ///
+ /// Adds a cache item with the specified key
+ ///
+ /// Key
+ /// Value
+ /// Absolute expiration time
+ ///
+ /// 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
@@ -77,44 +77,44 @@ public interface ICacheManager
/// /key
void Remove(string key);
- ///
- /// Scans for all keys matching the input pattern
- ///
- /// A key pattern. Can be null.
- /// An array of matching key names
- ///
- /// Supported glob-style patterns:
- /// - h?llo matches hello, hallo and hxllo
- /// - h*llo matches hllo and heeeello
- /// - h[ae]llo matches hello and hallo, but not hillo
- /// - h[^e]llo matches hallo, hbllo, ... but not hello
- /// - h[a-b]llo matches hallo and hbllo
- ///
- IEnumerable Keys(string pattern);
+ ///
+ /// Scans for all keys matching the input pattern
+ ///
+ /// A key pattern. Can be null.
+ /// An array of matching key names
+ ///
+ /// Supported glob-style patterns:
+ /// - h?llo matches hello, hallo and hxllo
+ /// - h*llo matches hllo and heeeello
+ /// - h[ae]llo matches hello and hallo, but not hillo
+ /// - h[^e]llo matches hallo, hbllo, ... but not hello
+ /// - h[a-b]llo matches hallo and hbllo
+ ///
+ IEnumerable Keys(string pattern);
- ///
- /// Removes all entries with keys matching the input pattern
- ///
- /// Glob pattern
- /// Count of removed cache items
- ///
- /// Supported glob-style patterns:
- /// - h?llo matches hello, hallo and hxllo
- /// - h*llo matches hllo and heeeello
- /// - h[ae]llo matches hello and hallo, but not hillo
- /// - h[^e]llo matches hallo, hbllo, ... but not hello
- /// - h[a-b]llo matches hallo and hbllo
- ///
- int RemoveByPattern(string pattern);
+ ///
+ /// Removes all entries with keys matching the input pattern
+ ///
+ /// Glob pattern
+ /// Count of removed cache items
+ ///
+ /// Supported glob-style patterns:
+ /// - h?llo matches hello, hallo and hxllo
+ /// - h*llo matches hllo and heeeello
+ /// - h[ae]llo matches hello and hallo, but not hillo
+ /// - h[^e]llo matches hallo, hbllo, ... but not hello
+ /// - h[a-b]llo matches hallo and hbllo
+ ///
+ int RemoveByPattern(string pattern);
///
/// Clear all cache data
///
void Clear();
- ///
- /// Gets a value indicating whether the cache is distributed (e.g. Redis)
- ///
- bool IsDistributedCache { get; }
+ ///
+ /// Gets a value indicating whether the cache is distributed (e.g. Redis)
+ ///
+ bool IsDistributedCache { get; }
}
}
diff --git a/src/Libraries/SmartStore.Core/Caching/ICacheScopeAccessor.cs b/src/Libraries/SmartStore.Core/Caching/ICacheScopeAccessor.cs
index a640e6b200..20d219e631 100644
--- a/src/Libraries/SmartStore.Core/Caching/ICacheScopeAccessor.cs
+++ b/src/Libraries/SmartStore.Core/Caching/ICacheScopeAccessor.cs
@@ -2,33 +2,33 @@
namespace SmartStore.Core.Caching
{
- public interface ICacheScopeAccessor
- {
- ///
- /// Returns the current cache acquire scope (for current thread)
- ///
- CacheScope Current { get; }
+ 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);
+ ///
+ /// 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);
+ ///
+ /// 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);
- }
+ ///
+ /// 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/IRequestCache.cs b/src/Libraries/SmartStore.Core/Caching/IRequestCache.cs
index 2ecd14f0b8..e37a607bff 100644
--- a/src/Libraries/SmartStore.Core/Caching/IRequestCache.cs
+++ b/src/Libraries/SmartStore.Core/Caching/IRequestCache.cs
@@ -1,5 +1,4 @@
using System;
-using System.Collections.Generic;
namespace SmartStore.Core.Caching
{
@@ -7,31 +6,31 @@ namespace SmartStore.Core.Caching
/// Request cache interface
///
public interface IRequestCache
- {
- ///
- /// Gets a cache item associated with the specified key
- ///
- /// The type of the item to get
- /// The cache item key
- /// Cached item value or null if item with specified key does not exist in the cache
- T Get(string key);
-
- ///
- /// Gets a cache item associated with the specified key or adds the item
- /// if it doesn't exist in the cache.
- ///
- /// The type of the item to get or add
- /// The cache item key
- /// Func which returns value to be added to the cache
- /// Cached item value
- T Get(string key, Func acquirer);
-
- ///
- /// Adds a cache item with the specified key
- ///
- /// Key
- /// Value
- void Put(string key, object value);
+ {
+ ///
+ /// Gets a cache item associated with the specified key
+ ///
+ /// The type of the item to get
+ /// The cache item key
+ /// Cached item value or null if item with specified key does not exist in the cache
+ T Get(string key);
+
+ ///
+ /// Gets a cache item associated with the specified key or adds the item
+ /// if it doesn't exist in the cache.
+ ///
+ /// The type of the item to get or add
+ /// The cache item key
+ /// Func which returns value to be added to the cache
+ /// Cached item value
+ T Get(string key, Func acquirer);
+
+ ///
+ /// Adds a cache item with the specified key
+ ///
+ /// Key
+ /// Value
+ void Put(string key, object value);
///
/// Gets a value indicating whether the value associated with the specified key is cached
@@ -46,63 +45,60 @@ public interface IRequestCache
/// /key
void Remove(string key);
- ///
- /// Removes items by pattern
- ///
- /// pattern
- void RemoveByPattern(string pattern);
-
- ///
- /// Clear all cache data
- ///
- void Clear();
- }
-
- ///
- /// For testing purposes
- ///
- public class NullRequestCache : IRequestCache
- {
- private static readonly IRequestCache s_instance = new NullRequestCache();
-
- public static IRequestCache Instance
- {
- get { return s_instance; }
- }
-
- public void Clear()
- {
- }
-
- public bool Contains(string key)
- {
- return false;
- }
-
- public T Get(string key)
- {
- return default(T);
- }
-
- public T Get(string key, Func acquirer)
- {
- if (acquirer == null)
- {
- return default(T);
- }
- return acquirer();
- }
-
- public void Remove(string key)
- {
- }
-
- public void RemoveByPattern(string pattern)
- {
- }
-
- public void Put(string key, object value)
- {
- }
- }
+ ///
+ /// Removes items by pattern
+ ///
+ /// pattern
+ void RemoveByPattern(string pattern);
+
+ ///
+ /// Clear all cache data
+ ///
+ void Clear();
+ }
+
+ ///
+ /// For testing purposes
+ ///
+ public class NullRequestCache : IRequestCache
+ {
+ private static readonly IRequestCache s_instance = new NullRequestCache();
+
+ public static IRequestCache Instance => s_instance;
+
+ public void Clear()
+ {
+ }
+
+ public bool Contains(string key)
+ {
+ return false;
+ }
+
+ public T Get(string key)
+ {
+ return default(T);
+ }
+
+ public T Get(string key, Func acquirer)
+ {
+ if (acquirer == null)
+ {
+ return default(T);
+ }
+ return acquirer();
+ }
+
+ public void Remove(string key)
+ {
+ }
+
+ public void RemoveByPattern(string pattern)
+ {
+ }
+
+ public void Put(string key, object value)
+ {
+ }
+ }
}
diff --git a/src/Libraries/SmartStore.Core/Caching/ISet.cs b/src/Libraries/SmartStore.Core/Caching/ISet.cs
index d0bff72bd7..3c2c7ff2b1 100644
--- a/src/Libraries/SmartStore.Core/Caching/ISet.cs
+++ b/src/Libraries/SmartStore.Core/Caching/ISet.cs
@@ -1,21 +1,20 @@
-using System;
-using System.Collections.Generic;
+using System.Collections.Generic;
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);
- bool Move(string destinationKey, string item);
+ public interface ISet : IEnumerable
+ {
+ bool Add(string item);
+ void AddRange(IEnumerable items);
+ void Clear();
+ bool Contains(string item);
+ bool Remove(string item);
+ bool Move(string destinationKey, string item);
- long UnionWith(params string[] keys);
- long IntersectWith(params string[] keys);
- long ExceptWith(params string[] keys);
+ long UnionWith(params string[] keys);
+ long IntersectWith(params string[] keys);
+ long ExceptWith(params string[] keys);
- int Count { get; }
- }
+ int Count { get; }
+ }
}
diff --git a/src/Libraries/SmartStore.Core/Caching/MemoryCacheManager.cs b/src/Libraries/SmartStore.Core/Caching/MemoryCacheManager.cs
index 78087b3f38..332b199c61 100644
--- a/src/Libraries/SmartStore.Core/Caching/MemoryCacheManager.cs
+++ b/src/Libraries/SmartStore.Core/Caching/MemoryCacheManager.cs
@@ -1,259 +1,254 @@
using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
using System.Linq;
using System.Runtime.Caching;
-using System.Threading.Tasks;
-using System.Collections.Concurrent;
+using System.Runtime.CompilerServices;
+using System.Text.RegularExpressions;
using System.Threading;
-using System.Collections.Generic;
+using System.Threading.Tasks;
+using SmartStore.Core.Infrastructure.DependencyManagement;
using SmartStore.Utilities;
-using System.Text.RegularExpressions;
-using System.Diagnostics;
using SmartStore.Utilities.Threading;
-using SmartStore.Core.Infrastructure.DependencyManagement;
-using System.Runtime.CompilerServices;
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 Work _scopeAccessor;
- private MemoryCache _cache;
- private readonly ConcurrentDictionary _keyLocks = new ConcurrentDictionary();
-
- public MemoryCacheManager(Work scopeAccessor)
- {
- _scopeAccessor = scopeAccessor;
- _cache = CreateCache();
- }
-
- private MemoryCache CreateCache()
- {
- return new MemoryCache("SmartStore");
- }
-
- public bool IsDistributedCache
- {
- get { return false; }
- }
-
- private bool TryGet(string key, bool independent, out T value)
- {
- value = default(T);
-
- object obj = _cache.Get(key);
-
- if (obj != null)
- {
- // Make the parent scope's entry depend on this
- if (!independent)
- {
- _scopeAccessor.Value.PropagateKey(key);
- }
-
- if (obj.Equals(FakeNull))
- {
- return true;
- }
-
- value = (T)obj;
- return true;
- }
-
- return false;
- }
+ const string LockRecursionExceptionMessage = "Acquiring identical cache items recursively is not supported. Key: {0}";
+
+ // We 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 Work _scopeAccessor;
+ private MemoryCache _cache;
+
+ public MemoryCacheManager(Work scopeAccessor)
+ {
+ _scopeAccessor = scopeAccessor;
+ _cache = CreateCache();
+ }
+
+ private MemoryCache CreateCache()
+ {
+ return new MemoryCache("SmartStore");
+ }
+
+ public bool IsDistributedCache => false;
+
+ private bool TryGet(string key, bool independent, out T value)
+ {
+ value = default(T);
+
+ object obj = _cache.Get(key);
+
+ if (obj != null)
+ {
+ // Make the parent scope's entry depend on this
+ if (!independent)
+ {
+ _scopeAccessor.Value.PropagateKey(key);
+ }
+
+ if (obj.Equals(FakeNull))
+ {
+ return true;
+ }
+
+ value = (T)obj;
+ return true;
+ }
+
+ return false;
+ }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public T Get(string key, bool independent = false)
- {
- TryGet(key, independent, out T value);
- return value;
- }
+ {
+ TryGet(key, independent, out T value);
+ return value;
+ }
public T Get(string key, Func acquirer, TimeSpan? duration = null, bool independent = false, bool allowRecursion = false)
{
- if (TryGet(key, independent, out T value))
- {
- return value;
- }
-
- if (!allowRecursion && _scopeAccessor.Value.HasScope(key))
- {
- throw new LockRecursionException(LockRecursionExceptionMessage.FormatInvariant(key));
- }
-
- // Get the (semaphore) locker specific to this key
- using (KeyedLock.Lock("cache:" + key, TimeSpan.FromSeconds(5)))
- {
- // Atomic operation must be outer locked
- if (!TryGet(key, independent, out value))
- {
- var scope = !allowRecursion ? _scopeAccessor.Value.BeginScope(key) : ActionDisposable.Empty;
- using (scope)
- {
- value = acquirer();
- var dependencies = !allowRecursion ? _scopeAccessor.Value.Current?.Dependencies : (IEnumerable)null;
- Put(key, value, duration, dependencies);
- return value;
- }
- }
- }
-
- return value;
- }
-
- public async Task GetAsync(string key, Func> acquirer, TimeSpan? duration = null, bool independent = false, bool allowRecursion = false)
- {
- if (TryGet(key, independent, out T value))
- {
- return value;
- }
-
- if (!allowRecursion && _scopeAccessor.Value.HasScope(key))
- {
- throw new LockRecursionException(LockRecursionExceptionMessage.FormatInvariant(key));
- }
-
- // Get the async (semaphore) locker specific to this key
- using (await KeyedLock.LockAsync("cache:" + key, TimeSpan.FromMinutes(1)))
- {
- if (!TryGet(key, independent, out value))
- {
- var scope = !allowRecursion ? _scopeAccessor.Value.BeginScope(key) : ActionDisposable.Empty;
- using (scope)
- {
- value = await acquirer();
- var dependencies = !allowRecursion ? _scopeAccessor.Value.Current?.Dependencies : (IEnumerable)null;
- Put(key, value, duration, dependencies);
- return value;
- }
- }
- }
-
- return value;
- }
-
- public void Put(string key, object value, TimeSpan? duration = null, IEnumerable dependencies = null)
- {
- _cache.Set(key, value ?? FakeNull, GetCacheItemPolicy(duration, dependencies));
- }
+ if (TryGet(key, independent, out T value))
+ {
+ return value;
+ }
+
+ if (!allowRecursion && _scopeAccessor.Value.HasScope(key))
+ {
+ throw new LockRecursionException(LockRecursionExceptionMessage.FormatInvariant(key));
+ }
+
+ // Get the (semaphore) locker specific to this key
+ using (KeyedLock.Lock("cache:" + key, TimeSpan.FromSeconds(5)))
+ {
+ // Atomic operation must be outer locked
+ if (!TryGet(key, independent, out value))
+ {
+ var scope = !allowRecursion ? _scopeAccessor.Value.BeginScope(key) : ActionDisposable.Empty;
+ using (scope)
+ {
+ value = acquirer();
+ var dependencies = !allowRecursion ? _scopeAccessor.Value.Current?.Dependencies : (IEnumerable)null;
+ Put(key, value, duration, dependencies);
+ return value;
+ }
+ }
+ }
+
+ return value;
+ }
+
+ public async Task GetAsync(string key, Func> acquirer, TimeSpan? duration = null, bool independent = false, bool allowRecursion = false)
+ {
+ if (TryGet(key, independent, out T value))
+ {
+ return value;
+ }
+
+ if (!allowRecursion && _scopeAccessor.Value.HasScope(key))
+ {
+ throw new LockRecursionException(LockRecursionExceptionMessage.FormatInvariant(key));
+ }
+
+ // Get the async (semaphore) locker specific to this key
+ using (await KeyedLock.LockAsync("cache:" + key, TimeSpan.FromMinutes(1)))
+ {
+ if (!TryGet(key, independent, out value))
+ {
+ var scope = !allowRecursion ? _scopeAccessor.Value.BeginScope(key) : ActionDisposable.Empty;
+ using (scope)
+ {
+ value = await acquirer();
+ var dependencies = !allowRecursion ? _scopeAccessor.Value.Current?.Dependencies : (IEnumerable)null;
+ Put(key, value, duration, dependencies);
+ return value;
+ }
+ }
+ }
+
+ return value;
+ }
+
+ public void Put(string key, object value, TimeSpan? duration = null, IEnumerable dependencies = null)
+ {
+ _cache.Set(key, value ?? FakeNull, GetCacheItemPolicy(duration, dependencies));
+ }
public bool Contains(string key)
{
- return _cache.Contains(key);
+ return _cache.Contains(key);
}
public void Remove(string key)
{
- _cache.Remove(key);
+ _cache.Remove(key);
}
- public IEnumerable Keys(string pattern)
- {
- Guard.NotEmpty(pattern, nameof(pattern));
+ public IEnumerable Keys(string pattern)
+ {
+ Guard.NotEmpty(pattern, nameof(pattern));
- var keys = _cache.AsParallel().Select(x => x.Key);
+ var keys = _cache.AsParallel().Select(x => x.Key);
- if (pattern.IsEmpty() || pattern == "*")
- {
- return keys.ToArray();
- }
+ if (pattern.IsEmpty() || pattern == "*")
+ {
+ return keys.ToArray();
+ }
- var wildcard = new Wildcard(pattern, RegexOptions.IgnoreCase);
- return keys.Where(x => wildcard.IsMatch(x)).ToArray();
- }
+ var wildcard = new Wildcard(pattern, RegexOptions.IgnoreCase);
+ return keys.Where(x => wildcard.IsMatch(x)).ToArray();
+ }
- public int RemoveByPattern(string pattern)
+ public int RemoveByPattern(string pattern)
{
- lock (_cache)
- {
- var keysToRemove = Keys(pattern);
- int count = 0;
-
- // lock atomic operation
- foreach (string key in keysToRemove)
- {
- _cache.Remove(key);
- count++;
- }
-
- return count;
- }
- }
+ lock (_cache)
+ {
+ var keysToRemove = Keys(pattern);
+ int count = 0;
+
+ // lock atomic operation
+ foreach (string key in keysToRemove)
+ {
+ _cache.Remove(key);
+ count++;
+ }
+
+ return count;
+ }
+ }
public void Clear()
{
- // 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, Func> acquirer = null)
- {
- 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, IEnumerable dependencies)
- {
- var absoluteExpiration = ObjectCache.InfiniteAbsoluteExpiration;
-
- if (duration.HasValue)
- {
- 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)
- {
- if (disposing)
- _cache.Dispose();
- }
- }
+ // 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, Func> acquirer = null)
+ {
+ 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, IEnumerable dependencies)
+ {
+ var absoluteExpiration = ObjectCache.InfiniteAbsoluteExpiration;
+
+ if (duration.HasValue)
+ {
+ 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)
+ {
+ if (disposing)
+ _cache.Dispose();
+ }
+ }
}
\ No newline at end of file
diff --git a/src/Libraries/SmartStore.Core/Caching/MemorySet.cs b/src/Libraries/SmartStore.Core/Caching/MemorySet.cs
index d8aa392712..8c1e13245b 100644
--- a/src/Libraries/SmartStore.Core/Caching/MemorySet.cs
+++ b/src/Libraries/SmartStore.Core/Caching/MemorySet.cs
@@ -1,65 +1,63 @@
using System;
using System.Collections.Generic;
using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
namespace SmartStore.Core.Caching
{
- [Serializable]
- internal class MemorySet : HashSet, ISet
- {
- private readonly ICacheManager _cache;
+ [Serializable]
+ internal class MemorySet : HashSet, ISet
+ {
+ private readonly ICacheManager _cache;
- public MemorySet(ICacheManager cache)
+ public MemorySet(ICacheManager cache)
: base(StringComparer.OrdinalIgnoreCase)
- {
- _cache = cache;
- }
-
- public void AddRange(IEnumerable items)
- {
- base.UnionWith(items);
- }
-
- public bool Move(string destinationKey, string item)
- {
- if (this.Contains(item))
- {
- var target = _cache.GetHashSet(destinationKey);
- return target.Add(item);
- }
-
- return false;
- }
-
- public long ExceptWith(params string[] keys)
- {
- return Combine(x => this.Except(x), keys);
- }
-
- public long IntersectWith(params string[] keys)
- {
- return Combine(x => this.Intersect(x), keys);
- }
-
- public long UnionWith(params string[] keys)
- {
- return Combine(x => this.Union(x), keys);
- }
-
- private long Combine(Func, IEnumerable> func, params string[] keys)
- {
- if (keys.Length == 0)
- return 0;
-
- var other = keys.SelectMany(x => _cache.GetHashSet(x)).Distinct();
- var result = func(other);
-
- this.Clear();
- this.AddRange(other);
-
- return this.Count;
- }
- }
+ {
+ _cache = cache;
+ }
+
+ public void AddRange(IEnumerable items)
+ {
+ base.UnionWith(items);
+ }
+
+ public bool Move(string destinationKey, string item)
+ {
+ if (this.Contains(item))
+ {
+ var target = _cache.GetHashSet(destinationKey);
+ return target.Add(item);
+ }
+
+ return false;
+ }
+
+ public long ExceptWith(params string[] keys)
+ {
+ return Combine(x => this.Except(x), keys);
+ }
+
+ public long IntersectWith(params string[] keys)
+ {
+ return Combine(x => this.Intersect(x), keys);
+ }
+
+ public long UnionWith(params string[] keys)
+ {
+ return Combine(x => this.Union(x), keys);
+ }
+
+ private long Combine(Func, IEnumerable> func, params string[] keys)
+ {
+ if (keys.Length == 0)
+ return 0;
+
+ var other = keys.SelectMany(x => _cache.GetHashSet(x)).Distinct();
+ var result = func(other);
+
+ this.Clear();
+ this.AddRange(other);
+
+ return this.Count;
+ }
+ }
}
diff --git a/src/Libraries/SmartStore.Core/Caching/NullCache.cs b/src/Libraries/SmartStore.Core/Caching/NullCache.cs
index 548bdfa18c..8910d0b004 100644
--- a/src/Libraries/SmartStore.Core/Caching/NullCache.cs
+++ b/src/Libraries/SmartStore.Core/Caching/NullCache.cs
@@ -1,60 +1,49 @@
using System;
-using SmartStore.Utilities;
using System.Collections.Generic;
-using System.Linq;
using System.Threading.Tasks;
namespace SmartStore.Core.Caching
{
- ///
- /// Represents a null cache
- ///
public partial class NullCache : ICacheManager
{
- private static readonly ICacheManager s_instance = new NullCache();
+ private static readonly ICacheManager s_instance = new NullCache();
- public static ICacheManager Instance
- {
- get { return s_instance; }
- }
+ public static ICacheManager Instance => s_instance;
- public bool IsDistributedCache
- {
- get { return false; }
- }
+ public bool IsDistributedCache => false;
- public T Get(string key, bool independent = false)
- {
- return default(T);
- }
+ public T Get(string key, bool independent = false)
+ {
+ return default(T);
+ }
- public T Get(string key, Func acquirer, TimeSpan? duration = null, bool independent = false, bool allowRecursion = false)
- {
- if (acquirer == null)
- {
- return default(T);
- }
- return acquirer();
- }
+ public T Get(string key, Func acquirer, TimeSpan? duration = null, bool independent = false, bool allowRecursion = false)
+ {
+ if (acquirer == null)
+ {
+ return default(T);
+ }
+ return acquirer();
+ }
- public Task GetAsync(string key, Func> acquirer, TimeSpan? duration = null, bool independent = false, bool allowRecursion = false)
- {
- if (acquirer == null)
- {
- return Task.FromResult(default(T));
- }
- return acquirer();
- }
+ public Task GetAsync(string key, Func> acquirer, TimeSpan? duration = null, bool independent = false, bool allowRecursion = false)
+ {
+ if (acquirer == null)
+ {
+ return Task.FromResult(default(T));
+ }
+ return acquirer();
+ }
- public ISet GetHashSet(string key, Func> acquirer = null)
- {
- return new MemorySet(this);
- }
+ public ISet GetHashSet(string key, Func> acquirer = null)
+ {
+ return new MemorySet(this);
+ }
- public void Put(string key, object value, TimeSpan? duration = null, IEnumerable dependencies = null)
- {
- }
+ public void Put(string key, object value, TimeSpan? duration = null, IEnumerable dependencies = null)
+ {
+ }
public bool Contains(string key)
{
@@ -65,18 +54,18 @@ public void Remove(string key)
{
}
- public IEnumerable Keys(string pattern)
- {
- return new string[0];
- }
+ public IEnumerable Keys(string pattern)
+ {
+ return new string[0];
+ }
- public int RemoveByPattern(string pattern)
+ public int RemoveByPattern(string pattern)
{
- return 0;
+ return 0;
}
public void Clear()
{
}
- }
+ }
}
\ No newline at end of file
diff --git a/src/Libraries/SmartStore.Core/Caching/OutputCache/DisplayControl.cs b/src/Libraries/SmartStore.Core/Caching/OutputCache/DisplayControl.cs
index 28a05372d5..f43b485741 100644
--- a/src/Libraries/SmartStore.Core/Caching/OutputCache/DisplayControl.cs
+++ b/src/Libraries/SmartStore.Core/Caching/OutputCache/DisplayControl.cs
@@ -8,244 +8,249 @@
using SmartStore.Core.Domain.Catalog;
using SmartStore.Core.Domain.Discounts;
using SmartStore.Core.Domain.Localization;
+using SmartStore.Core.Domain.Media;
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
- {
- private static readonly ConcurrentDictionary _handlers = new ConcurrentDictionary
- {
- [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(ProductMediaFile)] = (x, c) => new[] { "p" + ((ProductMediaFile)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 IComponentContext _componentContext;
-
- private bool _isIdle;
- private bool? _isUncacheableRequest;
-
- 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)
- {
- 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;
- return new ActionDisposable(() => _isIdle = false);
- }
-
- public virtual void Announce(BaseEntity entity)
- {
- if (!_isIdle && entity != null)
- {
- _entities.Add(entity);
- }
- }
-
- public bool IsDisplayed(BaseEntity entity)
- {
- if (entity == null)
- return false;
-
- return _entities.Contains(entity);
- }
-
- public void MarkRequestAsUncacheable()
- {
- // First wins: subsequent calls should not be able to cancel this
- if (!_isIdle)
- _isUncacheableRequest = true;
- }
-
- public bool IsUncacheableRequest
- {
- get
- {
- return _isUncacheableRequest.GetValueOrDefault() == true;
- }
- }
-
- public virtual IEnumerable GetCacheControlTagsFor(BaseEntity entity)
- {
- var empty = Enumerable.Empty();
-
- if (entity == null || entity.IsTransientRecord())
- {
- return empty;
- }
-
- var type = entity.GetUnproxiedType();
-
- if (!_handlers.TryGetValue(type, out var handler))
- {
- return empty;
- }
-
- return handler.Invoke(entity, _componentContext);
- }
-
- public IEnumerable GetAllCacheControlTags()
- {
- var tags = _entities
- .Where(x => x.Id > 0)
- .SelectMany(x => GetCacheControlTagsFor(x))
- .Where(x => x != null)
- .Distinct()
- .ToArray();
-
- return tags;
- }
- }
+ public delegate IEnumerable DisplayControlHandler(BaseEntity entity, IComponentContext ctx);
+
+ public partial class DisplayControl : IDisplayControl
+ {
+ private static readonly ConcurrentDictionary _handlers = new ConcurrentDictionary
+ {
+ [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(ProductMediaFile)] = (x, c) => new[] { "p" + ((ProductMediaFile)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(MediaFile)] = (x, c) => new[] { "mf" + 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 IComponentContext _componentContext;
+
+ private bool _isIdle;
+ private bool? _isUncacheableRequest;
+
+ 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)
+ {
+ 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(BlogPost):
+ prefix = "b";
+ break;
+ case nameof(Product):
+ prefix = "p";
+ break;
+ case nameof(Category):
+ prefix = "c";
+ break;
+ case nameof(Manufacturer):
+ prefix = "m";
+ break;
+ case nameof(NewsItem):
+ prefix = "n";
+ break;
+ case nameof(Topic):
+ prefix = "t";
+ break;
+ case nameof(MediaFile):
+ prefix = "mf";
+ 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;
+ return new ActionDisposable(() => _isIdle = false);
+ }
+
+ public virtual void Announce(BaseEntity entity)
+ {
+ if (!_isIdle && entity != null)
+ {
+ _entities.Add(entity);
+ }
+ }
+
+ public bool IsDisplayed(BaseEntity entity)
+ {
+ if (entity == null)
+ return false;
+
+ return _entities.Contains(entity);
+ }
+
+ public void MarkRequestAsUncacheable()
+ {
+ // First wins: subsequent calls should not be able to cancel this
+ if (!_isIdle)
+ _isUncacheableRequest = true;
+ }
+
+ public bool IsUncacheableRequest => _isUncacheableRequest.GetValueOrDefault() == true;
+
+ public virtual IEnumerable GetCacheControlTagsFor(BaseEntity entity)
+ {
+ var empty = Enumerable.Empty();
+
+ if (entity == null || entity.IsTransientRecord())
+ {
+ return empty;
+ }
+
+ var type = entity.GetUnproxiedType();
+
+ if (!_handlers.TryGetValue(type, out var handler))
+ {
+ return empty;
+ }
+
+ return handler.Invoke(entity, _componentContext);
+ }
+
+ public IEnumerable GetAllCacheControlTags()
+ {
+ var tags = _entities
+ .Where(x => x.Id > 0)
+ .SelectMany(x => GetCacheControlTagsFor(x))
+ .Where(x => x != null)
+ .Distinct()
+ .ToArray();
+
+ return tags;
+ }
+ }
}
diff --git a/src/Libraries/SmartStore.Core/Caching/OutputCache/ICacheableRouteRegistrar.cs b/src/Libraries/SmartStore.Core/Caching/OutputCache/ICacheableRouteRegistrar.cs
index b30feeb1e4..4898095adb 100644
--- a/src/Libraries/SmartStore.Core/Caching/OutputCache/ICacheableRouteRegistrar.cs
+++ b/src/Libraries/SmartStore.Core/Caching/OutputCache/ICacheableRouteRegistrar.cs
@@ -1,21 +1,19 @@
-using System;
-
-namespace SmartStore.Core.Caching
+namespace SmartStore.Core.Caching
{
- public interface ICacheableRouteRegistrar
- {
- void RegisterCacheableRoute(params string[] routes);
- void RemoveCacheableRoute(params string[] routes);
- }
+ public interface ICacheableRouteRegistrar
+ {
+ void RegisterCacheableRoute(params string[] routes);
+ void RemoveCacheableRoute(params string[] routes);
+ }
- public class NullCacheableRouteRegistrar : ICacheableRouteRegistrar
- {
- public void RegisterCacheableRoute(params string[] routes)
- {
- }
+ public class NullCacheableRouteRegistrar : ICacheableRouteRegistrar
+ {
+ public void RegisterCacheableRoute(params string[] routes)
+ {
+ }
- public void RemoveCacheableRoute(params string[] routes)
- {
- }
- }
+ public void RemoveCacheableRoute(params string[] routes)
+ {
+ }
+ }
}
diff --git a/src/Libraries/SmartStore.Core/Caching/OutputCache/IDisplayControl.cs b/src/Libraries/SmartStore.Core/Caching/OutputCache/IDisplayControl.cs
index 9646cf649e..f9549bc92c 100644
--- a/src/Libraries/SmartStore.Core/Caching/OutputCache/IDisplayControl.cs
+++ b/src/Libraries/SmartStore.Core/Caching/OutputCache/IDisplayControl.cs
@@ -1,37 +1,34 @@
using System;
using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
namespace SmartStore.Core.Caching
{
- public interface IDisplayControl
- {
- void Announce(BaseEntity entity);
+ public interface IDisplayControl
+ {
+ void Announce(BaseEntity entity);
- bool IsDisplayed(BaseEntity entity);
+ bool IsDisplayed(BaseEntity entity);
- void MarkRequestAsUncacheable();
+ void MarkRequestAsUncacheable();
- bool IsUncacheableRequest { get; }
+ bool IsUncacheableRequest { get; }
- IEnumerable GetCacheControlTagsFor(BaseEntity entity);
+ IEnumerable GetCacheControlTagsFor(BaseEntity entity);
- IEnumerable GetAllCacheControlTags();
+ IEnumerable GetAllCacheControlTags();
- IDisposable BeginIdleScope();
- }
+ IDisposable BeginIdleScope();
+ }
- public static class IDisplayControlExtensions
- {
- public static void AnnounceRange(this IDisplayControl displayedEntities, IEnumerable entities)
- {
- if (entities != null)
- {
- entities.Each(x => displayedEntities.Announce(x));
- }
- }
- }
+ public static class IDisplayControlExtensions
+ {
+ public static void AnnounceRange(this IDisplayControl displayedEntities, IEnumerable entities)
+ {
+ if (entities != null)
+ {
+ entities.Each(x => displayedEntities.Announce(x));
+ }
+ }
+ }
}
diff --git a/src/Libraries/SmartStore.Core/Caching/OutputCache/IOutputCacheInvalidationObserver.cs b/src/Libraries/SmartStore.Core/Caching/OutputCache/IOutputCacheInvalidationObserver.cs
index 234b67ced2..069a0bd62c 100644
--- a/src/Libraries/SmartStore.Core/Caching/OutputCache/IOutputCacheInvalidationObserver.cs
+++ b/src/Libraries/SmartStore.Core/Caching/OutputCache/IOutputCacheInvalidationObserver.cs
@@ -9,154 +9,151 @@
namespace SmartStore.Core.Caching
{
- public class ObserveEntityContext
- {
- public IOutputCacheProvider OutputCacheProvider { get; set; }
- public IDisplayControl DisplayControl { get; set; }
- public BaseEntity Entity { get; set; }
- public IHookedEntity EntityEntry { get; set; }
- public bool Handled { get; set; }
- public ContainerManager ServiceContainer { get; set; }
- }
-
- ///
- /// Allows registration of output cache invalidation handlers
- ///
- public interface IOutputCacheInvalidationObserver
- {
- ///
- /// Registers an entity observer. The passed observer is responsible for invalidating the output cache
- /// by calling one of the invalidation methods in the instance.
- /// The observer must then set the property to true
- /// to signal the framework that it should skip executing subsequent observers.
- ///
- /// The observer action
- ///
- /// The implementation of this interface is singleton scoped.
- /// Don't use objects with shorter lifetime in your handler as this will lead to memory leaks.
- /// If your handler needs to call service methods, resolve required services
- /// with .
- ///
- void ObserveEntity(Action observer);
-
- IEnumerable> GetEntityObservers();
-
- ///
- /// Registers a setting key to be observed by the framework. If the value for the passed
- /// setting key changes, the framework calls the handler.
- /// The key can either be fully qualified - e.g. "CatalogSettings.ShowProductSku" -,
- /// or prefixed - e.g. "CatalogSettings.*". The latter calls the invalidator when ANY CatalogSetting changes.
- ///
- ///
- /// The invalidation action handler. If null is passed, the framework
- /// uses the default invalidator, which is .
- ///
- void ObserveSetting(string settingKey, Action invalidationAction);
-
- Action GetInvalidationActionForSetting(string settingKey);
- }
-
- public class NullOutputCacheInvalidationObserver : IOutputCacheInvalidationObserver
- {
- private static readonly IOutputCacheInvalidationObserver _instance = new NullOutputCacheInvalidationObserver();
-
- public static IOutputCacheInvalidationObserver Instance
- {
- get { return _instance; }
- }
-
- public void ObserveEntity(Action observer)
- {
- }
-
- public IEnumerable> GetEntityObservers()
- {
- return Enumerable.Empty>();
- }
-
- public void ObserveSetting(string settingKey, Action invalidationAction)
- {
- }
-
- public Action GetInvalidationActionForSetting(string settingKey)
- {
- return null;
- }
- }
-
- public static class IOutputCacheInvalidationObserverExtensions
- {
- public static void ObserveSetting(this IOutputCacheInvalidationObserver observer, string settingKey)
- {
- observer.ObserveSetting(settingKey, null);
- }
-
- ///
- /// Registers a concrete setting class to be observed by the framework. If any setting property
- /// of changes, the framework will purge the cache.
- ///
- /// The type of the concrete setting class to observe
- ///
- /// A property observer precedes a class observer.
- ///
- public static void ObserveSettings(this IOutputCacheInvalidationObserver observer)
- where TSetting : ISettings
- {
- ObserveSettings(observer, null);
- }
-
- ///
- /// Registers a concrete setting class to be observed by the framework. If any setting property
- /// of changes, the framework will call the handler.
- ///
- /// The type of the concrete setting class to observe
- ///
- /// The invalidation action handler. If null is passed, the framework
- /// uses the default invalidator, which is .
- ///
- ///
- /// A property observer precedes a class observer.
- ///
- public static void ObserveSettings(
- this IOutputCacheInvalidationObserver observer,
- Action invalidationAction) where TSetting : ISettings
- {
- var key = typeof(TSetting).Name + ".*";
- observer.ObserveSetting(key, invalidationAction);
- }
-
- ///
- /// Registers a setting property to be observed by the framework. If the value for the passed
- /// property changes, the framework will purge the cache.
- ///
- /// The type of the concrete setting class which contains the property
- /// The property lambda
- public static void ObserveSettingProperty(
- this IOutputCacheInvalidationObserver observer,
- Expression> propertyAccessor) where TSetting : ISettings
- {
- ObserveSettingProperty(observer, propertyAccessor, null);
- }
-
- ///
- /// Registers a setting property to be observed by the framework. If the value for the passed
- /// property changes, the framework will call the handler.
- ///
- /// The type of the concrete setting class which contains the property
- /// The property lambda
- ///
- /// The invalidation action handler. If null is passed, the framework
- /// uses the default invalidator, which is .
- ///
- public static void ObserveSettingProperty(
- this IOutputCacheInvalidationObserver observer,
- Expression> propertyAccessor,
- Action invalidationAction) where TSetting : ISettings
- {
- Guard.NotNull(propertyAccessor, nameof(propertyAccessor));
-
- var key = TypeHelper.NameOf(propertyAccessor, true);
- observer.ObserveSetting(key, invalidationAction);
- }
- }
+ public class ObserveEntityContext
+ {
+ public IOutputCacheProvider OutputCacheProvider { get; set; }
+ public IDisplayControl DisplayControl { get; set; }
+ public BaseEntity Entity { get; set; }
+ public IHookedEntity EntityEntry { get; set; }
+ public bool Handled { get; set; }
+ public ContainerManager ServiceContainer { get; set; }
+ }
+
+ ///
+ /// Allows registration of output cache invalidation handlers
+ ///
+ public interface IOutputCacheInvalidationObserver
+ {
+ ///
+ /// Registers an entity observer. The passed observer is responsible for invalidating the output cache
+ /// by calling one of the invalidation methods in the