diff --git a/.gitignore b/.gitignore
index 118d11be55..85bd82b7ff 100644
--- a/.gitignore
+++ b/.gitignore
@@ -300,6 +300,7 @@ Kopie von*
/src/Presentation/SmartStore.Web/App_Data/ImportProfiles/
/src/Presentation/SmartStore.Web/App_Data/Indexing/
/src/Presentation/SmartStore.Web/App_Data/Lucene/
+/src/Presentation/SmartStore.Web/App_Data/EfCache/
/src/Presentation/SmartStore.Web/App_Data/Settings.txt
/src/Presentation/SmartStore.Web/App_Data/InstalledPlugins.txt
/src/Presentation/SmartStore.Web/App_Data/Licenses.lic
diff --git a/README.md b/README.md
index 95b669b8f9..f8e84aa9bb 100644
--- a/README.md
+++ b/README.md
@@ -1,24 +1,60 @@
-## Overview
+
+
+
+
+
+
+
+
+
+
+
+
+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 is a free, open source, full-featured e-commerce solution for companies of any size. It is web standards compliant and incorporates the newest Microsoft web technology stack.
+**Smartstore.NET includes all essential features to create multilingual and multi-currency stores** targeting desktop or mobile devices and enabling SEO optimized rich product catalogs with support for an unlimited number of products and categories, variants, bundles, datasheets, ESD, discounts, coupons and many more.
-**SmartStore.NET includes all essential features to create multilingual and multi-currency stores** targeting desktop or mobile devices and enabling SEO optimized rich product catalogs with support for an unlimited number of products and categories, variants, bundles, datasheets, ESD, discounts, coupons and many more.
+A comprehensive set of tools for CRM & CMS, sales, marketing, payment & shipping handling, etc. makes Smartstore.NET a powerful all-in-one solution fulfilling all your needs.
-A comprehensive set of tools for CRM & CMS, sales, marketing, payment & shipping handling, etc. makes SmartStore.NET a powerful all-in-one solution fulfilling all your needs.
+
+
+
+
+
-**SmartStore.NET delivers a beautiful and configurable shop front-end out-of-the-box**, built with a design approach on the highest level, including components like `Bootstrap 4`, `Sass` and others. The supplied theme _Flex_ is modern, clean and fully responsive, giving buyers the best possible shopping experience on any device.
+**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 ;-)
+The state-of-the-art architecture of Smartstore.NET - with `ASP.NET 4.5` + `MVC 5`, `Entity Framework 6` and Domain Driven Design approach - makes it easy to extend, extremely flexible and essentially fun to work with ;-)
* **Website:** [http://www.smartstore.com/en/net](http://www.smartstore.com/en/net)
* **Forum:** [http://community.smartstore.com](http://community.smartstore.com)
* **Marketplace:** [http://community.smartstore.com/marketplace](http://community.smartstore.com/marketplace)
* **Translations:** [http://translate.smartstore.com/](http://translate.smartstore.com/)
-* **Documentation:** [SmartStore.NET Documentation in English](http://docs.smartstore.com/display/SMNET/SmartStore.NET+Documentation+Home)
+* **Documentation:** [Smartstore.NET Documentation in English](http://docs.smartstore.com/display/SMNET/SmartStore.NET+Documentation+Home)
+
+
+
## Highlights
@@ -35,6 +71,8 @@ The state-of-the-art architecture of SmartStore.NET - with `ASP.NET 4.5` + `MVC
### Features
+* NEW: CMS Page Builder
+* NEW: CMS Menu Builder
* Multi-Store support
* Unlimited number of products and categories
* Product Bundles
@@ -55,40 +93,28 @@ The state-of-the-art architecture of SmartStore.NET - with `ASP.NET 4.5` + `MVC
* CMS (Blog, Forum, custom pages & HTML content etc.)
* and many more...
+
+
## Project Status
-SmartStore.NET V3.1.5 has been released on 25 May, 2018. The highlights are:
-
-* **Wallet \***: Enables full or partial order payment via credit account. Includes REST-Api.
-* **[Liquid](https://github.com/Shopify/liquid/wiki/Liquid-for-Designers) template engine**: very flexible templating for e-mails and campaigns with autocompletion and syntax highlighting.
-* **Cash Rounding**: define money rounding rules on currency and payment method level.
-* **Modern, responsive backend**: migrated backend to Bootstrap 4, overhauled and improved the user interface.
-* **RTL**: comprehensive RTL (Right-to-left) and bidi(rectional) support.
-* Search engine friendly topic URLs
-* Compliance with **EU-GDPR** requirements
-* **Enhanced MegaMenu \***: virtual dropdowns for surplus top-level categories and brands.
-* **Honeypot** bot detection for registration and contact forms.
-* **Amazon Pay**:
- * Supports merchants registered in the USA and Japan
- * External authentication via *Login with Amazon* button in shop frontend
- * Several improvements through the new *Login and pay with Amazon* services
-* **Image processing**: new processing and caching strategy! Thumbnails are not created synchronously during the main request anymore, instead a new middleware route defers processing until an image is requested by any client.
-* **TinyImage \***: scores ultra-high image compression rates (up to 80 %!) and enables WebP support.
-* **UrlRewriter \***: define URL redirection rules in the backend using *mod_rewrite* notation.
-* **Address formatting** templates by country
-* **Language packs**: downloader & auto-importer for packages available online.
-* ...and a lot more new features, enhancements and fixes
-
-\* Commercial plugin
+
+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.
+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
@@ -96,7 +122,7 @@ We have set up a live online demo for you so you are able to test SmartStore.NET
* Fire up your browser and follow the installation instructions
* Enjoy ;-)
-NOTE: SmartStore.NET 3 requires [Visual C++ Redistributable für Visual Studio 2015](https://www.microsoft.com/en-US/download/details.aspx?id=52685) which is already pre-installed on most systems. If, nevertheless, it is missing on your web server, just download and execute the installer or ask your hosting provider to do that for you.
+NOTE: Smartstore.NET 3 requires [Visual C++ Redistributable für Visual Studio 2015](https://www.microsoft.com/en-US/download/details.aspx?id=52685) which is already pre-installed on most systems. If, nevertheless, it is missing on your web server, just download and execute the installer or ask your hosting provider to do that for you.
### System requirements
@@ -107,6 +133,7 @@ NOTE: SmartStore.NET 3 requires [Visual C++ Redistributable für Visual Studio 2
* Full Trust
+
## License
-SmartStore.NET Community Edition is released under the [GPLv3 license](http://www.gnu.org/licenses/gpl-3.0.txt).
+Smartstore.NET Community Edition is released under the [GPLv3 license](http://www.gnu.org/licenses/gpl-3.0.txt).
diff --git a/SmartStoreNET.Tasks.Targets b/SmartStoreNET.Tasks.Targets
index fec484de68..080c78d497 100644
--- a/SmartStoreNET.Tasks.Targets
+++ b/SmartStoreNET.Tasks.Targets
@@ -39,7 +39,7 @@
x86
$(BUILD_NUMBER)
- 3.1.5
+ 3.2.2
$(StageFolder)
.$(Version)
@@ -90,21 +90,20 @@
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
@@ -126,6 +125,7 @@
+
@@ -137,9 +137,9 @@
OutDir=$(StageFolder)\Administration\bin\;Configuration=$(Configuration)" />
-
-
-
+
+
+
@@ -159,11 +159,11 @@
-
-
+
+
-
-
+
+
diff --git a/assets/SmartStoreNET.png b/assets/SmartStoreNET.png
deleted file mode 100644
index 1fc5ca5409..0000000000
Binary files a/assets/SmartStoreNET.png and /dev/null differ
diff --git a/assets/smartstore-icon.png b/assets/smartstore-icon.png
new file mode 100644
index 0000000000..e6414db195
Binary files /dev/null and b/assets/smartstore-icon.png differ
diff --git a/assets/smartstore-text.png b/assets/smartstore-text.png
new file mode 100644
index 0000000000..25bf9558fb
Binary files /dev/null and b/assets/smartstore-text.png differ
diff --git a/build.bat b/build.bat
index ef0004b549..94a6c72519 100644
--- a/build.bat
+++ b/build.bat
@@ -1,37 +1,45 @@
+SETLOCAL ENABLEDELAYEDEXPANSION
+
for /f "usebackq tokens=1* delims=: " %%i in (`lib\vswhere\vswhere -latest -requires Microsoft.Component.MSBuild`) do (
- if /i "%%i"=="installationPath" set InstallDir=%%j
+ if /i "%%i"=="installationPath" (
+ set InstallDir=%%j
+ echo !InstallDir!
+ if exist "!InstallDir!\MSBuild\15.0\Bin\MSBuild.exe" (
+ echo "Using MSBuild from Visual Studio 2017"
+ set msbuild="!InstallDir!\MSBuild\15.0\Bin\MSBuild.exe"
+ goto build
+ )
+ if exist "!InstallDir!\MSBuild\Current\Bin\MSBuild.exe" (
+ echo "Using MSBuild from Visual Studio 2019"
+ set msbuild="!InstallDir!\MSBuild\Current\Bin\MSBuild.exe"
+ goto build
+ )
+ )
)
FOR %%b in (
- "%InstallDir%\Common7\Tools\VsMSBuildCmd.bat"
- "%VS140COMNTOOLS%\Common7\Tools\vsvars32.bat"
+ "%VS150COMNTOOLS%\vsvars32.bat"
+ "%VS140COMNTOOLS%\vsvars32.bat"
+ "%VS120COMNTOOLS%\vsvars32.bat"
+ "%VS110COMNTOOLS%\vsvars32.bat"
) do (
if exist %%b (
- call %%b
- goto findmsbuild
+ echo "Using MSBuild from %%b"
+ call %%b
+ set msbuild="msbuild"
+ goto build
)
)
echo "Unable to detect suitable environment. Build may not succeed."
-:findmsbuild
-
-SETLOCAL ENABLEDELAYEDEXPANSION
-
-if exist "%InstallDir%\MSBuild\15.0\Bin\MSBuild.exe" (
- if not defined MsBuildPath (
- SET "MsBuildPath=%InstallDir%\MSBuild\15.0\Bin\MsBuild.exe"
- goto build
- )
-)
-
-
-echo "Unable to detect suitable MsBuild version (15.0). Build may not succeed."
-
:build
+
cd /d %~dp0
echo "Restoring NuGet packages"
lib\nuget\nuget.exe restore "src\SmartStoreNET.sln"
-call "!MsBuildPath!" SmartStoreNET.proj /p:SlnName=SmartStoreNET /m /p:DebugSymbols=false /p:DebugType=None /maxcpucount %*
+%msbuild% SmartStoreNET.proj /p:SlnName=SmartStoreNET /m /p:DebugSymbols=false /p:DebugType=None /maxcpucount %*
+
+:end
diff --git a/changelog.md b/changelog.md
index fc15d24f94..81357cca18 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,88 +1,192 @@
-# Release Notes
+# Release Notes
+
+## SmartStore.NET 3.2.2
+
+### New Features
+* External category links: alternative links for categories in the main menu and in category listings.
+* Page Builder: multistore support for stories.
+* #1639 Shipping.ByTotal: Add option to calculate the shipping rate based on the net total instead of the gross total.
+* #1537 Implement "Ratenzahlung powered by PayPal" provider.
+* New Heidelpay payment plugin.
+* Shop-Connector: mapping of product SKUs that differ between client shops.
+* MegaMenu supports configuration of menu items
+
+
+### Improvements
+* Amazon Pay: PSD2. Strong customer authentication (SCA) upgrade.
+* Updated *MaxMind.GeoIP* library and country database
+* Removed HTML encoding for meta tags and document title.
+* #1662 Quantity Units: added field for name pluralization.
+* (SEO) combined SSL and canonical host 301 redirections to prevent successive redirects.
+* (SEO) Activated GZIP compression for all dynamic and static responses.
+* Set max. file upload size to 2 GB (globally)
+* (Dev) Added a sample block for the *Page Builder* in DevTools plugin.
+* Various other minor improvements.
+* PayPal PLUS: paying by instalments can be selected in payment wall.
+* Payone: send more information to API (e.g. shipping address) to stay as frictionless as possible for PSD2.
+* PageBuilder: Added keyboard shortcuts
+* #1582 Added structured data to reviews
+* BMEcat: added support for import of specification attributes
+
+### Bugfixes
+* Fixed wrong XML sitemap index numbering.
+* #1646 Setting ManufacturerItemsToDisplayOnHomepage to 1 will not show any manufacturer.
+* #1647 All manufacturers page: link of last character missing in alphabetical letter list.
+* #1664 RTL: CMS menu issues in backend.
+* #1677 Page Builder: moving and resizing blocks via drag & drop didn't work correctly in RTL.
+* Do not adjust inventory twice when an order is cancelled and deleted.
+* #1500 Fallback to fixed VAT rate instead of rate by region for countries that are not subject to vat.
+* Fixed "The entity wrapper stored in the proxy does not reference the same proxy" when copying an export profile.
+* Various other minor fixes.
+* BMEcat: set TaxCategoryId & MainPictureId for newly imported products
+
+
+
+## SmartStore.NET 3.2.1
+
+### Improvements
+* Implemented file range support for CachedFileResult
+* Sitemap should only include products visible for current customer, which is the search engine system customer
+* Fixed uniform spelling for query string parameter "returnUrl"
+* Fixed search engine issue (itemtype, "offers" required) on product detail page
+* Summernote: solved html formatting issues thanks to "Beautifier" library
+* Media middleware: simplified ETag generation & handling
+
+### Bugfixes
+
+* Mega Menu: Product rotator wasn't working
+* Tiny Image: Fixed periodic IOException
+* URL Rewriter: performance optimization
+* Fixed more menu for main catalog navigation
+* Fixed DateTime nor expressed as UTC in AzureFile & LocalFile
+* Content Slider: Fixed slide display bug on surface and other touch devices
+* Content Slider: Randomize slides > Value was stored in database but toggle button has always shown "off"
+* Page Builder: Fixed slider bug for list blocks (brand, category, products) in Firefox
+* Page Builder: fixes blurry background images in IOS
+* Output Cache: action widgets didn't render when they were part of a widgetzone within a donut child action
+* Multishop resolution failed under certain circumstances
+* Fixed menu issue "collection was modified, enumeration operation may not execute"
+* Fixed "The parameters dictionary contains a null entry for parameter 'catId' of non-nullable type 'System.Int32'"
+* Fixed discount coupon sometimes wasn't applied
+* Fixed "Value cannot be null. Parameter name: key" in order list
+* Fixed topic link resolving by system name in multistore environment
+* Async event consumers were not guaranteed to be awaited
+
## SmartStore.NET 3.2
### Highlights
-* (Perf) #1202 Search: update to Lucene.NET 4.8
-* (Perf) Huge performance increase in discount resolution and calculation
+* (NEW) **CMS Page Builder**: Create fascinating content that will boost your sales. No coding is required thanks to a powerful WYSIWYG editor which utilizes the revolutionary *CSS Grid* system (commercial plugin)
+* (NEW) **CMS Menu Builder**: Visual manager for all sorts of menus. Change existing menus or create your own and place them anywhere you want.
+* (Perf) **Faster MegaSearch** thanks to Lucene.NET 4.8
+* (Perf) **Huge performance increase** in discount resolution and calculation.
### New Features
* **EmailReminder**:
* Reminders for open shopping carts
* Reminders for product reviews
* Reminders to visit the shop after a long absence
-* #1144 Enable multi server search index
-* Made Topic ACL enabled
-* Implemented paging & filtering for Topic grid
-* Topics: added **IsPublished**, **Short Title** (link text) and **Intro** (teaser) properties.
-* New storefront catalog options: **ShowSubCategoriesInSubPages**, **ShowDescriptionInSubPages** & **IncludeFeaturedProductsInSubPages** (Subpage = List index > 1 or any active filter).
-* New security option: Use invisible reCAPTCHA
+* **Scheduling**
+ * Task execution history
+ * Web farms: tasks can run decidedly on each machine of a web farm
+ * #1144 MegaSearch: multi server search index
+* **Topics & Pages**
+ * Access control lists
+ * Grid paging & filtering
+ * New properties: added **IsPublished**, **Short Title** (link text) and **Intro** (teaser) properties.
+ * #1542: added options to set body CSS class and Html Id.
+* **MegaSearch**:
+ * Supports searching for forum posts.
+ * #1172 Option to display related search terms on search page.
+ * Command to continue a previously aborted search index rebuild.
+* **Import/export of**:
+ * Product tags
+ * Tier prices
+ * Product attribute options
+ * Product attribute combinations (update only)
+* **Forum**:
+ * Added option to display a captcha on forum pages when creating or replying to a topic.
+ * #417 Restrict forum groups to specific customer roles.
+ * Added published property to forum topic and post.
+ * Added voting for forum posts.
+ * Several performance improvements.
* **BeezUp**:
* #1459 Add option to only submit one category name per product
* Allow to specify export categories per product
+* **Santander**:
+ * Added payment method for instalment purchase.
+ * Added financing calculator.
+* Updated icon library to **Font Awesome 5 Free** with the possibility to activate pro version in the frontend.
+* New storefront catalog options: **ShowSubCategoriesInSubPages**, **ShowDescriptionInSubPages** & **IncludeFeaturedProductsInSubPages** (Subpage = List index > 1 or any active filter).
+* New security option: Use **invisible reCAPTCHA**
* Wallet: Allow customer to choose whether refund should be submitted to his wallet.
* Added option to display preview pictures in product lists
* Added option to add multiple file versions to product download section
* Added options for alternating price display (in badges)
-* **Forum**:
- * Added option to display a captcha on forum pages when creating or replying to a topic.
- * #417 Restrict forum groups to specific customer roles.
- * Added published property to forum topic and post.
- * Added voting for forum posts.
- * Several performance improvements.
-* **MegaSearch**:
- * Supports searching for forum posts.
- * #1172 Option to display related search terms on search page.
- * Command to continue a previously aborted search index rebuild.
-* **Import/export of**:
- * Product tags.
- * Tier prices.
- * Product attribute options.
- * Product attribute combinations (update only).
* Customer avatar: Letter with colored background if no avatar image was uploaded.
* Viveum: Supports payment via "Virtual Account Brands" (e.g. PayPal).
* Added options for alternating price display (in badges).
* #1515 Poll: Add result tab with a list of answers and customers for a poll
* BMEcat: Added export and import of product tags.
+* Web-API: Added endpoints for ProductSpecificationAttribute.
+* Trusted Shops: Added new Trustbadge display for mobile devices
+* (Dev) Added *sm:EfCommandTimeout* setting to web.config (was 30 sec. fixed, now configurable)
### Improvements
* (Perf) Significantly increased query performance for products with a lot of category assignments (> 10).
+* (Perf) Increased app startup speed by up to 20%.
* (Perf) Ultra-fast file-based XML sitemap generation for extremely large catalogs (> 1M)
* At least 10x faster
* Generated files are saved on the hard disk now: a rebuild after an app restart is no longer necessary.
* No exclusive locks during rebuilds anymore: if an (outdated) file already exists, it is returned instantly.
+* (Dev) Enhanced pub/sub: new `IConsumer` marker interface for classes that contain one or more event message handlers. The generic `IConsumer` implements `IConsumer` but has been marked as `Obsolete`.
+* Better protection against XSS attacks
+* Updated to Bootstrap version 4.1.3
* **Debitoor**:
* Partially update customer instead of full update to avoid all fields being overwritten.
* #1540 Place company name in front of customer name (according to address format of the particular country).
+ * #1565 Add a button to manually create a draft invoice.
* #1479 Show in messages the delivery time at the time of purchase
-* #1184 Sort Current shopping carts & Current wishlists by ShoppingCartItem.CreatedOn.
+* #1184 Sort current shopping carts & current wishlists by `ShoppingCartItem.CreatedOn`.
* #1106 BMECat: import & export support for product keywords
-* #1499 Added hint to forms indicating that fields with an asterisk (*)
+* #1499 Added hint to forms indicating that fields with an asterisk (*) are required
* Added filter for newsletter subscriber export by working language
* Refactored download section
* Enhanced EntityPicker to pick from customers, manufacturers & categories
* #1510 Breadcrumb of an associated product should include the grouped product if it has no assigned categories.
* OpenTrans: added customer number to parties
-* Do not filter cookie using resources if cookie usage has not yet been consented to
+* Do not filter cookie using resources if cookie usage has not yet been consented to.
+* #1563 QueuedMessagesClearTask: add a setting for the age of the mails to be deleted.
+* #1569 Added a setting to show login note if no prices are displayed due to customer group permissions.
+* PayPal PLUS: Up to 10 more third party payment methods are allowed by PayPal now.
+* #1560 Tell-a-Friend and anonymous user: render alert box and tell guest to login to use this function
+* #1571 Compare products now shows all specification attribute options
+* #1539 Signing in is now allowed with e-mail and username
+* Trusted Shops: Trustbadge won't be displayed in Popups & Iframes anymore
+* #1461 Admin Grid: filter dialog will be displayed entirely even when grid has no data to display
+* Lots of minor enhancements for both frontend and admin theme
+* Many other minor improvements
### Bugfixes
-* In a multi-store environment, multiple topics with the same system name cannot be resolved reliably.
+* In a multi-store environment, multiple topics with the same system name can now be resolved reliably.
* **GMC**:
* Export the product images if no attribute images are defined
* Do not export the first image twice for additional images
* Export image URL of full size image (not default size) for additional images
+ * Custom labels are now being exported
* Media middleware: 0-byte files should be treated as missing.
-* Megamenu alpha/omega blends do not toggle correctly on touch devices
+* Megamenu alpha/omega blends do now toggle correctly on touch devices
* Summernote HTML editor exceeds parent container width when CodeMirror is activated
* Only display a zero search hits warning if at least one filter is activated
* #1436 Do not display delivery time in customer order completed messages
* "ArgumentNullException: The value must not be NULL" if a topic is password protected
* Tax by region: Fixes after inserting a tax rate country column shows "Unavailable"
* #1014 Switching to default language keeps specific URL alias of current page
-* Shipping by total: Fixes when inserting a record the country was not saved
-* #1460 Editing of the customer title is missing on customer and address pages in the backend
+* Shipping by total: When inserting a record the country now will be saved
+* #1460 Editing of the customer title was missing on customer and address pages in the backend
* #1447 Checkout button payment methods (Amazon, PayPal Express) won't work in conjunction with mandatory checkout attributes
-* When creating a topic, the widget zone input shows System.String[]
+* Fixed Autofac circularity error
+* When creating a topic, the widget zone input has shown System.String[]
* Switching the language always redirected to the home page if SEO friendly URLs was deactivated.
* File upload of a checkout attribute was not stored on cart page.
* Redirecting within checkout may have displayed an incorrect URL in the browser.
@@ -91,19 +195,27 @@
* In a multi-store, message templates may have loaded the wrong disclaimer and conditions-of-use text.
* NullReferenceException in manufacturer list when there is no manufacturer.
* Wrong order of featured products on category page.
-* #1504 Cart item price calculation wrong if attribute combinations with text types are involved.
-* #1485 Dropdown list for product sorting does not work with Internet Explorer 11.
-* #1468 Twitter authentication not working anymore.
+* #1504 Cart item price calculation was wrong if attribute combinations with text types were involved.
+* #1485 Dropdown list for product sorting didn't not work with Internet Explorer 11.
+* #1468 Twitter authentication wasn't working anymore.
* Newsletter subscription didn't work when customer privacy setting DisplayGdprConsentOnForms was turned off
* Fixed social media image detection
* Fixed redirection of bots when several languages were active
* Region cannot be selected in checkout when entering a billing or shipping address
* Fixed invalid conversion of "System.Int32" to "SmartStore.Core.Domain.Tax.VatNumberStatus" when placing an order
* MegaMenu: Improved item rendering for third tier elements
-* Product display order on category and manufacturer pages sometimes wrong when using linq search.
+* Product display order on category and manufacturer pages was sometimes wrong when using linq search.
* Debitoor: Wrong invoice total if a subtotal discount has been applied.
* Import: Fixes invalid conversion "System.Double" to "SmartStore.Core.Domain.Catalog.QuantityControlType".
-* Topics: Fixes "Cannot insert duplicate key row in object 'dbo.UrlRecord' with unique index 'IX_UrlRecord_Slug'"
+* Topics: Fixes "Cannot insert duplicate key row in object 'dbo.UrlRecord' with unique index 'IX_UrlRecord_Slug'".
+* #1566 Santander: eliminate the 1 cent rounding difference at amountTotalNet.
+* Fixed redirection to the homepage for pages which are loaded while the application is restarted.
+* #1570 Filter option "Only deactivated customers" filters deleted instead of deactivated customers.
+* #1475 select boxes must be wrapped on mobile devices if data-select-url is set
+* Fixed the redirection to the homepage for pages which were loaded while the application was restarted
+* Fixes product feeds expect a different base price formatting.
+* #1369 Shopping cart shows "Discount code applied", although it is not applied due to a lower tier price.
+* Many other fixes...
## SmartStore.NET 3.1.5
@@ -362,7 +474,7 @@
* Added config setting *sm:PdfEngineBaseUrl*. There are cases where the PDF converter exits with a network error, when it is unable to load automatically resolved URLs.
* (Dev) Added *Retry* utility class
* #1176 Admin > Product Search: It ain't possible to search for parts of a product name
-
+
### Bugfixes
* #1145: Fixed HTTP 404 after switching language
* Fixed null reference exception in product lists if sorting is not allowed
@@ -650,7 +762,8 @@
* #495 Implement option to search product detail description by default
### Improvements
-* (Perf) Implemented static caches for URL aliases and localized properties. Increases app startup and request speed by up to 30%.
+* (Perf) Implemented static caches for URL aliases and localized properties. Increases
+* and request speed by up to 30%.
* (Perf) Significantly reduced number of database reads during product list rendering. Increases request speed by up to 10%.
* (Perf) Implemented 2nd level cache for infrequently changed entities. Increases request speed by up to 10%.
* TaskScheduler: Rewritten from scratch to be suitable for Web Farms
@@ -1270,7 +1383,7 @@
* [SMNET-202] - SmartTabSelection mit verschachtelten Tabs fehlerhaft nach Reload einer Seite
### Improvement
-
+
* [SMNET-13] - Attributwerte: der Text "Aufpreis" muss um "Minderpreis" erweitert werden.
* [SMNET-15] - Umgestaltung der Darstellung der Staffelpreise (Popover ab dem fünften Element)
* [SMNET-30] - MessageTemplates teilweise auf Englisch
diff --git a/lib/SmartStore.Licensing/SmartStore.Licensing.dll b/lib/SmartStore.Licensing/SmartStore.Licensing.dll
index 2c82c7853f..17bf4e2ad9 100644
Binary files a/lib/SmartStore.Licensing/SmartStore.Licensing.dll and b/lib/SmartStore.Licensing/SmartStore.Licensing.dll differ
diff --git a/src/AssemblySharedInfo.cs b/src/AssemblySharedInfo.cs
index 79225de4c7..b58492422f 100644
--- a/src/AssemblySharedInfo.cs
+++ b/src/AssemblySharedInfo.cs
@@ -7,6 +7,6 @@
[assembly: AssemblyDescription("SmartStore.NET")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("SmartStore AG")]
-[assembly: AssemblyCopyright("Copyright © SmartStore AG 2016")]
+[assembly: AssemblyCopyright("Copyright © SmartStore AG 2019")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
diff --git a/src/AssemblyVersionInfo.cs b/src/AssemblyVersionInfo.cs
index d9e092d65c..719cd1d6ff 100644
--- a/src/AssemblyVersionInfo.cs
+++ b/src/AssemblyVersionInfo.cs
@@ -9,7 +9,7 @@
//
// You can specify all the values or you can default the Revision and Build Numbers
// by using the '*' as shown below:
-[assembly: AssemblyVersion("3.1.5.0")]
+[assembly: AssemblyVersion("3.2.2.0")]
-[assembly: AssemblyFileVersion("3.1.5.0")]
-[assembly: AssemblyInformationalVersion("3.1.5.0")]
+[assembly: AssemblyFileVersion("3.2.2.0")]
+[assembly: AssemblyInformationalVersion("3.2.2.0")]
diff --git a/src/Libraries/SmartStore.Core/Async/AsyncLock.cs b/src/Libraries/SmartStore.Core/Async/AsyncLock.cs
deleted file mode 100644
index b93da92afc..0000000000
--- a/src/Libraries/SmartStore.Core/Async/AsyncLock.cs
+++ /dev/null
@@ -1,110 +0,0 @@
-using System;
-using System.Collections.Concurrent;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
-using SmartStore.Utilities;
-
-namespace SmartStore.Core.Async
-{
- public sealed class AsyncLock : DisposableObject
- {
- private static readonly TimeSpan _noTimeout = TimeSpan.FromMilliseconds(-1);
- private static readonly ConcurrentDictionary _keyLocks = new ConcurrentDictionary();
-
- private readonly SemaphoreSlim _semaphore;
- private readonly int _initialCount;
-
- public AsyncLock()
- : this(1, int.MaxValue)
- {
- }
-
- public AsyncLock(int initialCount)
- : this(initialCount, int.MaxValue)
- {
- }
-
- public AsyncLock(int initialCount, int maxCount)
- {
- _semaphore = new SemaphoreSlim(initialCount, maxCount);
- _initialCount = initialCount;
- }
-
- public static AsyncLock Acquire(object key)
- {
- Guard.NotNull(key, nameof(key));
-
- var asyncLock = _keyLocks.GetOrAdd(key, x => new AsyncLock());
-
- // Perf: once we have more than 100 lock objects in the storage, remove the idle ones.
- if (_keyLocks.Count > 100)
- {
- // never remove the current one and the locked ones
- var idles = _keyLocks.Where(kvp => kvp.Key != key && !kvp.Value.IsLockHeld).ToArray();
- foreach (var kvp in idles)
- {
- var value = kvp.Value;
- // double check for held lock
- if (!value.IsLockHeld)
- {
- _keyLocks.TryRemove(kvp.Key, out value);
- }
- }
- }
-
- return asyncLock;
- }
-
- public bool IsLockHeld
- {
- get { return _semaphore.CurrentCount < _initialCount; }
- }
-
- public int Release()
- {
- return _semaphore.Release();
- }
-
- public int Release(int releaseCount)
- {
- return _semaphore.Release(releaseCount);
- }
-
- public Task LockAsync()
- {
- return LockAsync(_noTimeout, CancellationToken.None);
- }
-
- public Task LockAsync(TimeSpan timeout)
- {
- return LockAsync(timeout, CancellationToken.None);
- }
-
- public Task LockAsync(CancellationToken cancellationToken)
- {
- return LockAsync(_noTimeout, cancellationToken);
- }
-
- public Task LockAsync(TimeSpan timeout, CancellationToken cancellationToken)
- {
- var wait = _semaphore.WaitAsync(timeout, cancellationToken);
-
- return wait.IsCompleted
- ? Task.FromResult(new ActionDisposable(() => _semaphore.Release()) as IDisposable)
- : wait.ContinueWith((t, s) => new ActionDisposable(() => ((SemaphoreSlim)s).Release()) as IDisposable,
- _semaphore,
- CancellationToken.None,
- TaskContinuationOptions.ExecuteSynchronously,
- TaskScheduler.Default);
- }
-
- protected override void OnDispose(bool disposing)
- {
- if (disposing)
- _semaphore.Dispose();
- }
- }
-}
diff --git a/src/Libraries/SmartStore.Core/Async/AsyncRunner.cs b/src/Libraries/SmartStore.Core/Async/AsyncRunner.cs
index 1fce2cc436..2751259eef 100644
--- a/src/Libraries/SmartStore.Core/Async/AsyncRunner.cs
+++ b/src/Libraries/SmartStore.Core/Async/AsyncRunner.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
@@ -8,113 +9,166 @@
namespace SmartStore.Core.Async
{
- public static class AsyncRunner
+ using EventQueue = ConcurrentQueue>;
+
+ public class AsyncRunner : IDisposable
{
- private static readonly BackgroundWorkHost _host = new BackgroundWorkHost();
+ #region Instance members
- ///
- /// Gets the global cancellation token which signals the application shutdown
- ///
- public static CancellationToken AppShutdownCancellationToken
+ private ExclusiveSynchronizationContext CurrentContext;
+ private SynchronizationContext OldContext;
+ private int TaskCount;
+
+ private AsyncRunner()
{
- get { return _host.ShutdownCancellationTokenSource.Token; }
+ OldContext = SynchronizationContext.Current;
+ CurrentContext = new ExclusiveSynchronizationContext(OldContext);
+ SynchronizationContext.SetSynchronizationContext(CurrentContext);
+ }
+
+ public void Dispose()
+ {
+ try
+ {
+ CurrentContext.BeginMessageLoop();
+ }
+ catch (Exception e)
+ {
+ throw e;
+ }
+ finally
+ {
+ SynchronizationContext.SetSynchronizationContext(OldContext);
+ }
+ }
+
+ private void Increment()
+ {
+ Interlocked.Increment(ref TaskCount);
+ }
+
+ private void Decrement()
+ {
+ Interlocked.Decrement(ref TaskCount);
+ if (TaskCount == 0)
+ {
+ CurrentContext.EndMessageLoop();
+ }
}
///
/// Executes an async Task method which has a void return value synchronously
///
- /// Task method to execute
- public static void RunSync(Func func)
+ /// Task execute
+ public void Run(Task task, Action continuation = null)
{
- var oldContext = SynchronizationContext.Current;
- var synch = new ExclusiveSynchronizationContext();
- SynchronizationContext.SetSynchronizationContext(synch);
- synch.Post(async _ =>
+ CurrentContext.Post(async _ =>
{
try
{
- await func();
+ Increment();
+ await task;
+
+ continuation?.Invoke(task);
}
catch (Exception e)
{
- synch.InnerException = e;
- throw;
+ CurrentContext.InnerException = e;
}
finally
{
- synch.EndMessageLoop();
+ Decrement();
}
}, null);
- synch.BeginMessageLoop();
-
- SynchronizationContext.SetSynchronizationContext(oldContext);
}
///
/// Executes an async Task method which has a TResult return type synchronously
///
/// Return Type
- /// Task method to execute
- ///
- public static TResult RunSync(Func> func)
+ /// Task to execute
+ public TResult Run(Task task, Action> continuation = null)
{
- var oldContext = SynchronizationContext.Current;
- var synch = new ExclusiveSynchronizationContext();
- SynchronizationContext.SetSynchronizationContext(synch);
- TResult ret = default(TResult);
- synch.Post(async _ =>
+ var result = default(TResult);
+
+ CurrentContext.Post(async _ =>
{
try
{
- ret = await func();
+ Increment();
+ result = await task;
+
+ continuation?.Invoke(task);
}
catch (Exception e)
{
- synch.InnerException = e;
- throw;
+ CurrentContext.InnerException = e;
}
finally
{
- synch.EndMessageLoop();
+ Decrement();
}
}, null);
- synch.BeginMessageLoop();
- SynchronizationContext.SetSynchronizationContext(oldContext);
- return ret;
- }
- public static Task Run(Action action)
- {
- return Run(action, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default);
+ return result;
}
- public static Task Run(Action action, CancellationToken cancellationToken)
+ #endregion
+
+ private static readonly BackgroundWorkHost _host = new BackgroundWorkHost();
+
+ ///
+ /// Gets the global cancellation token which signals the application shutdown
+ ///
+ public static CancellationToken AppShutdownCancellationToken
{
- return Run(action, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default);
+ get { return _host.ShutdownCancellationTokenSource.Token; }
}
- public static Task Run(Action action, TaskCreationOptions options)
+ public static AsyncRunner Create()
{
- return Run(action, CancellationToken.None, options, TaskScheduler.Default);
+ return new AsyncRunner();
}
- public static Task Run(Action action, CancellationToken cancellationToken, TaskCreationOptions options)
+ ///
+ /// Executes an async Task method which has a void return value synchronously
+ ///
+ /// Task method to execute
+ public static void RunSync(Func func, Action continuation = null)
{
- return Run(action, cancellationToken, options, TaskScheduler.Default);
+ using (var runner = new AsyncRunner())
+ {
+ runner.Run(func(), continuation);
+ }
}
- public static Task Run(Action action, TaskScheduler scheduler)
+ ///
+ /// Executes an async Task method which has a TResult return type synchronously
+ ///
+ /// Return Type
+ /// Task method to execute
+ ///
+ public static TResult RunSync(Func> func, Action> continuation = null)
{
- return Run(action, CancellationToken.None, TaskCreationOptions.None, scheduler);
+ var result = default(TResult);
+
+ using (var runner = new AsyncRunner())
+ {
+ result = runner.Run(func(), continuation);
+ }
+
+ return result;
}
- public static Task Run(Action action, CancellationToken cancellationToken, TaskCreationOptions options, TaskScheduler scheduler)
+ public static Task Run(
+ Action action,
+ CancellationToken cancellationToken = default(CancellationToken),
+ TaskCreationOptions options = TaskCreationOptions.LongRunning,
+ TaskScheduler scheduler = null)
{
Guard.NotNull(action, nameof(action));
- Guard.NotNull(scheduler, nameof(scheduler));
var ct = _host.CreateCompositeCancellationTokenSource(cancellationToken).Token;
- options |= TaskCreationOptions.LongRunning; // enforce an exclusive thread (not from pool)
var t = Task.Factory.StartNew(() => {
var accessor = EngineContext.Current.ContainerManager.ScopeAccessor;
@@ -122,21 +176,24 @@ public static Task Run(Action action, Cancell
{
action(accessor.GetLifetimeScope(null), ct);
}
- }, ct, options, scheduler);
+ }, ct, options, scheduler ?? TaskScheduler.Default);
_host.Register(t, ct);
return t;
}
- public static Task Run(Action action, object state, CancellationToken cancellationToken, TaskCreationOptions options, TaskScheduler scheduler)
+ public static Task Run(
+ Action action,
+ object state,
+ CancellationToken cancellationToken = default(CancellationToken),
+ TaskCreationOptions options = TaskCreationOptions.LongRunning,
+ TaskScheduler scheduler = null)
{
Guard.NotNull(state, nameof(state));
Guard.NotNull(action, nameof(action));
- Guard.NotNull(scheduler, nameof(scheduler));
var ct = _host.CreateCompositeCancellationTokenSource(cancellationToken).Token;
- options |= TaskCreationOptions.LongRunning; // enforce an exclusive thread (not from pool)
var t = Task.Factory.StartNew((o) =>
{
@@ -145,47 +202,22 @@ public static Task Run(Action action,
{
action(accessor.GetLifetimeScope(null), ct, o);
}
- }, state, ct, options, scheduler);
+ }, state, ct, options, scheduler ?? TaskScheduler.Default);
_host.Register(t, ct);
return t;
}
-
-
- public static Task Run(Func function)
- {
- return Run(function, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default);
- }
-
- public static Task Run(Func function, CancellationToken cancellationToken)
- {
- return Run(function, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default);
- }
-
- public static Task Run(Func function, TaskCreationOptions options)
- {
- return Run(function, CancellationToken.None, options, TaskScheduler.Default);
- }
-
- public static Task Run(Func function, CancellationToken cancellationToken, TaskCreationOptions options)
- {
- return Run(function, cancellationToken, options, TaskScheduler.Default);
- }
-
- public static Task Run(Func function, TaskScheduler scheduler)
- {
- return Run(function, CancellationToken.None, TaskCreationOptions.None, scheduler);
- }
-
- public static Task Run(Func function, CancellationToken cancellationToken, TaskCreationOptions options, TaskScheduler scheduler)
+ public static Task Run(
+ Func function,
+ CancellationToken cancellationToken = default(CancellationToken),
+ TaskCreationOptions options = TaskCreationOptions.LongRunning,
+ TaskScheduler scheduler = null)
{
Guard.NotNull(function, nameof(function));
- Guard.NotNull(scheduler, nameof(scheduler));
var ct = _host.CreateCompositeCancellationTokenSource(cancellationToken).Token;
- options |= TaskCreationOptions.LongRunning; // enforce an exclusive thread (not from pool)
var t = Task.Factory.StartNew(() =>
{
@@ -194,21 +226,24 @@ public static Task Run(Func Run(Func function, object state, CancellationToken cancellationToken, TaskCreationOptions options, TaskScheduler scheduler)
+ public static Task Run(
+ Func function,
+ object state,
+ CancellationToken cancellationToken = default(CancellationToken),
+ TaskCreationOptions options = TaskCreationOptions.LongRunning,
+ TaskScheduler scheduler = null)
{
Guard.NotNull(state, nameof(state));
Guard.NotNull(function, nameof(function));
- Guard.NotNull(scheduler, nameof(scheduler));
var ct = _host.CreateCompositeCancellationTokenSource(cancellationToken).Token;
- options |= TaskCreationOptions.LongRunning; // enforce an exclusive thread (not from pool)
var t = Task.Factory.StartNew((o) =>
{
@@ -217,19 +252,61 @@ public static Task Run(Func function,
+ object state,
+ CancellationToken cancellationToken = default(CancellationToken))
+ {
+ Guard.NotNull(state, nameof(state));
+ Guard.NotNull(function, nameof(function));
+
+ var ct = _host.CreateCompositeCancellationTokenSource(cancellationToken).Token;
+
+ var accessor = EngineContext.Current.ContainerManager.ScopeAccessor;
+ var scope = accessor.BeginLifetimeScope(null);
+
+ Task task = null;
+
+ try
+ {
+ task = function(scope, ct).ContinueWith(x =>
+ {
+ scope.Dispose();
+ });
+ _host.Register(task, ct);
+ }
+ catch
+ {
+ scope.Dispose();
+ }
+
+ return task;
+ }
private class ExclusiveSynchronizationContext : SynchronizationContext
{
private bool _done;
- readonly AutoResetEvent _workItemsWaiting = new AutoResetEvent(false);
- readonly Queue> _items = new Queue>();
+ private readonly AutoResetEvent _workItemsWaiting = new AutoResetEvent(false);
+ private readonly EventQueue _items;
+
+ public ExclusiveSynchronizationContext(SynchronizationContext old)
+ {
+ if (old is ExclusiveSynchronizationContext oldEx)
+ {
+ this._items = oldEx._items;
+ }
+ else
+ {
+ this._items = new EventQueue();
+ }
+ }
public Exception InnerException { get; set; }
@@ -240,10 +317,7 @@ public override void Send(SendOrPostCallback d, object state)
public override void Post(SendOrPostCallback d, object state)
{
- lock (_items)
- {
- _items.Enqueue(Tuple.Create(d, state));
- }
+ _items.Enqueue(Tuple.Create(d, state));
_workItemsWaiting.Set();
}
@@ -256,26 +330,19 @@ public void BeginMessageLoop()
{
while (!_done)
{
- Tuple task = null;
- lock (_items)
+ if (!_items.TryDequeue(out var task))
{
- if (_items.Count > 0)
- {
- task = _items.Dequeue();
- }
+ _workItemsWaiting.WaitOne();
}
- if (task != null)
+ else
{
task.Item1(task.Item2);
- if (InnerException != null) // the method threw an exeption
+
+ if (InnerException != null) // method threw an exeption
{
throw new AggregateException("AsyncHelpers.Run method threw an exception.", InnerException);
}
}
- else
- {
- _workItemsWaiting.WaitOne();
- }
}
}
@@ -291,7 +358,7 @@ internal class BackgroundWorkHost : IRegisteredObject
{
private readonly CancellationTokenSource _shutdownCancellationTokenSource = new CancellationTokenSource();
private int _numRunningWorkItems;
-
+
public BackgroundWorkHost()
{
HostingEnvironment.RegisterObject(this);
@@ -337,7 +404,7 @@ public void Register(Task work, CancellationToken cancellationToken)
}
_numRunningWorkItems++;
}
-
+
work.ContinueWith(
WorkItemComplete,
CancellationToken.None,
@@ -353,7 +420,7 @@ private void WorkItemComplete(Task work)
lock (this)
{
num = --_numRunningWorkItems;
- isCancellationRequested = _shutdownCancellationTokenSource.IsCancellationRequested;
+ isCancellationRequested = _shutdownCancellationTokenSource.IsCancellationRequested;
}
if (num == 0 && isCancellationRequested)
{
diff --git a/src/Libraries/SmartStore.Core/Caching/CacheScope.cs b/src/Libraries/SmartStore.Core/Caching/CacheScope.cs
new file mode 100644
index 0000000000..b705ab10b6
--- /dev/null
+++ b/src/Libraries/SmartStore.Core/Caching/CacheScope.cs
@@ -0,0 +1,41 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace SmartStore.Core.Caching
+{
+ public class CacheScope
+ {
+ private HashSet _dependencies;
+
+ public CacheScope(string key)
+ {
+ Key = key;
+ }
+
+ public string Key { get; private set; }
+
+ public void AddDependency(string key)
+ {
+ if (key == null)
+ {
+ return;
+ }
+
+ if (_dependencies == null)
+ {
+ _dependencies = new HashSet();
+ }
+
+ _dependencies.Add(key);
+ }
+
+ public IEnumerable Dependencies
+ {
+ get
+ {
+ return _dependencies ?? Enumerable.Empty();
+ }
+ }
+ }
+}
diff --git a/src/Libraries/SmartStore.Core/Caching/CacheScopeAccessor.cs b/src/Libraries/SmartStore.Core/Caching/CacheScopeAccessor.cs
new file mode 100644
index 0000000000..b04e0a7d45
--- /dev/null
+++ b/src/Libraries/SmartStore.Core/Caching/CacheScopeAccessor.cs
@@ -0,0 +1,62 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using SmartStore.Utilities;
+
+namespace SmartStore.Core.Caching
+{
+ public class CacheScopeAccessor : ICacheScopeAccessor
+ {
+ private readonly Stack _stack = new Stack();
+
+ public CacheScopeAccessor()
+ {
+ }
+
+ public CacheScope Current
+ {
+ get
+ {
+ _stack.TryPeek(out var result);
+ return result;
+ }
+ }
+
+ public bool HasScope(string key)
+ {
+ return _stack.Any(x => x.Key.IsCaseInsensitiveEqual(key));
+ }
+
+ public void PropagateKey(string key)
+ {
+ var current = Current;
+ if (current != null)
+ {
+ current.AddDependency(key);
+ }
+ }
+
+ public IDisposable BeginScope(string key, bool independent = false)
+ {
+ if (Current?.Key == key)
+ {
+ // Void if parent scope has the same key.
+ return ActionDisposable.Empty;
+ }
+
+ _stack.Push(new CacheScope(key));
+
+ var action = new ActionDisposable(() =>
+ {
+ _stack.TryPop(out _);
+
+ if (!independent)
+ {
+ PropagateKey(key);
+ }
+ });
+
+ return action;
+ }
+ }
+}
diff --git a/src/Libraries/SmartStore.Core/Caching/ICacheManager.cs b/src/Libraries/SmartStore.Core/Caching/ICacheManager.cs
index c28fc3d0ff..855349106f 100644
--- a/src/Libraries/SmartStore.Core/Caching/ICacheManager.cs
+++ b/src/Libraries/SmartStore.Core/Caching/ICacheManager.cs
@@ -14,8 +14,9 @@ public interface ICacheManager
///
/// The type of the item to get
/// The cache item key
+ /// When true , no attemp will be made to invalidate depending/parent cache entries.
/// Cached item value or null if item with specified key does not exist in the cache
- T Get(string key);
+ T Get(string key, bool independent = false);
///
/// Gets a cache item associated with the specified key or adds the item
@@ -25,8 +26,9 @@ public interface ICacheManager
/// The cache item key
/// Func which returns value to be added to the cache
/// Absolute expiration time
+ /// When true , no attemp will be made to invalidate depending/parent cache entries.
/// Cached item value
- T Get(string key, Func acquirer, TimeSpan? duration = null);
+ T Get(string key, Func acquirer, TimeSpan? duration = null, bool independent = false);
///
/// Gets a cache item associated with the specified key or - if it doesn't exist in the cache -
@@ -36,16 +38,18 @@ public interface ICacheManager
/// The cache item key
/// Func which returns value to be added to the cache
/// Absolute expiration time
+ /// When true , no attemp will be made to invalidate depending/parent cache entries.
/// Cached item value
- Task GetAsync(string key, Func> acquirer, TimeSpan? duration = null);
+ Task GetAsync(string key, Func> acquirer, TimeSpan? duration = null, bool independent = false);
///
/// Gets a hashset associated with the specified key or. If key does not exist,
/// a new set is created and put to cache automatically
///
/// The set cache item key
+ /// Optional acquirer callback that is invoked when requested set does not exist yet.
/// Cached item value
- ISet GetHashSet(string key);
+ ISet GetHashSet(string key, Func> acquirer = null);
///
/// Adds a cache item with the specified key
@@ -53,7 +57,10 @@ public interface ICacheManager
/// Key
/// Value
/// Absolute expiration time
- void Put(string key, object value, TimeSpan? duration = null);
+ ///
+ /// A list of keys for cache entries that should be removed from the cache whenever this entry changes.
+ ///
+ void Put(string key, object value, TimeSpan? duration = null, IEnumerable dependencies = null);
///
/// Gets a value indicating whether the value associated with the specified key is cached
diff --git a/src/Libraries/SmartStore.Core/Caching/ICacheScopeAccessor.cs b/src/Libraries/SmartStore.Core/Caching/ICacheScopeAccessor.cs
new file mode 100644
index 0000000000..a640e6b200
--- /dev/null
+++ b/src/Libraries/SmartStore.Core/Caching/ICacheScopeAccessor.cs
@@ -0,0 +1,34 @@
+using System;
+
+namespace SmartStore.Core.Caching
+{
+ public interface ICacheScopeAccessor
+ {
+ ///
+ /// Returns the current cache acquire scope (for current thread)
+ ///
+ CacheScope Current { get; }
+
+ ///
+ /// Whether any scope with the passed exists already. Used to prevent lock recursions.
+ ///
+ /// Key
+ bool HasScope(string key);
+
+ ///
+ /// Notifies the current scope about a cache dependency.
+ ///
+ ///
+ void PropagateKey(string key);
+
+ ///
+ /// Begins a new cache acquire scope.
+ ///
+ /// Key
+ ///
+ /// true means that no key propagation will be performed, making the current cache entry independent from its parent.
+ /// Meaning: parent cache entries will not be auto-invalidated when this entry changes.
+ ///
+ IDisposable BeginScope(string key, bool independent = false);
+ }
+}
diff --git a/src/Libraries/SmartStore.Core/Caching/ISet.cs b/src/Libraries/SmartStore.Core/Caching/ISet.cs
index 59decb5d52..d0bff72bd7 100644
--- a/src/Libraries/SmartStore.Core/Caching/ISet.cs
+++ b/src/Libraries/SmartStore.Core/Caching/ISet.cs
@@ -6,6 +6,7 @@ namespace SmartStore.Core.Caching
public interface ISet : IEnumerable
{
bool Add(string item);
+ void AddRange(IEnumerable items);
void Clear();
bool Contains(string item);
bool Remove(string item);
diff --git a/src/Libraries/SmartStore.Core/Caching/MemoryCacheManager.cs b/src/Libraries/SmartStore.Core/Caching/MemoryCacheManager.cs
index 89a2c707e6..a00224784d 100644
--- a/src/Libraries/SmartStore.Core/Caching/MemoryCacheManager.cs
+++ b/src/Libraries/SmartStore.Core/Caching/MemoryCacheManager.cs
@@ -4,27 +4,37 @@
using System.Threading.Tasks;
using System.Collections.Concurrent;
using System.Threading;
-using SmartStore.Core.Async;
-using System.Collections;
using System.Collections.Generic;
using SmartStore.Utilities;
using System.Text.RegularExpressions;
+using System.Diagnostics;
+using SmartStore.Utilities.Threading;
+using SmartStore.Core.Infrastructure.DependencyManagement;
namespace SmartStore.Core.Caching
{
public partial class MemoryCacheManager : DisposableObject, ICacheManager
{
+ const string LockRecursionExceptionMessage = "Acquiring identical cache items recursively is not supported. Key: {0}";
+
// Wwe put a special string into cache if value is null,
// otherwise our 'Contains()' would always return false,
// which is bad if we intentionally wanted to save NULL values.
public const string FakeNull = "__[NULL]__";
- private readonly MemoryCache _cache;
+ private readonly Work _scopeAccessor;
+ private MemoryCache _cache;
private readonly ConcurrentDictionary _keyLocks = new ConcurrentDictionary();
- public MemoryCacheManager()
+ public MemoryCacheManager(Work scopeAccessor)
{
- _cache = new MemoryCache("SmartStore");
+ _scopeAccessor = scopeAccessor;
+ _cache = CreateCache();
+ }
+
+ private MemoryCache CreateCache()
+ {
+ return new MemoryCache("SmartStore");
}
public bool IsDistributedCache
@@ -32,7 +42,7 @@ public bool IsDistributedCache
get { return false; }
}
- private bool TryGet(string key, out T value)
+ private bool TryGet(string key, bool independent, out T value)
{
value = default(T);
@@ -40,6 +50,12 @@ private bool TryGet(string key, out T value)
if (obj != null)
{
+ // Make the parent scope's entry depend on this
+ if (!independent)
+ {
+ _scopeAccessor.Value.PropagateKey(key);
+ }
+
if (obj.Equals(FakeNull))
{
return true;
@@ -52,65 +68,74 @@ private bool TryGet(string key, out T value)
return false;
}
- public T Get(string key)
+ public T Get(string key, bool independent = false)
{
- T value;
- TryGet(key, out value);
-
+ TryGet(key, independent, out T value);
return value;
}
- public T Get(string key, Func acquirer, TimeSpan? duration = null)
+ public T Get(string key, Func acquirer, TimeSpan? duration = null, bool independent = false)
{
- T value;
-
- if (TryGet(key, out value))
+ if (TryGet(key, independent, out T value))
{
return value;
}
- lock (String.Intern(key))
+ if (_scopeAccessor.Value.HasScope(key))
{
- // atomic operation must be outer locked
- if (!TryGet(key, out value))
+ throw new LockRecursionException(LockRecursionExceptionMessage.FormatInvariant(key));
+ }
+
+ // Get the (semaphore) locker specific to this key
+ using (KeyedLock.Lock("cache:" + key, TimeSpan.FromMinutes(1)))
+ {
+ // Atomic operation must be outer locked
+ if (!TryGet(key, independent, out value))
{
- value = acquirer();
- Put(key, value, duration);
- return value;
+ using (_scopeAccessor.Value.BeginScope(key))
+ {
+ value = acquirer();
+ Put(key, value, duration, _scopeAccessor.Value.Current.Dependencies);
+ return value;
+ }
}
}
return value;
}
- public async Task GetAsync(string key, Func> acquirer, TimeSpan? duration = null)
+ public async Task GetAsync(string key, Func> acquirer, TimeSpan? duration = null, bool independent = false)
{
- T value;
-
- if (TryGet(key, out value))
+ if (TryGet(key, independent, out T value))
{
return value;
}
- // get the async (semaphore) locker specific to this key
- var keyLock = AsyncLock.Acquire(key);
+ if (_scopeAccessor.Value.HasScope(key))
+ {
+ throw new LockRecursionException(LockRecursionExceptionMessage.FormatInvariant(key));
+ }
- using (await keyLock.LockAsync())
+ // Get the async (semaphore) locker specific to this key
+ using (await KeyedLock.LockAsync("cache:" + key, TimeSpan.FromMinutes(1)))
{
- if (!TryGet(key, out value))
+ if (!TryGet(key, independent, out value))
{
- value = await acquirer().ConfigureAwait(false);
- Put(key, value, duration);
- return value;
+ using (_scopeAccessor.Value.BeginScope(key))
+ {
+ value = await acquirer();
+ Put(key, value, duration, _scopeAccessor.Value.Current.Dependencies);
+ return value;
+ }
}
}
return value;
}
- public void Put(string key, object value, TimeSpan? duration = null)
+ public void Put(string key, object value, TimeSpan? duration = null, IEnumerable dependencies = null)
{
- _cache.Set(key, value ?? FakeNull, GetCacheItemPolicy(duration));
+ _cache.Set(key, value ?? FakeNull, GetCacheItemPolicy(duration, dependencies));
}
public bool Contains(string key)
@@ -135,39 +160,53 @@ public IEnumerable Keys(string pattern)
}
var wildcard = new Wildcard(pattern, RegexOptions.IgnoreCase);
- return keys.Where(x => wildcard.IsMatch(x));
+ return keys.Where(x => wildcard.IsMatch(x)).ToArray();
}
public int RemoveByPattern(string pattern)
{
- var keysToRemove = Keys(pattern);
- int count = 0;
-
lock (_cache)
{
+ var keysToRemove = Keys(pattern);
+ int count = 0;
+
// lock atomic operation
foreach (string key in keysToRemove)
{
_cache.Remove(key);
count++;
}
- }
- return count;
+ return count;
+ }
}
public void Clear()
{
- RemoveByPattern("*");
- }
+ // Faster way of clearing cache: https://stackoverflow.com/questions/8043381/how-do-i-clear-a-system-runtime-caching-memorycache
+ var oldCache = Interlocked.Exchange(ref _cache, CreateCache());
+ oldCache.Dispose();
+ GC.Collect();
+ }
- public virtual ISet GetHashSet(string key)
+ public virtual ISet GetHashSet(string key, Func> acquirer = null)
{
- var set = Get(key, () => new MemorySet(this));
- return set;
+ var result = Get(key, () =>
+ {
+ var set = new MemorySet(this);
+ var items = acquirer?.Invoke();
+ if (items != null)
+ {
+ set.AddRange(items);
+ }
+
+ return set;
+ });
+
+ return result;
}
- private CacheItemPolicy GetCacheItemPolicy(TimeSpan? duration)
+ private CacheItemPolicy GetCacheItemPolicy(TimeSpan? duration, IEnumerable dependencies)
{
var absoluteExpiration = ObjectCache.InfiniteAbsoluteExpiration;
@@ -175,16 +214,35 @@ private CacheItemPolicy GetCacheItemPolicy(TimeSpan? duration)
{
absoluteExpiration = DateTime.UtcNow + duration.Value;
}
-
+
var cacheItemPolicy = new CacheItemPolicy
{
AbsoluteExpiration = absoluteExpiration,
SlidingExpiration = ObjectCache.NoSlidingExpiration
};
+
+ if (dependencies != null && dependencies.Any())
+ {
+ // INFO: we can only depend on existing items, otherwise this entry will be removed immediately.
+ dependencies = dependencies.Where(x => x != null && _cache.Contains(x));
+ if (dependencies.Any())
+ {
+ cacheItemPolicy.ChangeMonitors.Add(_cache.CreateCacheEntryChangeMonitor(dependencies));
+ }
+ }
+
+ //cacheItemPolicy.RemovedCallback = OnRemoveEntry;
return cacheItemPolicy;
}
+ //private void OnRemoveEntry(CacheEntryRemovedArguments args)
+ //{
+ // if (args.RemovedReason == CacheEntryRemovedReason.ChangeMonitorChanged)
+ // {
+ // Debug.WriteLine("MEMCACHE: remove depending entry '{0}'.".FormatInvariant(args.CacheItem.Key));
+ // }
+ //}
protected override void OnDispose(bool disposing)
{
diff --git a/src/Libraries/SmartStore.Core/Caching/MemorySet.cs b/src/Libraries/SmartStore.Core/Caching/MemorySet.cs
index 51254daa26..c88cacd703 100644
--- a/src/Libraries/SmartStore.Core/Caching/MemorySet.cs
+++ b/src/Libraries/SmartStore.Core/Caching/MemorySet.cs
@@ -16,6 +16,11 @@ public MemorySet(ICacheManager cache)
_cache = cache;
}
+ public void AddRange(IEnumerable items)
+ {
+ base.UnionWith(items);
+ }
+
public bool Move(string destinationKey, string item)
{
if (this.Contains(item))
diff --git a/src/Libraries/SmartStore.Core/Caching/NullCache.cs b/src/Libraries/SmartStore.Core/Caching/NullCache.cs
index ae8d19b285..e75abfd91d 100644
--- a/src/Libraries/SmartStore.Core/Caching/NullCache.cs
+++ b/src/Libraries/SmartStore.Core/Caching/NullCache.cs
@@ -23,12 +23,12 @@ public bool IsDistributedCache
get { return false; }
}
- public T Get(string key)
+ public T Get(string key, bool independent = false)
{
return default(T);
}
- public T Get(string key, Func acquirer, TimeSpan? duration = null)
+ public T Get(string key, Func acquirer, TimeSpan? duration = null, bool independent = false)
{
if (acquirer == null)
{
@@ -37,7 +37,7 @@ public T Get(string key, Func acquirer, TimeSpan? duration = null)
return acquirer();
}
- public Task GetAsync(string key, Func> acquirer, TimeSpan? duration = null)
+ public Task GetAsync(string key, Func> acquirer, TimeSpan? duration = null, bool independent = false)
{
if (acquirer == null)
{
@@ -47,12 +47,12 @@ public Task GetAsync(string key, Func> acquirer, TimeSpan? duratio
}
- public ISet GetHashSet(string key)
+ public ISet GetHashSet(string key, Func> acquirer = null)
{
return new MemorySet(this);
}
- public void Put(string key, object value, TimeSpan? duration = null)
+ public void Put(string key, object value, TimeSpan? duration = null, IEnumerable dependencies = null)
{
}
diff --git a/src/Libraries/SmartStore.Core/Collections/ConcurrentMultimap.cs b/src/Libraries/SmartStore.Core/Collections/ConcurrentMultimap.cs
index 0e02723ec4..86d5b00249 100644
--- a/src/Libraries/SmartStore.Core/Collections/ConcurrentMultimap.cs
+++ b/src/Libraries/SmartStore.Core/Collections/ConcurrentMultimap.cs
@@ -4,6 +4,8 @@
using System.Collections.Concurrent;
using System.Linq;
using Newtonsoft.Json;
+using SmartStore.Utilities;
+using System.Threading;
namespace SmartStore.Collections
{
@@ -14,40 +16,65 @@ namespace SmartStore.Collections
/// The type of value.
[JsonConverter(typeof(ConcurrentMultiMapConverter))]
[Serializable]
- public class ConcurrentMultimap : IEnumerable>>
+ public class ConcurrentMultimap : IEnumerable>>
{
- private readonly ConcurrentDictionary> _items;
- private static readonly Func, IProducerConsumerCollection> _bagCreator = (col) => new ConcurrentBag(col);
+ private readonly ConcurrentDictionary> _dict;
+ private readonly Func, ICollection> _collectionCreator;
public ConcurrentMultimap()
- : this(null, null)
+ : this(null, null, null)
{
}
public ConcurrentMultimap(IEqualityComparer comparer)
- : this(null, comparer)
+ : this(null, comparer, null)
{
}
- public ConcurrentMultimap(IEnumerable>> items)
- : this(items, null)
+ public ConcurrentMultimap(IEnumerable>> items)
+ : this(items, null, null)
{
// for serialization
}
- public ConcurrentMultimap(IEnumerable>> items, IEqualityComparer comparer)
+ public ConcurrentMultimap(
+ IEnumerable>> items,
+ IEqualityComparer comparer)
+ : this(items, comparer, null)
{
- _items = new ConcurrentDictionary>(comparer ?? EqualityComparer.Default);
+ // for serialization
+ }
+
+ public ConcurrentMultimap(
+ IEnumerable>> items,
+ IEqualityComparer comparer,
+ Func, ICollection> collectionCreator)
+ {
+ _collectionCreator = collectionCreator;
+ _dict = new ConcurrentDictionary>(
+ ConvertItems(items),
+ comparer ?? EqualityComparer.Default);
+ }
+
+ private IEnumerable>> ConvertItems(IEnumerable>> items)
+ {
+ if (items == null)
+ yield break;
- if (items != null)
+ foreach (var item in items)
{
- foreach (var kvp in items)
- {
- _items.TryAdd(kvp.Key, _bagCreator(kvp.Value ?? Enumerable.Empty()));
- }
+ yield return new KeyValuePair>(item.Key, CreateCollection(item.Value));
}
}
+ protected virtual SyncedCollection CreateCollection(IEnumerable values)
+ {
+ var creator = _collectionCreator ?? Multimap.DefaultCollectionCreator;
+ var col = creator(values ?? Enumerable.Empty());
+
+ return col.AsSynchronized();
+ }
+
///
/// Gets the count of groups/keys.
///
@@ -55,7 +82,7 @@ public int Count
{
get
{
- return _items.Keys.Count;
+ return _dict.Keys.Count;
}
}
@@ -66,25 +93,33 @@ public int TotalValueCount
{
get
{
- return this._items.Values.Sum(x => x.Count);
+ return this._dict.Values.Sum(x => x.Count);
}
}
+ ///
+ /// Gets the count of items in the requested group.
+ ///
+ public int ValueCount(TKey key)
+ {
+ if (_dict.TryGetValue(key, out var col))
+ {
+ return col.Count;
+ }
+
+ return 0;
+ }
+
///
/// Gets the collection of values stored under the specified key.
///
/// The key.
- public virtual IProducerConsumerCollection this[TKey key]
+ public virtual SyncedCollection this[TKey key]
{
get
{
- IProducerConsumerCollection value;
- if (!_items.TryGetValue(key, out value))
- {
- _items.TryAdd(key, _bagCreator(Enumerable.Empty()));
- }
-
- return value;
+ GetOrCreateValues(key, null, out var col);
+ return col;
}
}
@@ -93,15 +128,15 @@ public virtual IProducerConsumerCollection this[TKey key]
///
public virtual ICollection Keys
{
- get { return _items.Keys; }
+ get { return _dict.Keys; }
}
///
- /// Gets the collection of collections of values.
+ /// Gets all value collections.
///
- public virtual ICollection> Values
+ public virtual ICollection> Values
{
- get { return _items.Values; }
+ get { return _dict.Values; }
}
///
@@ -111,7 +146,10 @@ public virtual ICollection> Values
/// The value.
public virtual void TryAdd(TKey key, TValue value)
{
- this[key].TryAdd(value);
+ if (!GetOrCreateValues(key, new[] { value }, out var col))
+ {
+ col.Add(value);
+ }
}
///
@@ -123,18 +161,63 @@ public virtual void TryAddRange(TKey key, IEnumerable values)
{
Guard.NotNull(values, nameof(values));
- values.Each(x => this[key].TryAdd(x));
+ if (!GetOrCreateValues(key, values, out var col))
+ {
+ col.AddRange(values);
+ }
+ }
+
+ ///
+ /// Attempts to remove the specified value for the specified key.
+ ///
+ /// The key.
+ /// The value.
+ /// True if such a value existed and was removed; otherwise false .
+ public virtual bool TryRemove(TKey key, TValue value)
+ {
+ if (!_dict.ContainsKey(key))
+ return false;
+
+ var col = _dict[key];
+ var removed = false;
+
+ removed = col.Remove(value);
+ if (col.Count == 0)
+ {
+ _dict.TryRemove(key, out _);
+ }
+
+ return removed;
+ }
+
+ ///
+ /// Attempts to remove a range of values for the specified key.
+ ///
+ /// The key.
+ /// The values to remove from the group.
+ /// True if at least one item in group has been removed; otherwise false .
+ public virtual bool TryRemoveRange(TKey key, IEnumerable values)
+ {
+ Guard.NotNull(values, nameof(values));
+
+ int numRemoved = 0;
+
+ if (_dict.TryGetValue(key, out var col))
+ {
+ numRemoved = col.RemoveRange(values);
+ }
+
+ return numRemoved > 0;
}
///
- /// Attempts to remove all values for the specified key.
+ /// Attempts to remove and return all values for the specified key.
///
/// The key.
/// True if any such values existed; otherwise false .
- public virtual bool TryRemoveAll(TKey key)
+ public virtual bool TryRemoveAll(TKey key, out SyncedCollection collection)
{
- IProducerConsumerCollection collection;
- return _items.TryRemove(key, out collection);
+ return _dict.TryRemove(key, out collection);
}
///
@@ -142,7 +225,7 @@ public virtual bool TryRemoveAll(TKey key)
///
public virtual void Clear()
{
- _items.Clear();
+ _dict.Clear();
}
///
@@ -152,7 +235,7 @@ public virtual void Clear()
/// True if the multimap has one or more values for the specified key, otherwise false .
public virtual bool ContainsKey(TKey key)
{
- return _items.ContainsKey(key);
+ return _dict.ContainsKey(key);
}
///
@@ -163,7 +246,7 @@ public virtual bool ContainsKey(TKey key)
/// True if the multimap contains such a value; otherwise, false .
public virtual bool ContainsValue(TKey key, TValue value)
{
- return _items.ContainsKey(key) && _items[key].Contains(value);
+ return _dict.ContainsKey(key) && _dict[key].Contains(value);
}
///
@@ -179,12 +262,26 @@ IEnumerator IEnumerable.GetEnumerator()
/// Returns an enumerator that iterates through the multimap.
///
/// An object that can be used to iterate through the multimap.
- public virtual IEnumerator>> GetEnumerator()
+ public virtual IEnumerator>> GetEnumerator()
{
- foreach (var pair in _items)
+ foreach (var pair in _dict)
yield return pair;
}
+ private bool GetOrCreateValues(TKey key, IEnumerable initial, out SyncedCollection col)
+ {
+ // Return true when created
+ var created = false;
+
+ col = _dict.GetOrAdd(key, k =>
+ {
+ created = true;
+ return CreateCollection(initial);
+ });
+
+ return created;
+ }
+
#region Static members
public static ConcurrentMultimap CreateFromLookup(ILookup source)
diff --git a/src/Libraries/SmartStore.Core/Collections/MostRecentlyUsedList.cs b/src/Libraries/SmartStore.Core/Collections/MostRecentlyUsedList.cs
deleted file mode 100644
index 114c4e24bf..0000000000
--- a/src/Libraries/SmartStore.Core/Collections/MostRecentlyUsedList.cs
+++ /dev/null
@@ -1,101 +0,0 @@
-using System.Collections;
-using System.Collections.Generic;
-using System.Linq;
-
-namespace SmartStore.Collections
-{
- public class MostRecentlyUsedList : IEnumerable
- {
- private readonly int _maxSize;
- private readonly List _mru;
-
- public MostRecentlyUsedList(int maxSize)
- {
- _maxSize = maxSize;
- _mru = new List();
- }
-
- public MostRecentlyUsedList(IEnumerable collection, int maxSize)
- {
- _maxSize = maxSize;
- _mru = collection.ToList();
-
- Normalize();
- }
-
- public MostRecentlyUsedList(string collection, int maxSize)
- {
- _maxSize = maxSize;
- _mru = collection.SplitSafe(Delimiter).Cast().ToList();
-
- Normalize();
- }
-
- public MostRecentlyUsedList(IEnumerable collection, T newItem, int maxSize)
- {
- _maxSize = maxSize;
- _mru = collection.ToList();
-
- Add(newItem);
- }
-
- public MostRecentlyUsedList(string collection, T newItem, int maxSize)
- {
- _maxSize = maxSize;
- _mru = collection.SplitSafe(Delimiter).Cast().Distinct().ToList();
-
- Add(newItem);
- }
-
- public static string Delimiter { get { return ";"; } }
-
- private void Normalize()
- {
- if (_maxSize >= 0)
- {
- while (_mru.Count > _maxSize)
- _mru.RemoveAt(_mru.Count - 1);
- }
- }
-
- public IEnumerator GetEnumerator()
- {
- return _mru.GetEnumerator();
- }
-
- IEnumerator IEnumerable.GetEnumerator()
- {
- return _mru.GetEnumerator();
- }
-
- public override string ToString()
- {
- return string.Join(Delimiter, _mru);
- }
-
- public T this[int key]
- {
- get
- {
- return _mru[key];
- }
- set
- {
- _mru[key] = value;
- }
- }
-
- public int Count { get { return _mru.Count; } }
-
- public void Add(T item)
- {
- int i = _mru.IndexOf(item);
- if (i > -1)
- _mru.RemoveAt(i);
-
- _mru.Insert(0, item);
-
- Normalize();
- }
- }
-}
diff --git a/src/Libraries/SmartStore.Core/Collections/MultiMap.cs b/src/Libraries/SmartStore.Core/Collections/MultiMap.cs
index 6c8f85fc78..738ffab5fa 100644
--- a/src/Libraries/SmartStore.Core/Collections/MultiMap.cs
+++ b/src/Libraries/SmartStore.Core/Collections/MultiMap.cs
@@ -1,7 +1,6 @@
using System;
using System.Collections;
using System.Collections.Generic;
-using System.Collections.Concurrent;
using System.Linq;
using System.Linq.Expressions;
using Newtonsoft.Json;
@@ -17,11 +16,12 @@ namespace SmartStore.Collections
[Serializable]
public class Multimap : IEnumerable>>
{
- private readonly IDictionary> _items;
+ private readonly IDictionary> _dict;
private readonly Func, ICollection> _collectionCreator;
private readonly bool _isReadonly = false;
- private readonly static Func, ICollection> _defaultCollectionCreator = x => new List(x ?? Enumerable.Empty());
+ internal readonly static Func, ICollection> DefaultCollectionCreator =
+ x => new List(x ?? Enumerable.Empty());
public Multimap()
: this(EqualityComparer.Default)
@@ -30,8 +30,8 @@ public Multimap()
public Multimap(IEqualityComparer comparer)
{
- _items = new Dictionary>(comparer ?? EqualityComparer.Default);
- _collectionCreator = _defaultCollectionCreator;
+ _dict = new Dictionary>(comparer ?? EqualityComparer.Default);
+ _collectionCreator = DefaultCollectionCreator;
}
public Multimap(Func, ICollection> collectionCreator)
@@ -49,7 +49,7 @@ internal Multimap(IDictionary> dictionary, Func> dictionary, bool isRea
{
Guard.NotNull(dictionary, nameof(dictionary));
- _items = dictionary;
+ _dict = dictionary;
if (isReadonly && dictionary != null)
{
@@ -81,21 +81,20 @@ public Multimap(IEnumerable>> items, IEqu
// for serialization
Guard.NotNull(items, nameof(items));
- _items = new Dictionary>(comparer ?? EqualityComparer.Default);
- _collectionCreator = _defaultCollectionCreator;
+ _dict = new Dictionary>(comparer ?? EqualityComparer.Default);
if (items != null)
{
foreach (var kvp in items)
{
- _items[kvp.Key] = CreateCollection(kvp.Value);
+ _dict[kvp.Key] = CreateCollection(kvp.Value);
}
}
}
protected virtual ICollection CreateCollection(IEnumerable values)
{
- return (_collectionCreator ?? _defaultCollectionCreator)(values ?? Enumerable.Empty());
+ return (_collectionCreator ?? DefaultCollectionCreator)(values ?? Enumerable.Empty());
}
///
@@ -105,7 +104,7 @@ public int Count
{
get
{
- return this._items.Keys.Count;
+ return this._dict.Keys.Count;
}
}
@@ -116,7 +115,7 @@ public int TotalValueCount
{
get
{
- return this._items.Values.Sum(x => x.Count);
+ return this._dict.Values.Sum(x => x.Count);
}
}
@@ -128,15 +127,15 @@ public virtual ICollection this[TKey key]
{
get
{
- if (!_items.ContainsKey(key))
+ if (!_dict.ContainsKey(key))
{
if (!_isReadonly)
- _items[key] = CreateCollection(null);
+ _dict[key] = CreateCollection(null);
else
return null;
}
- return _items[key];
+ return _dict[key];
}
}
@@ -145,15 +144,15 @@ public virtual ICollection this[TKey key]
///
public virtual ICollection Keys
{
- get { return _items.Keys; }
+ get { return _dict.Keys; }
}
- ///
- /// Gets the collection of collections of values.
- ///
+ ///
+ /// Gets all value collections.
+ ///
public virtual ICollection> Values
{
- get { return _items.Values; }
+ get { return _dict.Values; }
}
public IEnumerable Find(TKey key, Expression> predicate)
@@ -161,9 +160,9 @@ public IEnumerable Find(TKey key, Expression> predica
Guard.NotNull(key, nameof(key));
Guard.NotNull(predicate, nameof(predicate));
- if (_items.ContainsKey(key))
+ if (_dict.ContainsKey(key))
{
- return _items[key].Where(predicate.Compile());
+ return _dict[key].Where(predicate.Compile());
}
return Enumerable.Empty();
@@ -188,7 +187,10 @@ public virtual void Add(TKey key, TValue value)
/// The values.
public virtual void AddRange(TKey key, IEnumerable values)
{
- CheckNotReadonly();
+ if (values == null || !values.Any())
+ return;
+
+ CheckNotReadonly();
this[key].AddRange(values);
}
@@ -203,12 +205,12 @@ public virtual bool Remove(TKey key, TValue value)
{
CheckNotReadonly();
- if (!_items.ContainsKey(key))
+ if (!_dict.ContainsKey(key))
return false;
- bool result = _items[key].Remove(value);
- if (_items[key].Count == 0)
- _items.Remove(key);
+ bool result = _dict[key].Remove(value);
+ if (_dict[key].Count == 0)
+ _dict.Remove(key);
return result;
}
@@ -221,7 +223,7 @@ public virtual bool Remove(TKey key, TValue value)
public virtual bool RemoveAll(TKey key)
{
CheckNotReadonly();
- return _items.Remove(key);
+ return _dict.Remove(key);
}
///
@@ -230,7 +232,7 @@ public virtual bool RemoveAll(TKey key)
public virtual void Clear()
{
CheckNotReadonly();
- _items.Clear();
+ _dict.Clear();
}
///
@@ -240,7 +242,7 @@ public virtual void Clear()
/// True if the multimap has one or more values for the specified key, otherwise false .
public virtual bool ContainsKey(TKey key)
{
- return _items.ContainsKey(key);
+ return _dict.ContainsKey(key);
}
///
@@ -251,7 +253,7 @@ public virtual bool ContainsKey(TKey key)
/// True if the multimap contains such a value; otherwise, false .
public virtual bool ContainsValue(TKey key, TValue value)
{
- return _items.ContainsKey(key) && _items[key].Contains(value);
+ return _dict.ContainsKey(key) && _dict[key].Contains(value);
}
///
@@ -269,7 +271,7 @@ IEnumerator IEnumerable.GetEnumerator()
/// An object that can be used to iterate through the multimap.
public virtual IEnumerator>> GetEnumerator()
{
- foreach (KeyValuePair> pair in _items)
+ foreach (KeyValuePair> pair in _dict)
yield return pair;
}
diff --git a/src/Libraries/SmartStore.Core/Collections/SyncedCollection.cs b/src/Libraries/SmartStore.Core/Collections/SyncedCollection.cs
new file mode 100644
index 0000000000..d8100effd8
--- /dev/null
+++ b/src/Libraries/SmartStore.Core/Collections/SyncedCollection.cs
@@ -0,0 +1,222 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace SmartStore.Collections
+{
+ public sealed class SyncedCollection : ICollection
+ {
+ // INFO: Don't call it SynchronizedCollection because of framework dupe.
+ private readonly ICollection _col;
+
+ public SyncedCollection(ICollection wrappedCollection)
+ : this(wrappedCollection, new object())
+ {
+ }
+
+ public SyncedCollection(ICollection wrappedCollection, object syncRoot)
+ {
+ Guard.NotNull(wrappedCollection, nameof(wrappedCollection));
+ Guard.NotNull(syncRoot, nameof(syncRoot));
+
+ _col = wrappedCollection;
+ SyncRoot = syncRoot;
+ }
+
+ public object SyncRoot { get; }
+
+ public bool ReadLockFree { get; set; }
+
+ public void AddRange(IEnumerable collection)
+ {
+ lock (SyncRoot)
+ {
+ _col.AddRange(collection);
+ }
+ }
+
+ public void Insert(int index, T item)
+ {
+ if (_col is List list)
+ {
+ lock (SyncRoot)
+ {
+ list.Insert(index, item);
+ }
+ }
+
+ throw new NotSupportedException();
+ }
+
+ public void InsertRange(int index, IEnumerable values)
+ {
+ if (_col is List list)
+ {
+ lock (SyncRoot)
+ {
+ list.InsertRange(index, values);
+ }
+ }
+
+ throw new NotSupportedException();
+ }
+
+ public int RemoveRange(IEnumerable values)
+ {
+ int numRemoved = 0;
+
+ lock (SyncRoot)
+ {
+ foreach (var value in values)
+ {
+ if (_col.Remove(value))
+ numRemoved++;
+ }
+ }
+
+ return numRemoved;
+ }
+
+ public void RemoveRange(int index, int count)
+ {
+ if (_col is List list)
+ {
+ lock (SyncRoot)
+ {
+ list.RemoveRange(index, count);
+ }
+ }
+
+ throw new NotSupportedException();
+ }
+
+ public void RemoveAt(int index)
+ {
+ lock (SyncRoot)
+ {
+ if (_col is List list)
+ {
+ list.RemoveAt(index);
+ }
+ else
+ {
+ var item = _col.ElementAtOrDefault(index);
+ if (item != null)
+ {
+ _col.Remove(item);
+ }
+ }
+ }
+ }
+
+ public T this[int index]
+ {
+ get
+ {
+ if (ReadLockFree)
+ {
+ return _col.ElementAt(index);
+ }
+ else
+ {
+ lock (SyncRoot)
+ {
+ return _col.ElementAt(index);
+ }
+ }
+ }
+ }
+
+ #region ICollection
+
+ public int Count
+ {
+ get
+ {
+ if (ReadLockFree)
+ {
+ return _col.Count();
+ }
+ else
+ {
+ lock (SyncRoot)
+ {
+ return _col.Count();
+ }
+ }
+ }
+ }
+
+ public bool IsReadOnly => _col.IsReadOnly;
+
+ public void Add(T item)
+ {
+ lock (SyncRoot)
+ {
+ _col.Add(item);
+ }
+ }
+
+ public void Clear()
+ {
+ lock (SyncRoot)
+ {
+ _col.Clear();
+ }
+ }
+
+ public bool Contains(T item)
+ {
+ if (ReadLockFree)
+ {
+ return _col.Contains(item);
+ }
+ else
+ {
+ lock (SyncRoot)
+ {
+ return _col.Contains(item);
+ }
+ }
+ }
+
+ public void CopyTo(T[] array, int arrayIndex)
+ {
+ lock (SyncRoot)
+ {
+ _col.CopyTo(array, arrayIndex);
+ }
+ }
+
+ public bool Remove(T item)
+ {
+ lock (SyncRoot)
+ {
+ return _col.Remove(item);
+ }
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return this.GetEnumerator();
+ }
+
+ public IEnumerator GetEnumerator()
+ {
+ if (ReadLockFree)
+ {
+ return _col.GetEnumerator();
+ }
+ else
+ {
+ lock (SyncRoot)
+ {
+ return _col.GetEnumerator();
+ }
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/src/Libraries/SmartStore.Core/Collections/TreeNode.cs b/src/Libraries/SmartStore.Core/Collections/TreeNode.cs
index 19cf141de9..00034d0778 100644
--- a/src/Libraries/SmartStore.Core/Collections/TreeNode.cs
+++ b/src/Libraries/SmartStore.Core/Collections/TreeNode.cs
@@ -227,7 +227,17 @@ public override void WriteJson(JsonWriter writer, object value, JsonSerializer s
if (GetPropValue("Metadata", value) is IDictionary dict && dict.Count > 0)
{
writer.WritePropertyName("Metadata");
- serializer.Serialize(writer, dict);
+
+ var typeNameHandling = serializer.TypeNameHandling;
+ try
+ {
+ serializer.TypeNameHandling = TypeNameHandling.All;
+ serializer.Serialize(writer, dict);
+ }
+ finally
+ {
+ serializer.TypeNameHandling = typeNameHandling;
+ }
}
// Children
diff --git a/src/Libraries/SmartStore.Core/Collections/TreeNodeBase.cs b/src/Libraries/SmartStore.Core/Collections/TreeNodeBase.cs
index 8dd7bee9b2..8264db12e1 100644
--- a/src/Libraries/SmartStore.Core/Collections/TreeNodeBase.cs
+++ b/src/Libraries/SmartStore.Core/Collections/TreeNodeBase.cs
@@ -40,13 +40,10 @@ public object Id
{
var map = GetIdNodeMap();
- if (_id != value)
+ if (_id != null && map.ContainsKey(_id))
{
- if (_id != null && map.ContainsKey(_id))
- {
- // Remove old id from map
- map.Remove(_id);
- }
+ // Remove old id from map
+ map.Remove(_id);
}
if (value != null)
@@ -142,7 +139,12 @@ public TMetadata GetMetadata(string key, bool recursive = true)
break;
}
- return (TMetadata)metadata;
+ if (metadata != null)
+ {
+ return (TMetadata)metadata;
+ }
+
+ return default(TMetadata);
}
private bool TryGetMetadataForNode(TreeNodeBase node, string key, out object metadata)
diff --git a/src/Libraries/SmartStore.Core/Collections/TrimmedBuffer.cs b/src/Libraries/SmartStore.Core/Collections/TrimmedBuffer.cs
new file mode 100644
index 0000000000..db54bf8936
--- /dev/null
+++ b/src/Libraries/SmartStore.Core/Collections/TrimmedBuffer.cs
@@ -0,0 +1,106 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace SmartStore.Collections
+{
+ public sealed class TrimmedBuffer : ICollection
+ {
+ private readonly int _maxSize;
+ private readonly List _list;
+
+ public TrimmedBuffer(int maxSize)
+ : this((IEnumerable)null, default(T), maxSize)
+ {
+ }
+
+ public TrimmedBuffer(IEnumerable collection, int maxSize)
+ : this(collection, default(T), maxSize)
+ {
+ }
+
+ public TrimmedBuffer(string collection, int maxSize)
+ : this(collection.NullEmpty()?.Convert>()?.Distinct(), default(T), maxSize)
+ {
+ }
+
+ public TrimmedBuffer(string collection, T newItem, int maxSize)
+ : this(collection.NullEmpty()?.Convert>()?.Distinct(), newItem, maxSize)
+ {
+ }
+
+ public TrimmedBuffer(IEnumerable collection, T newItem, int maxSize)
+ {
+ Guard.IsTrue(maxSize >= 0, nameof(maxSize));
+
+ _maxSize = maxSize;
+ _list = new List(collection ?? Enumerable.Empty());
+
+ if (newItem != null)
+ {
+ Add(newItem);
+ }
+
+ Trim();
+ }
+
+ private void Trim()
+ {
+ if (_maxSize >= 0)
+ {
+ while (_list.Count > _maxSize)
+ _list.RemoveAt(_list.Count - 1);
+ }
+ }
+
+ public T this[int index]
+ {
+ get
+ {
+ return _list[index];
+ }
+ set
+ {
+ _list[index] = value;
+ }
+ }
+
+ public void Add(T item)
+ {
+ var i = _list.IndexOf(item);
+ if (i > -1)
+ _list.RemoveAt(i);
+
+ _list.Insert(0, item);
+
+ Trim();
+ }
+
+ public void Clear() => _list.Clear();
+
+ public bool Contains(T item) => _list.Contains(item);
+
+ public void CopyTo(T[] array, int arrayIndex) => _list.CopyTo(array, arrayIndex);
+
+ public bool Remove(T item) => _list.Remove(item);
+
+ public int Count => _list.Count;
+
+ public bool IsReadOnly => false;
+
+ public IEnumerator GetEnumerator()
+ {
+ return _list.GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return _list.GetEnumerator();
+ }
+
+ public override string ToString()
+ {
+ return string.Join(";", _list);
+ }
+ }
+}
diff --git a/src/Libraries/SmartStore.Core/ComponentModel/FastActivator.cs b/src/Libraries/SmartStore.Core/ComponentModel/FastActivator.cs
index dea60d7a62..85419958d3 100644
--- a/src/Libraries/SmartStore.Core/ComponentModel/FastActivator.cs
+++ b/src/Libraries/SmartStore.Core/ComponentModel/FastActivator.cs
@@ -93,8 +93,7 @@ public static FastActivator[] GetActivators(Type type)
private static FastActivator[] GetActivatorsCore(Type type)
{
- FastActivator[] activators;
- if (!_activatorsCache.TryGetValue(type, out activators))
+ if (!_activatorsCache.TryGetValue(type, out FastActivator[] activators))
{
var candidates = GetCandidateConstructors(type);
activators = candidates.Select(c => new FastActivator(c)).ToArray();
diff --git a/src/Libraries/SmartStore.Core/ComponentModel/FastInvoker.cs b/src/Libraries/SmartStore.Core/ComponentModel/FastInvoker.cs
index d88dbc43ae..ba43358e1a 100644
--- a/src/Libraries/SmartStore.Core/ComponentModel/FastInvoker.cs
+++ b/src/Libraries/SmartStore.Core/ComponentModel/FastInvoker.cs
@@ -136,21 +136,6 @@ public static object Invoke(object obj, string methodName, Type[] parameterTypes
return invoker.Invoke(obj, parameters ?? Array.Empty());
}
- ///
- /// Creates and caches a fast method invoker.
- ///
- /// Method info instance to create an invoker for.
- /// The fast method invoker.
- public static FastInvoker GetInvoker(MethodInfo methodInfo)
- {
- Guard.NotNull(methodInfo, nameof(methodInfo));
-
- return GetInvoker(
- methodInfo.DeclaringType,
- methodInfo.Name,
- methodInfo.GetParameters().Select(x => x.ParameterType).ToArray());
- }
-
///
/// Creates and caches a fast method invoker.
///
@@ -192,6 +177,26 @@ public static FastInvoker GetInvoker(Type type, string methodName, params Type[]
return invoker;
}
+ ///
+ /// Creates and caches a fast method invoker.
+ ///
+ /// Method info instance to create an invoker for.
+ /// The fast method invoker.
+ public static FastInvoker GetInvoker(MethodInfo method)
+ {
+ Guard.NotNull(method, nameof(method));
+
+ var cacheKey = MethodKey.Create(method);
+
+ if (!_invokersCache.TryGetValue(cacheKey, out var invoker))
+ {
+ invoker = new FastInvoker(method);
+ _invokersCache.TryAdd(cacheKey, invoker);
+ }
+
+ return invoker;
+ }
+
private static MethodInfo FindMatchingMethod(Type type, string methodName, Type[] argTypes)
{
var method = argTypes == null || argTypes.Length == 0
@@ -208,20 +213,80 @@ private static MethodInfo FindMatchingMethod(Type type, string methodName, Type[
#endregion
- class MethodKey : Tuple
+ abstract class MethodKey
+ {
+ public override bool Equals(object obj) =>
+ throw new NotImplementedException();
+
+ public override int GetHashCode() =>
+ throw new NotImplementedException();
+
+ public static bool operator ==(MethodKey left, MethodKey right) =>
+ object.Equals(left, right);
+
+ public static bool operator !=(MethodKey left, MethodKey right) =>
+ !(left == right);
+
+ internal static MethodKey Create(Type type, string methodName, IEnumerable parameterTypes)
+ {
+ return new HashMethodKey(type, methodName, parameterTypes);
+ }
+
+ internal static MethodKey Create(MethodInfo method)
+ {
+ return new MethodInfoKey(method);
+ }
+ }
+
+ class HashMethodKey : MethodKey, IEquatable
{
- public MethodKey(Type type, string methodName, int parametersHash)
- : base(type, methodName, parametersHash)
+ private readonly int _hash;
+
+ public HashMethodKey(Type type, string methodName, IEnumerable parameterTypes)
{
+ _hash = HashCodeCombiner.Start().Add(type).Add(methodName).Add(parameterTypes).CombinedHash;
+ }
+
+ public override bool Equals(object obj) =>
+ this.Equals(obj as HashMethodKey);
+
+ public bool Equals(HashMethodKey other)
+ {
+ if (other == null)
+ return false;
+
+ return this._hash == other._hash;
+ }
+
+ public override int GetHashCode()
+ {
+ return _hash;
+ }
+ }
+
+ class MethodInfoKey : MethodKey, IEquatable
+ {
+ private readonly MethodInfo _method;
+
+ public MethodInfoKey(MethodInfo method)
+ {
+ _method = method;
+ }
+
+ public override bool Equals(object obj) =>
+ this.Equals(obj as MethodInfoKey);
+
+ public bool Equals(MethodInfoKey other)
+ {
+ if (other == null)
+ return false;
+
+ return this._method == other._method;
}
- public Type Type { get { return base.Item1; } }
- public string MethodName { get { return base.Item2; } }
- public int ParameterHash { get { return base.Item3; } }
- public static MethodKey Create(Type type, string methodName, IEnumerable parameterTypes)
+ public override int GetHashCode()
{
- var hashCombiner = HashCodeCombiner.Start().Add(parameterTypes);
- return new MethodKey(type, methodName, hashCombiner.CombinedHash);
+ return _method.GetHashCode();
}
}
}
diff --git a/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/EnumerableConverter.cs b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/EnumerableConverter.cs
index 5687317362..ae60b58488 100644
--- a/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/EnumerableConverter.cs
+++ b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/EnumerableConverter.cs
@@ -125,7 +125,7 @@ public override object ConvertTo(CultureInfo culture, string format, object valu
if (value is IEnumerable enumerable)
{
- // we don't use string.Join() because it doesn't support invariant culture
+ // We don't use string.Join() because it doesn't support invariant culture
foreach (var token in enumerable)
{
var str = _elementTypeConverter.ConvertTo(culture, format, token, typeof(string));
diff --git a/src/Libraries/SmartStore.Core/Configuration/JsonPersistAttribute.cs b/src/Libraries/SmartStore.Core/Configuration/JsonPersistAttribute.cs
index 1e467280a9..00d96fe9ba 100644
--- a/src/Libraries/SmartStore.Core/Configuration/JsonPersistAttribute.cs
+++ b/src/Libraries/SmartStore.Core/Configuration/JsonPersistAttribute.cs
@@ -11,6 +11,7 @@ namespace SmartStore.Core.Configuration
/// into single properties.
///
[AttributeUsage(AttributeTargets.Class)]
+ [Obsolete]
public class JsonPersistAttribute : Attribute
{
}
diff --git a/src/Libraries/SmartStore.Core/Data/DbContextScope.cs b/src/Libraries/SmartStore.Core/Data/DbContextScope.cs
index 93a7552c50..7d3b433e8e 100644
--- a/src/Libraries/SmartStore.Core/Data/DbContextScope.cs
+++ b/src/Libraries/SmartStore.Core/Data/DbContextScope.cs
@@ -3,6 +3,7 @@
using System.Linq.Expressions;
using System.Collections.Generic;
using System.Linq;
+using System.Threading.Tasks;
namespace SmartStore.Core.Data
{
@@ -89,7 +90,12 @@ public int Commit()
return _ctx.SaveChanges();
}
- public void Dispose()
+ public async Task CommitAsync()
+ {
+ return await _ctx.SaveChangesAsync();
+ }
+
+ public void Dispose()
{
_ctx.AutoDetectChangesEnabled = _autoDetectChangesEnabled;
_ctx.ProxyCreationEnabled = _proxyCreationEnabled;
diff --git a/src/Libraries/SmartStore.Core/Data/Hooks/DefaultDbHookHandler.cs b/src/Libraries/SmartStore.Core/Data/Hooks/DefaultDbHookHandler.cs
index 0700430685..39126f48f8 100644
--- a/src/Libraries/SmartStore.Core/Data/Hooks/DefaultDbHookHandler.cs
+++ b/src/Libraries/SmartStore.Core/Data/Hooks/DefaultDbHookHandler.cs
@@ -8,7 +8,6 @@ namespace SmartStore.Core.Data.Hooks
{
public class DefaultDbHookHandler : IDbHookHandler
{
- private readonly IEnumerable> _hooks;
private readonly IList> _saveHooks;
private readonly Multimap _hooksRequestCache = new Multimap();
@@ -27,7 +26,6 @@ public class DefaultDbHookHandler : IDbHookHandler
public DefaultDbHookHandler(IEnumerable> hooks)
{
- _hooks = hooks;
_saveHooks = hooks.Where(x => x.Metadata.IsLoadHook == false).ToList();
}
@@ -35,7 +33,7 @@ public ILogger Logger
{
get;
set;
- }
+ } = NullLogger.Instance;
public bool HasImportantSaveHooks()
{
diff --git a/src/Libraries/SmartStore.Core/Data/RepositoryExtensions.cs b/src/Libraries/SmartStore.Core/Data/RepositoryExtensions.cs
index a3d54c9f75..5041bebcdf 100644
--- a/src/Libraries/SmartStore.Core/Data/RepositoryExtensions.cs
+++ b/src/Libraries/SmartStore.Core/Data/RepositoryExtensions.cs
@@ -4,6 +4,7 @@
using System.Linq;
using System.Linq.Expressions;
using System.Text;
+using System.Threading.Tasks;
namespace SmartStore.Core.Data
{
@@ -95,7 +96,57 @@ public static int DeleteAll(this IRepository rs, Expression>
return count;
}
- public static IQueryable Get(
+ ///
+ /// Truncates the table asynchronously.
+ ///
+ /// Entity type
+ /// The repository
+ /// An optional filter
+ ///
+ /// false : does not make any attempts to determine dependant entities, just deletes ONLY them (faster).
+ /// true : loads all entities into the context first and deletes them, along with their dependencies (slower).
+ ///
+ /// The total number of affected entities
+ ///
+ /// This method turns off auto detection, validation and hooking.
+ ///
+ [SuppressMessage("ReSharper", "UnusedVariable")]
+ public async static Task DeleteAllAsync(this IRepository rs, Expression> predicate = null, bool cascade = false) where T : BaseEntity
+ {
+ var count = 0;
+
+ using (var scope = new DbContextScope(ctx: rs.Context, autoDetectChanges: false, validateOnSave: false, hooksEnabled: false, autoCommit: false))
+ {
+ var query = rs.Table;
+ if (predicate != null)
+ {
+ query = query.Where(predicate);
+ }
+
+ if (cascade)
+ {
+ var records = query.ToList();
+ foreach (var chunk in records.Slice(500))
+ {
+ rs.DeleteRange(chunk.ToList());
+ count += await rs.Context.SaveChangesAsync();
+ }
+ }
+ else
+ {
+ var ids = query.Select(x => new { x.Id }).ToList();
+ foreach (var chunk in ids.Slice(500))
+ {
+ rs.DeleteRange(chunk.Select(x => x.Id));
+ count += await rs.Context.SaveChangesAsync();
+ }
+ }
+ }
+
+ return count;
+ }
+
+ public static IQueryable Get(
this IRepository rs,
Expression> predicate = null,
Func, IOrderedQueryable> orderBy = null,
diff --git a/src/Libraries/SmartStore.Core/Domain/Catalog/CatalogSettings.cs b/src/Libraries/SmartStore.Core/Domain/Catalog/CatalogSettings.cs
index c6d208659f..394e9e4e9e 100644
--- a/src/Libraries/SmartStore.Core/Domain/Catalog/CatalogSettings.cs
+++ b/src/Libraries/SmartStore.Core/Domain/Catalog/CatalogSettings.cs
@@ -68,7 +68,7 @@ public CatalogSettings()
ShowColorSquaresInLists = true;
ShowDiscountSign = true;
ShowVariantCombinationPriceAdjustment = true;
- ShowLinkedAttributeValueImage = true;
+ ShowLinkedAttributeValueImage = true;
EnableDynamicPriceUpdate = true;
ShowProductReviewsInProductDetail = true;
EnableHtmlTextCollapser = true;
@@ -129,10 +129,15 @@ public CatalogSettings()
///
public bool ShowVariantCombinationPriceAdjustment { get; set; }
- ///
- /// Gets or sets a value indicating whether to display quantity of linked product at attribute values
- ///
- public bool ShowLinkedAttributeValueQuantity { get; set; }
+ ///
+ /// Indicates whether to show a login note if the user is not authorized to see prices.
+ ///
+ public bool ShowLoginForPriceNote { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether to display quantity of linked product at attribute values
+ ///
+ public bool ShowLinkedAttributeValueQuantity { get; set; }
///
/// Gets or sets a value indicating whether to display the image of linked product at attribute values
diff --git a/src/Libraries/SmartStore.Core/Domain/Catalog/Category.cs b/src/Libraries/SmartStore.Core/Domain/Catalog/Category.cs
index 5a895b03a8..d74e7c0c73 100644
--- a/src/Libraries/SmartStore.Core/Domain/Catalog/Category.cs
+++ b/src/Libraries/SmartStore.Core/Domain/Catalog/Category.cs
@@ -42,6 +42,12 @@ public partial class Category : BaseEntity, ICategoryNode, IAuditable, ISoftDele
[DataMember]
public string BottomDescription { get; set; }
+ ///
+ /// Gets or sets the external link expression. If set, any category menu item will navigate to the specified link.
+ ///
+ [DataMember]
+ public string ExternalLink { get; set; }
+
///
/// Gets or sets a text displayed in a badge next to the category within menus
///
diff --git a/src/Libraries/SmartStore.Core/Domain/Catalog/CategoryNode.cs b/src/Libraries/SmartStore.Core/Domain/Catalog/CategoryNode.cs
index 14ba3736a4..c07c9fe69e 100644
--- a/src/Libraries/SmartStore.Core/Domain/Catalog/CategoryNode.cs
+++ b/src/Libraries/SmartStore.Core/Domain/Catalog/CategoryNode.cs
@@ -10,7 +10,8 @@ public interface ICategoryNode : ILocalizedEntity, ISlugSupported, IAclSupported
{
int ParentCategoryId { get; }
string Name { get; }
- string Alias { get; }
+ string ExternalLink { get; }
+ string Alias { get; }
int? PictureId { get; }
bool Published { get; }
int DisplayOrder { get; }
@@ -25,7 +26,8 @@ public class CategoryNode : ICategoryNode
public int Id { get; set; }
public int ParentCategoryId { get; set; }
public string Name { get; set; }
- public string Alias { get; set; }
+ public string ExternalLink { get; set; }
+ public string Alias { get; set; }
public int? PictureId { get; set; }
public bool Published { get; set; }
public int DisplayOrder { get; set; }
diff --git a/src/Libraries/SmartStore.Core/Domain/Catalog/ProductBundleData.cs b/src/Libraries/SmartStore.Core/Domain/Catalog/ProductBundleData.cs
index 354720c7ea..60373a2d15 100644
--- a/src/Libraries/SmartStore.Core/Domain/Catalog/ProductBundleData.cs
+++ b/src/Libraries/SmartStore.Core/Domain/Catalog/ProductBundleData.cs
@@ -1,18 +1,10 @@
-using System;
-using System.Collections.Generic;
-using System.ComponentModel;
-using System.Globalization;
-using System.IO;
-using System.Text;
-using System.Xml.Serialization;
-
-namespace SmartStore.Core.Domain.Catalog
+namespace SmartStore.Core.Domain.Catalog
{
- public partial class ProductBundleItemData
+ public partial class ProductBundleItemData
{
public ProductBundleItemData(ProductBundleItem item)
{
- Item = item; // can be null... test with IsValid
+ Item = item; // Can be null.
}
public ProductBundleItem Item { get; private set; }
diff --git a/src/Libraries/SmartStore.Core/Domain/Catalog/ProductCategory.cs b/src/Libraries/SmartStore.Core/Domain/Catalog/ProductCategory.cs
index 5cb9acaa5f..ee5adfe594 100644
--- a/src/Libraries/SmartStore.Core/Domain/Catalog/ProductCategory.cs
+++ b/src/Libraries/SmartStore.Core/Domain/Catalog/ProductCategory.cs
@@ -33,15 +33,17 @@ public partial class ProductCategory : BaseEntity
///
[DataMember]
public int DisplayOrder { get; set; }
-
+
///
/// Gets the category
///
+ [DataMember]
public virtual Category Category { get; set; }
///
/// Gets the product
///
+ [DataMember]
public virtual Product Product { get; set; }
}
diff --git a/src/Libraries/SmartStore.Core/Domain/Catalog/ProductManufacturer.cs b/src/Libraries/SmartStore.Core/Domain/Catalog/ProductManufacturer.cs
index 087a1f9351..73edaec790 100644
--- a/src/Libraries/SmartStore.Core/Domain/Catalog/ProductManufacturer.cs
+++ b/src/Libraries/SmartStore.Core/Domain/Catalog/ProductManufacturer.cs
@@ -37,11 +37,13 @@ public partial class ProductManufacturer : BaseEntity
///
/// Gets or sets the manufacturer
///
+ [DataMember]
public virtual Manufacturer Manufacturer { get; set; }
///
/// Gets or sets the product
///
+ [DataMember]
public virtual Product Product { get; set; }
}
diff --git a/src/Libraries/SmartStore.Core/Domain/Catalog/ProductSpecificationAttribute.cs b/src/Libraries/SmartStore.Core/Domain/Catalog/ProductSpecificationAttribute.cs
index 132e767131..eb8595b3af 100644
--- a/src/Libraries/SmartStore.Core/Domain/Catalog/ProductSpecificationAttribute.cs
+++ b/src/Libraries/SmartStore.Core/Domain/Catalog/ProductSpecificationAttribute.cs
@@ -46,6 +46,7 @@ public partial class ProductSpecificationAttribute : BaseEntity
///
/// Gets or sets the specification attribute option
///
+ [DataMember]
public virtual SpecificationAttributeOption SpecificationAttributeOption { get; set; }
}
}
diff --git a/src/Libraries/SmartStore.Core/Domain/Cms/MenuItemRecord.cs b/src/Libraries/SmartStore.Core/Domain/Cms/MenuItemRecord.cs
new file mode 100644
index 0000000000..9aab96cc35
--- /dev/null
+++ b/src/Libraries/SmartStore.Core/Domain/Cms/MenuItemRecord.cs
@@ -0,0 +1,117 @@
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using SmartStore.Core.Domain.Localization;
+
+namespace SmartStore.Core.Domain.Cms
+{
+ ///
+ /// Represents a menu item.
+ ///
+ [Table("MenuItemRecord")]
+ public class MenuItemRecord : BaseEntity, ILocalizedEntity
+ {
+ ///
+ /// Gets or sets the menu identifier.
+ ///
+ [Required]
+ public int MenuId { get; set; }
+
+ ///
+ /// Gets the menu.
+ ///
+ [ForeignKey("MenuId")]
+ public virtual MenuRecord Menu { get; set; }
+
+ ///
+ /// Gets or sets the parent menu item identifier. 0 if the item has no parent.
+ ///
+ [Index("IX_MenuItem_ParentItemId")]
+ public int ParentItemId { get; set; }
+
+ ///
+ /// Gets or sets the provider name.
+ ///
+ [StringLength(100)]
+ public string ProviderName { get; set; }
+
+ ///
+ /// Gets or sets the model.
+ ///
+ [MaxLength]
+ public string Model { get; set; }
+
+ ///
+ /// Gets or sets the title.
+ ///
+ [StringLength(400)]
+ public string Title { get; set; }
+
+ ///
+ /// Gets or sets the short description. It is used for the link title attribute.
+ ///
+ [StringLength(400)]
+ public string ShortDescription { get; set; }
+
+ ///
+ /// Gets or sets permission names.
+ ///
+ [MaxLength]
+ public string PermissionNames { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether the menu item is published.
+ ///
+ [Index("IX_MenuItem_Published")]
+ public bool Published { get; set; } = true;
+
+ ///
+ /// Gets or sets the display order.
+ ///
+ [Index("IX_MenuItem_DisplayOrder")]
+ public int DisplayOrder { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether the menu item has a divider or a group header.
+ ///
+ public bool BeginGroup { get; set; }
+
+ ///
+ /// If selected and this menu item has children, the menu will initially appear expanded.
+ ///
+ public bool ShowExpanded { get; set; }
+
+ ///
+ /// Gets or sets the no-follow link attribute.
+ ///
+ public bool NoFollow { get; set; }
+
+ ///
+ /// Gets or sets the blank target link attribute.
+ ///
+ public bool NewWindow { get; set; }
+
+ ///
+ /// Gets or sets fontawesome icon class.
+ ///
+ [StringLength(100)]
+ public string Icon { get; set; }
+
+ ///
+ /// Gets or sets fontawesome icon style.
+ ///
+ [StringLength(10)]
+ public string Style { get; set; }
+
+ ///
+ /// Gets or sets HTML id attribute.
+ ///
+ [StringLength(100)]
+ public string HtmlId { get; set; }
+
+ ///
+ /// Gets or sets the CSS class.
+ ///
+ [StringLength(100)]
+ public string CssClass { get; set; }
+ }
+}
diff --git a/src/Libraries/SmartStore.Core/Domain/Cms/MenuRecord.cs b/src/Libraries/SmartStore.Core/Domain/Cms/MenuRecord.cs
new file mode 100644
index 0000000000..c8ebedf1c1
--- /dev/null
+++ b/src/Libraries/SmartStore.Core/Domain/Cms/MenuRecord.cs
@@ -0,0 +1,96 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using System.Linq;
+using SmartStore.Core.Domain.Localization;
+using SmartStore.Core.Domain.Security;
+using SmartStore.Core.Domain.Stores;
+
+namespace SmartStore.Core.Domain.Cms
+{
+ ///
+ /// Represents a menu.
+ ///
+ [Table("MenuRecord")]
+ public class MenuRecord : BaseEntity, ILocalizedEntity, IStoreMappingSupported, IAclSupported
+ {
+ private ICollection _items;
+
+ ///
+ /// /// Gets or sets the menu items.
+ ///
+ public virtual ICollection Items
+ {
+ get { return _items ?? (_items = new HashSet()); }
+ protected set { _items = value; }
+ }
+
+ ///
+ /// Gets or sets the system name. It identifies the menu.
+ ///
+ [Required, StringLength(400), Index("IX_Menu_SystemName_IsSystemMenu", Order = 0)]
+ public string SystemName { get; set; }
+
+ ///
+ /// Gets or sets the value indicating whether this menu is deleteable by a user.
+ ///
+ [Index("IX_Menu_SystemName_IsSystemMenu", Order = 1)]
+ public bool IsSystemMenu { get; set; }
+
+ ///
+ /// Gets or sets the menu template name.
+ ///
+ [StringLength(400)]
+ public string Template { get; set; }
+
+ ///
+ /// Gets or sets the widget zone name.
+ ///
+ [StringLength(4000)]
+ public string WidgetZone { get; set; }
+
+ ///
+ /// Gets or sets the title.
+ ///
+ [StringLength(400)]
+ public string Title { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether the menu is published.
+ ///
+ [Index("IX_Menu_Published")]
+ public bool Published { get; set; } = true;
+
+ ///
+ /// Gets or sets the order for widget registration.
+ ///
+ public int DisplayOrder { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether the entity is limited/restricted to certain stores.
+ ///
+ [Index("IX_Menu_LimitedToStores")]
+ public bool LimitedToStores { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether the entity is subject to ACL.
+ ///
+ [Index("IX_Menu_SubjectToAcl")]
+ public bool SubjectToAcl { get; set; }
+
+ ///
+ /// Helper function to get the comma-separated WidgetZone property as an enumerable of strings.
+ ///
+ /// Widget zones.
+ public IEnumerable GetWidgetZones()
+ {
+ if (WidgetZone.IsEmpty())
+ {
+ return Enumerable.Empty();
+ }
+
+ return WidgetZone.Trim().Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim());
+ }
+ }
+}
diff --git a/src/Libraries/SmartStore.Core/Domain/Common/CommonSettings.cs b/src/Libraries/SmartStore.Core/Domain/Common/CommonSettings.cs
index 46d94f8c98..523a5dbe52 100644
--- a/src/Libraries/SmartStore.Core/Domain/Common/CommonSettings.cs
+++ b/src/Libraries/SmartStore.Core/Domain/Common/CommonSettings.cs
@@ -11,6 +11,7 @@ public CommonSettings()
EntityPickerPageSize = 48;
MaxScheduleHistoryAgeInDays = 30;
MaxNumberOfScheduleHistoryEntries = 100;
+ MaxQueuedMessagesAgeInDays = 14;
}
public bool UseSystemEmailForContactUsForm { get; set; }
@@ -40,5 +41,10 @@ public CommonSettings()
/// Gets or sets the maximum number of schedule history entries per task.
///
public int MaxNumberOfScheduleHistoryEntries { get; set; }
+
+ ///
+ /// Gets or sets the maximum age of sent queued messages (in days).
+ ///
+ public int MaxQueuedMessagesAgeInDays { get; set; }
}
}
\ No newline at end of file
diff --git a/src/Libraries/SmartStore.Core/Domain/Common/SocialSettings.cs b/src/Libraries/SmartStore.Core/Domain/Common/SocialSettings.cs
index 46980652d0..1a45382602 100644
--- a/src/Libraries/SmartStore.Core/Domain/Common/SocialSettings.cs
+++ b/src/Libraries/SmartStore.Core/Domain/Common/SocialSettings.cs
@@ -4,50 +4,34 @@ namespace SmartStore.Core.Domain.Common
{
public class SocialSettings : ISettings
{
- public SocialSettings()
- {
- ShowSocialLinksInFooter = true;
- FacebookLink = "#";
- GooglePlusLink = "#";
- TwitterLink = "#";
- PinterestLink = "#";
- //YoutubeLink = "#";
- //InstagramLink = "#";
- }
-
- ///
+ ///
/// Gets or sets the value that determines whether social links should be show in the footer
///
- public bool ShowSocialLinksInFooter { get; set; }
+ public bool ShowSocialLinksInFooter { get; set; } = true;
///
/// Gets or sets the facebook link
///
- public string FacebookLink { get; set; }
-
- ///
- /// Gets or sets the google plus link
- ///
- public string GooglePlusLink { get; set; }
+ public string FacebookLink { get; set; } = "#";
///
/// Gets or sets the twitter link
///
- public string TwitterLink { get; set; }
+ public string TwitterLink { get; set; } = "#";
///
/// Gets or sets the pinterest link
///
- public string PinterestLink { get; set; }
+ public string PinterestLink { get; set; } = "#";
///
/// Gets or sets the youtube link
///
- public string YoutubeLink { get; set; }
+ public string YoutubeLink { get; set; } = "#";
- ///
- /// Gets or sets the instagram link
- ///
- public string InstagramLink { get; set; }
- }
+ ///
+ /// Gets or sets the instagram link
+ ///
+ public string InstagramLink { get; set; } = "#";
+ }
}
\ No newline at end of file
diff --git a/src/Libraries/SmartStore.Core/Domain/Customers/CustomerLoginType.cs b/src/Libraries/SmartStore.Core/Domain/Customers/CustomerLoginType.cs
new file mode 100644
index 0000000000..a9c9479506
--- /dev/null
+++ b/src/Libraries/SmartStore.Core/Domain/Customers/CustomerLoginType.cs
@@ -0,0 +1,23 @@
+namespace SmartStore.Core.Domain.Customers
+{
+ ///
+ /// Represents the customer login type
+ ///
+ public enum CustomerLoginType
+ {
+ ///
+ /// The username will be used to login
+ ///
+ Username = 10,
+
+ ///
+ /// The email will be used to login
+ ///
+ Email = 20,
+
+ ///
+ /// The username or the email address can be used to login
+ ///
+ UsernameOrEmail = 30
+ }
+}
diff --git a/src/Libraries/SmartStore.Core/Domain/Customers/CustomerSettings.cs b/src/Libraries/SmartStore.Core/Domain/Customers/CustomerSettings.cs
index 7c51a9ced5..20033f0c21 100644
--- a/src/Libraries/SmartStore.Core/Domain/Customers/CustomerSettings.cs
+++ b/src/Libraries/SmartStore.Core/Domain/Customers/CustomerSettings.cs
@@ -6,7 +6,7 @@ public class CustomerSettings : ISettings
{
public CustomerSettings()
{
- UsernamesEnabled = true;
+ CustomerLoginType = CustomerLoginType.UsernameOrEmail;
CustomerNumberMethod = CustomerNumberMethod.Disabled;
CustomerNumberVisibility = CustomerNumberVisibility.None;
DefaultPasswordFormat = PasswordFormat.Hashed;
@@ -25,11 +25,11 @@ public CustomerSettings()
FirstNameRequired = false;
LastNameRequired = false;
}
-
- ///
- /// Gets or sets a value indicating whether usernames are used instead of emails
+
+ ///
+ /// Gets or sets a value indicating the customer login type
///
- public bool UsernamesEnabled { get; set; }
+ public CustomerLoginType CustomerLoginType { get; set; }
///
/// Gets or sets the customer number method
@@ -255,9 +255,6 @@ public CustomerSettings()
#endregion
- public string PrefillLoginUsername { get; set; }
- public string PrefillLoginPwd { get; set; }
-
///
/// Identifier of a customer role that new registered customers will be assigned to
///
diff --git a/src/Libraries/SmartStore.Core/Domain/Directory/QuantityUnit.cs b/src/Libraries/SmartStore.Core/Domain/Directory/QuantityUnit.cs
index f10f0543c3..4e671a5384 100644
--- a/src/Libraries/SmartStore.Core/Domain/Directory/QuantityUnit.cs
+++ b/src/Libraries/SmartStore.Core/Domain/Directory/QuantityUnit.cs
@@ -1,6 +1,5 @@
-using System;
-using SmartStore.Core.Domain.Localization;
using System.Runtime.Serialization;
+using SmartStore.Core.Domain.Localization;
namespace SmartStore.Core.Domain.Directory
{
@@ -16,6 +15,12 @@ public partial class QuantityUnit : BaseEntity, ILocalizedEntity
[DataMember]
public string Name { get; set; }
+ ///
+ /// Gets or sets the name plural.
+ ///
+ [DataMember]
+ public string NamePlural { get; set; }
+
///
/// Gets or sets the description
///
@@ -38,7 +43,6 @@ public partial class QuantityUnit : BaseEntity, ILocalizedEntity
/// Gets or sets the default quantity unit
///
[DataMember]
- public bool IsDefault { get; set; }
-
+ public bool IsDefault { get; set; }
}
}
diff --git a/src/Libraries/SmartStore.Core/Domain/Seo/SeoSettings.cs b/src/Libraries/SmartStore.Core/Domain/Seo/SeoSettings.cs
index 7160bcf30c..9d1d5f5bce 100644
--- a/src/Libraries/SmartStore.Core/Domain/Seo/SeoSettings.cs
+++ b/src/Libraries/SmartStore.Core/Domain/Seo/SeoSettings.cs
@@ -81,7 +81,7 @@ public SeoSettings()
public string SeoNameCharConversion { get; set; }
public bool CanonicalUrlsEnabled { get; set; }
- public CanonicalHostNameRule CanonicalHostNameRule { get; set; }
+ public CanonicalHostNameRule CanonicalHostNameRule { get; set; }
///
/// Slugs (sename) reserved for some other needs
diff --git a/src/Libraries/SmartStore.Core/Domain/Stores/Store.cs b/src/Libraries/SmartStore.Core/Domain/Stores/Store.cs
index 0fb734efbd..d9325aa60a 100644
--- a/src/Libraries/SmartStore.Core/Domain/Stores/Store.cs
+++ b/src/Libraries/SmartStore.Core/Domain/Stores/Store.cs
@@ -97,7 +97,7 @@ public partial class Store : BaseEntity
///
/// Gets the security mode for the store
///
- public HttpSecurityMode GetSecurityMode(bool? useSsl = null)
+ public virtual HttpSecurityMode GetSecurityMode(bool? useSsl = null)
{
if (useSsl ?? SslEnabled)
{
diff --git a/src/Libraries/SmartStore.Core/Domain/Stores/StoreExtensions.cs b/src/Libraries/SmartStore.Core/Domain/Stores/StoreExtensions.cs
index 3273ab21da..1185f4b399 100644
--- a/src/Libraries/SmartStore.Core/Domain/Stores/StoreExtensions.cs
+++ b/src/Libraries/SmartStore.Core/Domain/Stores/StoreExtensions.cs
@@ -7,42 +7,44 @@ namespace SmartStore.Core.Domain.Stores
public static class StoreExtensions
{
///
- /// Parse comma-separated Hosts
+ /// Indicates whether a store contains a specified host
///
/// Store
- /// Comma-separated hosts
- public static string[] ParseHostValues(this Store store)
+ /// Host
+ /// true - contains, false - no
+ public static bool ContainsHostValue(this Store store, string host)
{
- if (store == null)
- throw new ArgumentNullException("store");
+ Guard.NotNull(store, nameof(store));
- var parsedValues = new List();
- if (!string.IsNullOrEmpty(store.Hosts))
+ if (string.IsNullOrEmpty(host))
{
- var hosts = store.Hosts.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
- parsedValues.AddRange(hosts.Select(host => host.Trim()).Where(tmp => !string.IsNullOrEmpty(tmp)));
- }
+ return false;
+ }
- return parsedValues.ToArray();
+ var contains = store.ParseHostValues()
+ .FirstOrDefault(x => x.Equals(host, StringComparison.InvariantCultureIgnoreCase)) != null;
+ return contains;
}
///
- /// Indicates whether a store contains a specified host
+ /// Parse comma-separated hosts
///
/// Store
- /// Host
- /// true - contains, false - no
- public static bool ContainsHostValue(this Store store, string host)
+ /// Comma-separated hosts
+ public static string[] ParseHostValues(this Store store)
{
- if (store == null)
- throw new ArgumentNullException("store");
+ Guard.NotNull(store, nameof(store));
- if (String.IsNullOrEmpty(host))
- return false;
+ if (string.IsNullOrWhiteSpace(store.Hosts))
+ {
+ return Array.Empty();
+ }
- var contains = store.ParseHostValues()
- .FirstOrDefault(x => x.Equals(host, StringComparison.InvariantCultureIgnoreCase)) != null;
- return contains;
+ return store.Hosts
+ .Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
+ .Select(host => host.Trim())
+ .Where(host => !string.IsNullOrWhiteSpace(host))
+ .ToArray();
}
}
}
diff --git a/src/Libraries/SmartStore.Core/Domain/Tasks/ScheduleTask.cs b/src/Libraries/SmartStore.Core/Domain/Tasks/ScheduleTask.cs
index c6d22dd2e0..f516fc53da 100644
--- a/src/Libraries/SmartStore.Core/Domain/Tasks/ScheduleTask.cs
+++ b/src/Libraries/SmartStore.Core/Domain/Tasks/ScheduleTask.cs
@@ -88,12 +88,25 @@ protected set
public ScheduleTask Clone()
{
- return (ScheduleTask)this.MemberwiseClone();
+ var task = new ScheduleTask
+ {
+ Name = Name,
+ Alias = Alias,
+ CronExpression = CronExpression,
+ Type = Type,
+ Enabled = Enabled,
+ StopOnError = StopOnError,
+ NextRunUtc = NextRunUtc,
+ IsHidden = IsHidden,
+ RunPerMachine = RunPerMachine
+ };
+
+ return task;
}
object ICloneable.Clone()
{
- return this.MemberwiseClone();
+ return this.Clone();
}
}
}
diff --git a/src/Libraries/SmartStore.Core/Domain/Tax/TaxSettings.cs b/src/Libraries/SmartStore.Core/Domain/Tax/TaxSettings.cs
index 1d79ba018b..28fd36b269 100644
--- a/src/Libraries/SmartStore.Core/Domain/Tax/TaxSettings.cs
+++ b/src/Libraries/SmartStore.Core/Domain/Tax/TaxSettings.cs
@@ -12,7 +12,8 @@ public TaxSettings()
DisplayTaxSuffix = true;
PricesIncludeTax = true;
EuVatAllowVatExemption = true;
- ShowLegalHintsInFooter = true;
+ ShippingPriceIncludesTax = true;
+ ShowLegalHintsInFooter = true;
ShowLegalHintsInProductDetails = true;
ShowLegalHintsInProductList = true;
AuxiliaryServicesTaxingType = AuxiliaryServicesTaxType.SpecifiedTaxCategory;
diff --git a/src/Libraries/SmartStore.Core/Domain/Topics/Topic.cs b/src/Libraries/SmartStore.Core/Domain/Topics/Topic.cs
index 36a6d95ac3..d05d74c975 100644
--- a/src/Libraries/SmartStore.Core/Domain/Topics/Topic.cs
+++ b/src/Libraries/SmartStore.Core/Domain/Topics/Topic.cs
@@ -32,10 +32,22 @@ public Topic()
[DataMember]
public bool IsSystemTopic { get; set; }
- ///
- /// Gets or sets the value indicating whether this topic should be included in sitemap
- ///
- [DataMember]
+ ///
+ /// Gets or sets the html id
+ ///
+ [DataMember]
+ public string HtmlId { get; set; }
+
+ ///
+ /// Gets or sets the body css class
+ ///
+ [DataMember]
+ public string BodyCssClass { get; set; }
+
+ ///
+ /// Gets or sets the value indicating whether this topic should be included in sitemap
+ ///
+ [DataMember]
public bool IncludeInSitemap { get; set; }
///
diff --git a/src/Libraries/SmartStore.Core/Email/DefaultEmailSender.cs b/src/Libraries/SmartStore.Core/Email/DefaultEmailSender.cs
index 15fc63095f..5492f4631f 100644
--- a/src/Libraries/SmartStore.Core/Email/DefaultEmailSender.cs
+++ b/src/Libraries/SmartStore.Core/Email/DefaultEmailSender.cs
@@ -83,20 +83,19 @@ public void SendEmail(SmtpContext context, EmailMessage message)
}
}
- public Task SendEmailAsync(SmtpContext context, EmailMessage message)
+ public async Task SendEmailAsync(SmtpContext context, EmailMessage message)
{
Guard.NotNull(context, nameof(context));
Guard.NotNull(message, nameof(message));
- var client = context.ToSmtpClient();
- ApplySettings(client);
- var msg = this.BuildMailMessage(message);
-
- return client.SendMailAsync(msg).ContinueWith(t =>
+ using (var msg = this.BuildMailMessage(message))
{
- client.Dispose();
- msg.Dispose();
- });
+ using (var client = context.ToSmtpClient())
+ {
+ ApplySettings(client);
+ await client.SendMailAsync(msg);
+ }
+ }
}
private void ApplySettings(SmtpClient client)
diff --git a/src/Libraries/SmartStore.Core/Events/AmbigousConsumerException.cs b/src/Libraries/SmartStore.Core/Events/AmbigousConsumerException.cs
new file mode 100644
index 0000000000..18b72d94da
--- /dev/null
+++ b/src/Libraries/SmartStore.Core/Events/AmbigousConsumerException.cs
@@ -0,0 +1,13 @@
+using System;
+using System.Reflection;
+
+namespace SmartStore.Core.Events
+{
+ public sealed class AmbigousConsumerException : Exception
+ {
+ public AmbigousConsumerException(MethodInfo method1, MethodInfo method2)
+ : base("Ambigous consumer methods detected in '{0}'. Method 1: '{1}', Method 2: '{2}'.".FormatInvariant(method1.DeclaringType.FullName, method1.ToString(), method2.ToString()))
+ {
+ }
+ }
+}
diff --git a/src/Libraries/SmartStore.Core/Events/AsyncConsumerAttribute.cs b/src/Libraries/SmartStore.Core/Events/AsyncConsumerAttribute.cs
deleted file mode 100644
index 4a33b18f31..0000000000
--- a/src/Libraries/SmartStore.Core/Events/AsyncConsumerAttribute.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-using System;
-
-namespace SmartStore.Core.Events
-{
- [AttributeUsage(AttributeTargets.Class, Inherited=true, AllowMultiple=false)]
- public class AsyncConsumerAttribute : Attribute
- {
- }
-}
diff --git a/src/Libraries/SmartStore.Core/Events/ConsumeContext.cs b/src/Libraries/SmartStore.Core/Events/ConsumeContext.cs
new file mode 100644
index 0000000000..d52607845a
--- /dev/null
+++ b/src/Libraries/SmartStore.Core/Events/ConsumeContext.cs
@@ -0,0 +1,23 @@
+using System;
+
+namespace SmartStore.Core.Events
+{
+ ///
+ /// Wrapper/Envelope for event message objects. For now, only wraps
+ /// the message. There may be other context data in future.
+ ///
+ /// Type of message.
+ public class ConsumeContext
+ {
+ public ConsumeContext(TMessage message)
+ {
+ Message = message;
+ }
+
+ public TMessage Message
+ {
+ get;
+ private set;
+ }
+ }
+}
diff --git a/src/Libraries/SmartStore.Core/Events/ConsumerDescriptor.cs b/src/Libraries/SmartStore.Core/Events/ConsumerDescriptor.cs
new file mode 100644
index 0000000000..f990c1e61c
--- /dev/null
+++ b/src/Libraries/SmartStore.Core/Events/ConsumerDescriptor.cs
@@ -0,0 +1,50 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using System.Threading.Tasks;
+using SmartStore.ComponentModel;
+using SmartStore.Core.Plugins;
+
+namespace SmartStore.Core.Events
+{
+ ///
+ /// Contains metadata about a consumer / event handler method.
+ ///
+ public class ConsumerDescriptor
+ {
+ public ConsumerDescriptor()
+ {
+ }
+
+ public ConsumerDescriptor(EventConsumerMetadata metadata)
+ {
+ ContainerType = metadata.ContainerType;
+ PluginDescriptor = metadata.PluginDescriptor;
+ }
+
+ public bool WithEnvelope { get; set; }
+ public bool IsAsync { get; set; }
+ public bool FireForget { get; set; }
+ public PluginDescriptor PluginDescriptor { get; set; }
+
+ public Type MessageType { get; set; }
+ public Type ContainerType { get; set; }
+
+ public MethodInfo Method { get; set; }
+ public ParameterInfo MessageParameter { get; set; }
+
+ ///
+ /// All method parameters except
+ /// or empty array if is the only parameter.
+ ///
+ public ParameterInfo[] Parameters { get; set; }
+
+ public override string ToString()
+ {
+ return "MessageType: {0} - Consumer: {1}.{2}".FormatCurrent(MessageType.Name, ContainerType.FullName, Method.Name);
+ }
+ }
+}
diff --git a/src/Libraries/SmartStore.Core/Events/ConsumerInvoker.cs b/src/Libraries/SmartStore.Core/Events/ConsumerInvoker.cs
new file mode 100644
index 0000000000..9eb50ecb00
--- /dev/null
+++ b/src/Libraries/SmartStore.Core/Events/ConsumerInvoker.cs
@@ -0,0 +1,193 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Web;
+using Autofac;
+using SmartStore.ComponentModel;
+using SmartStore.Core.Async;
+using SmartStore.Core.Logging;
+
+namespace SmartStore.Core.Events
+{
+ public class ConsumerInvoker : IConsumerInvoker
+ {
+ private readonly IConsumerResolver _resolver;
+
+ public ConsumerInvoker(IConsumerResolver resolver)
+ {
+ _resolver = resolver;
+
+ Logger = NullLogger.Instance;
+ }
+
+ public ILogger Logger { get; set; }
+
+ public void Invoke(ConsumerDescriptor descriptor, IConsumer consumer, ConsumeContext envelope) where TMessage : class
+ {
+ var d = descriptor;
+ var p = descriptor.WithEnvelope ? (object)envelope : envelope.Message;
+ var fastInvoker = FastInvoker.GetInvoker(d.Method);
+
+ if (!d.FireForget && !d.IsAsync)
+ {
+ // The all synch case
+ try
+ {
+ InvokeCore();
+ }
+ catch (Exception ex)
+ {
+ HandleException(ex, d);
+ }
+ }
+ else if (!d.FireForget && d.IsAsync)
+ {
+ //// The awaitable Task case
+ //BeginInvoke((Task)InvokeCore(cancelToken: AsyncRunner.AppShutdownCancellationToken), EndInvoke, d);
+
+ // For now we must go with the AsyncRunner, the above call to BeginInvoke (APM > TPL) does not always
+ // guarantee that the task is awaited and throws exceptions especially when EF is involved.
+ using (var runner = AsyncRunner.Create())
+ {
+ try
+ {
+ runner.Run((Task)InvokeCore(cancelToken: AsyncRunner.AppShutdownCancellationToken));
+ }
+ catch (Exception ex)
+ {
+ HandleException(ex, d);
+ }
+ }
+ }
+ else if (d.FireForget && !d.IsAsync)
+ {
+ // A synch method should be executed async (without awaiting)
+ AsyncRunner.Run((c, ct, state) => InvokeCore(c, ct), d)
+ .ContinueWith(t => EndInvoke(t), /*TaskContinuationOptions.OnlyOnFaulted*/ TaskContinuationOptions.None)
+ .ConfigureAwait(false);
+ }
+ else if (d.FireForget && d.IsAsync)
+ {
+ // An async (Task) method should be executed without awaiting
+ AsyncRunner.Run((c, ct) => (Task)InvokeCore(c, ct), d)
+ .ContinueWith(t => EndInvoke(t), TaskContinuationOptions.OnlyOnFaulted)
+ .ConfigureAwait(false);
+ }
+
+ object InvokeCore(IComponentContext c = null, CancellationToken cancelToken = default(CancellationToken))
+ {
+ if (d.Parameters.Length == 0)
+ {
+ // Only one method param: the message!
+ return fastInvoker.Invoke(consumer, p);
+ }
+
+ var parameters = new object[d.Parameters.Length + 1];
+ parameters[0] = p;
+
+ int i = 0;
+ foreach (var obj in ResolveParameters(c, d, cancelToken).ToArray())
+ {
+ i++;
+ parameters[i] = obj;
+ }
+
+ return fastInvoker.Invoke(consumer, parameters);
+ }
+ }
+
+ private void HandleException(Exception ex, ConsumerDescriptor descriptor)
+ {
+ if (ex is AggregateException ae)
+ {
+ ae.InnerExceptions.Each(x => Logger.Error(x));
+ }
+ else
+ {
+ Logger.Error(ex);
+ }
+
+ if (!descriptor.FireForget)
+ {
+ throw ex;
+ }
+ }
+
+ protected internal virtual IEnumerable ResolveParameters(
+ IComponentContext container,
+ ConsumerDescriptor descriptor,
+ CancellationToken cancelToken)
+ {
+ foreach (var p in descriptor.Parameters)
+ {
+ if (p.ParameterType == typeof(CancellationToken))
+ {
+ yield return cancelToken;
+ }
+ else
+ {
+ yield return _resolver.ResolveParameter(p, container);
+ }
+ }
+ }
+
+ #region APM > TPL pattern (does not always work stable)
+
+ ///
+ /// Wraps a into the Begin method of an APM pattern.
+ ///
+ /// The task to wrap.
+ /// The callback method passed into the Begin method of the APM pattern.
+ /// The state passed into the Begin method of the APM pattern.
+ /// The asynchronous operation, to be returned by the Begin method of the APM pattern.
+ protected IAsyncResult BeginInvoke(Task task, AsyncCallback callback, ConsumerDescriptor descriptor)
+ {
+ var options = TaskCreationOptions.RunContinuationsAsynchronously;
+ var tcs = new TaskCompletionSource(descriptor, options);
+
+ // "_ =" to discard 'async/await' compiler warning
+ _ = AwaitCompletionAsync(task, callback, tcs, descriptor);
+
+ return tcs.Task;
+ }
+
+ private async Task AwaitCompletionAsync(
+ Task task,
+ AsyncCallback callback,
+ TaskCompletionSource tcs,
+ ConsumerDescriptor descriptor)
+ {
+ try
+ {
+ await task;
+ tcs.TrySetResult(null);
+ }
+ catch (OperationCanceledException ex)
+ {
+ tcs.TrySetCanceled(ex.CancellationToken);
+ }
+ catch (Exception ex)
+ {
+ tcs.TrySetException(ex);
+ }
+ finally
+ {
+ callback?.Invoke(tcs.Task);
+ }
+ }
+
+ protected virtual void EndInvoke(IAsyncResult asyncResult)
+ {
+ var task = (Task)asyncResult;
+
+ if (task.IsFaulted && task.Exception != null)
+ {
+ HandleException(task.Exception, (ConsumerDescriptor)asyncResult.AsyncState);
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/src/Libraries/SmartStore.Core/Events/EventConsumerMetadata.cs b/src/Libraries/SmartStore.Core/Events/EventConsumerMetadata.cs
index 1ae4f063fb..0c233e9da1 100644
--- a/src/Libraries/SmartStore.Core/Events/EventConsumerMetadata.cs
+++ b/src/Libraries/SmartStore.Core/Events/EventConsumerMetadata.cs
@@ -5,8 +5,8 @@ namespace SmartStore.Core.Events
{
public class EventConsumerMetadata
{
- public bool ExecuteAsync { get; set; }
public bool IsActive { get; set; }
+ public Type ContainerType { get; set; }
public PluginDescriptor PluginDescriptor { get; set; }
}
}
diff --git a/src/Libraries/SmartStore.Core/Events/FireForgetAttribute.cs b/src/Libraries/SmartStore.Core/Events/FireForgetAttribute.cs
new file mode 100644
index 0000000000..a5076473c9
--- /dev/null
+++ b/src/Libraries/SmartStore.Core/Events/FireForgetAttribute.cs
@@ -0,0 +1,20 @@
+using System;
+
+namespace SmartStore.Core.Events
+{
+ ///
+ /// Specifies whether an event consumer method should be invoked asynchronously without awaiting.
+ /// This can be advantegous in long running processes, because the current request thread will not 'block'
+ /// until method completion.
+ ///
+ ///
+ /// Use this with caution and only if you know what you are doing :-)
+ /// A class containing a fire & forget consumer should NOT take dependencies on request scoped services, because task continuation
+ /// happens on another thread and context gets lost. Pass required dependencies as method parameters instead, the consumer invoker
+ /// will spawn a new private context for the unit of work and resolve dependencies from this context.
+ ///
+ [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
+ public sealed class FireForgetAttribute : Attribute
+ {
+ }
+}
diff --git a/src/Libraries/SmartStore.Core/Events/IConsumer.cs b/src/Libraries/SmartStore.Core/Events/IConsumer.cs
index 5fee38c56e..da2ff66d89 100644
--- a/src/Libraries/SmartStore.Core/Events/IConsumer.cs
+++ b/src/Libraries/SmartStore.Core/Events/IConsumer.cs
@@ -1,8 +1,45 @@
-
+using System;
+
namespace SmartStore.Core.Events
{
- public interface IConsumer
- {
- void HandleEvent(T message);
- }
+ ///
+ /// Marker interface for classes that contain one or more
+ /// event message handlers. A handler is a public instance method following some conventions.
+ /// The name of the handler method must be one of:
+ ///
+ /// - void Handle(TMessage msg)
+ /// - void HandleEvent(TMessage msg)
+ /// - void Consume(TMessage msg)
+ /// - Task HandleAsync(TMessage msg)
+ /// - Task HandleEventAsync(TMessage msg)
+ /// - Task ConsumeAsync(TMessage msg)
+ ///
+ /// Alternatively, the TMessage param can be the generic type.
+ ///
+ /// The decides how to call the method based on its signature:
+ ///
+ /// void method are invoked synchronously.
+ /// methods are invoked asynchronously and awaited.
+ /// - The
makes the method run in the background without awaiting.
+ ///
+ ///
+ ///
+ /// You can declare additional dependency parameters in the handler method, e.g.:
+ ///
+ /// Task HandleEventAsync(SomeEvent message, IDbContext dbContext, ICacheManager cache, CancellationToken cancelToken)
+ ///
+ /// Order does not matter. The invoker will auto-resolve appropriate instances and pass them to the method. But any unregistered
+ /// dependency or a primitive type will result in an exception (except of which always resolves to
+ /// ).
+ ///
+ ///
+ public interface IConsumer
+ {
+ }
+
+ [Obsolete("Implement the non-generic 'IConsumer' interface instead.")]
+ public interface IConsumer : IConsumer
+ {
+ void HandleEvent(T message);
+ }
}
diff --git a/src/Libraries/SmartStore.Core/Events/IConsumerFactory.cs b/src/Libraries/SmartStore.Core/Events/IConsumerFactory.cs
deleted file mode 100644
index a55c9fce79..0000000000
--- a/src/Libraries/SmartStore.Core/Events/IConsumerFactory.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-using System.Collections.Generic;
-
-namespace SmartStore.Core.Events
-{
-
- public interface IConsumerFactory
- {
- ///
- /// Resolves all event consumers of type
- ///
- ///
- /// Specifies whether only async consumers, sync consumers or both should be resolved
- /// (null = all, true = only async, false = only sync)
- ///
- /// The consumer instances
- IEnumerable> GetConsumers(bool? resolveAsyncs = null);
-
- bool HasAsyncConsumer { get; }
- }
-
-}
diff --git a/src/Libraries/SmartStore.Core/Events/IConsumerInvoker.cs b/src/Libraries/SmartStore.Core/Events/IConsumerInvoker.cs
new file mode 100644
index 0000000000..85036e7528
--- /dev/null
+++ b/src/Libraries/SmartStore.Core/Events/IConsumerInvoker.cs
@@ -0,0 +1,12 @@
+using System;
+
+namespace SmartStore.Core.Events
+{
+ ///
+ /// Responsible for invoking event message handler methods.
+ ///
+ public interface IConsumerInvoker
+ {
+ void Invoke(ConsumerDescriptor descriptor, IConsumer consumer, ConsumeContext envelope) where TMessage : class;
+ }
+}
diff --git a/src/Libraries/SmartStore.Core/Events/IConsumerRegistry.cs b/src/Libraries/SmartStore.Core/Events/IConsumerRegistry.cs
new file mode 100644
index 0000000000..4339d6fb4f
--- /dev/null
+++ b/src/Libraries/SmartStore.Core/Events/IConsumerRegistry.cs
@@ -0,0 +1,13 @@
+using System;
+using System.Collections.Generic;
+
+namespace SmartStore.Core.Events
+{
+ ///
+ /// A registry for fast lookup.
+ ///
+ public interface IConsumerRegistry
+ {
+ IEnumerable GetConsumers(object message);
+ }
+}
diff --git a/src/Libraries/SmartStore.Core/Events/IConsumerResolver.cs b/src/Libraries/SmartStore.Core/Events/IConsumerResolver.cs
new file mode 100644
index 0000000000..7a7be293d7
--- /dev/null
+++ b/src/Libraries/SmartStore.Core/Events/IConsumerResolver.cs
@@ -0,0 +1,16 @@
+using System;
+using System.Reflection;
+using Autofac;
+
+namespace SmartStore.Core.Events
+{
+ ///
+ /// Resolves the concrete class instance which contains
+ /// the requested consumer.
+ ///
+ public interface IConsumerResolver
+ {
+ IConsumer Resolve(ConsumerDescriptor descriptor);
+ object ResolveParameter(ParameterInfo p, IComponentContext c = null);
+ }
+}
diff --git a/src/Libraries/SmartStore.Core/Events/IEventPublisher.cs b/src/Libraries/SmartStore.Core/Events/IEventPublisher.cs
index 54fb88e357..21f0f6ebe6 100644
--- a/src/Libraries/SmartStore.Core/Events/IEventPublisher.cs
+++ b/src/Libraries/SmartStore.Core/Events/IEventPublisher.cs
@@ -1,8 +1,9 @@
-
+using System;
+
namespace SmartStore.Core.Events
{
- public interface IEventPublisher
- {
- void Publish(T eventMessage);
- }
+ public interface IEventPublisher
+ {
+ void Publish(T message) where T : class;
+ }
}
diff --git a/src/Libraries/SmartStore.Core/Events/NullEventPublisher.cs b/src/Libraries/SmartStore.Core/Events/NullEventPublisher.cs
index d0bff29ca8..07d7ae6e11 100644
--- a/src/Libraries/SmartStore.Core/Events/NullEventPublisher.cs
+++ b/src/Libraries/SmartStore.Core/Events/NullEventPublisher.cs
@@ -10,7 +10,7 @@ public static IEventPublisher Instance
get { return s_instance; }
}
- public void Publish(T eventMessage)
+ public void Publish(T eventMessage) where T : class
{
// Noop
}
diff --git a/src/Libraries/SmartStore.Core/Extensions/CollectionExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/CollectionExtensions.cs
index 8fec538fbe..5bd474e782 100644
--- a/src/Libraries/SmartStore.Core/Extensions/CollectionExtensions.cs
+++ b/src/Libraries/SmartStore.Core/Extensions/CollectionExtensions.cs
@@ -1,9 +1,6 @@
using System;
using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Web;
-using System.Web.Routing;
+using SmartStore.Collections;
namespace SmartStore
{
@@ -26,7 +23,22 @@ public static void AddRange(this ICollection initial, IEnumerable other
}
}
- public static bool IsNullOrEmpty(this ICollection source)
+ public static SyncedCollection AsSynchronized(this ICollection source)
+ {
+ return AsSynchronized(source, new object());
+ }
+
+ public static SyncedCollection AsSynchronized(this ICollection source, object syncRoot)
+ {
+ if (source is SyncedCollection sc)
+ {
+ return sc;
+ }
+
+ return new SyncedCollection(source, syncRoot);
+ }
+
+ public static bool IsNullOrEmpty(this ICollection source)
{
return source == null || source.Count == 0;
}
diff --git a/src/Libraries/SmartStore.Core/Extensions/ConversionExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/ConversionExtensions.cs
index 35fa7b73e2..58992963be 100644
--- a/src/Libraries/SmartStore.Core/Extensions/ConversionExtensions.cs
+++ b/src/Libraries/SmartStore.Core/Extensions/ConversionExtensions.cs
@@ -213,16 +213,21 @@ public static byte[] ToByteArray(this Stream stream)
if (stream == null)
throw new ArgumentNullException(nameof(stream));
- if (stream is MemoryStream)
+ if (stream is MemoryStream mem)
{
- return ((MemoryStream)stream).ToArray();
+ if (mem.TryGetBuffer(out var buffer))
+ {
+ return buffer.Array;
+ }
+
+ return mem.ToArray();
}
else
{
using (var streamReader = new MemoryStream())
{
stream.CopyTo(streamReader);
- return streamReader.ToArray();
+ return streamReader.GetBuffer();
}
}
}
@@ -232,9 +237,14 @@ public static async Task ToByteArrayAsync(this Stream stream)
if (stream == null)
throw new ArgumentNullException(nameof(stream));
- if (stream is MemoryStream)
+ if (stream is MemoryStream mem)
{
- return ((MemoryStream)stream).ToArray();
+ if (mem.TryGetBuffer(out var buffer))
+ {
+ return buffer.Array;
+ }
+
+ return mem.ToArray();
}
else
{
diff --git a/src/Libraries/SmartStore.Core/Extensions/EnumerableExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/EnumerableExtensions.cs
index a711c68aa7..cb529dcace 100644
--- a/src/Libraries/SmartStore.Core/Extensions/EnumerableExtensions.cs
+++ b/src/Libraries/SmartStore.Core/Extensions/EnumerableExtensions.cs
@@ -423,6 +423,36 @@ public static int Remove(this IList list, Func selector)
return count;
}
- #endregion
- }
+ #endregion
+
+ #region Stack
+
+ public static bool TryPeek(this Stack stack, out T value)
+ {
+ value = default(T);
+
+ if (stack.Count > 0)
+ {
+ value = stack.Peek();
+ return true;
+ }
+
+ return false;
+ }
+
+ public static bool TryPop(this Stack stack, out T value)
+ {
+ value = default(T);
+
+ if (stack.Count > 0)
+ {
+ value = stack.Pop();
+ return true;
+ }
+
+ return false;
+ }
+
+ #endregion
+ }
}
diff --git a/src/Libraries/SmartStore.Core/Extensions/HttpExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/HttpExtensions.cs
index 1d6bb939dc..59851619db 100644
--- a/src/Libraries/SmartStore.Core/Extensions/HttpExtensions.cs
+++ b/src/Libraries/SmartStore.Core/Extensions/HttpExtensions.cs
@@ -11,6 +11,7 @@
using SmartStore.Core.Infrastructure;
using SmartStore.Core;
using System.Web.Mvc;
+using SmartStore.Core.Fakes;
namespace SmartStore
{
@@ -28,6 +29,47 @@ public static class HttpExtensions
new Tuple("X-Url-Scheme", "https")
};
+ ///
+ /// Tries to get the instance without throwing exceptions
+ ///
+ /// The instance or null .
+ public static HttpRequestBase SafeGetHttpRequest(this HttpContext httpContext)
+ {
+ if (httpContext == null)
+ {
+ return null;
+ }
+
+ return SafeGetHttpRequest(new HttpContextWrapper(httpContext));
+ }
+
+ ///
+ /// Tries to get the instance without throwing exceptions
+ ///
+ /// The instance or null .
+ public static HttpRequestBase SafeGetHttpRequest(this HttpContextBase httpContext)
+ {
+ if (httpContext == null)
+ {
+ return null;
+ }
+
+ if (httpContext.Handler != null || httpContext is FakeHttpContext)
+ {
+ // Having a handler means we're most likely in the MVC routing pipeline.
+ return httpContext.Request;
+ }
+
+ try
+ {
+ return httpContext.Request;
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
///
/// Returns wether the specified url is local to the host or not
///
@@ -242,7 +284,7 @@ where key.StartsWith(pattern, StringComparison.OrdinalIgnoreCase)
return keys.ToArray();
}
- public static ControllerContext GetMasterControllerContext(this ControllerContext controllerContext)
+ public static ControllerContext GetRootControllerContext(this ControllerContext controllerContext)
{
Guard.NotNull(controllerContext, nameof(controllerContext));
@@ -255,6 +297,21 @@ public static ControllerContext GetMasterControllerContext(this ControllerContex
return ctx;
}
- }
+ public static bool IsBareBonePage(this ControllerContext controllerContext)
+ {
+ var ctx = controllerContext.GetRootControllerContext();
+
+ if (ctx is ViewContext viewContext)
+ {
+ // IsPopUp or Framed
+ if (viewContext.ViewBag.IsPopup == true || viewContext.ViewBag.Framed == true)
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+ }
}
diff --git a/src/Libraries/SmartStore.Core/Extensions/IOExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/IOExtensions.cs
index 5b6c628c10..2104c9c3ee 100644
--- a/src/Libraries/SmartStore.Core/Extensions/IOExtensions.cs
+++ b/src/Libraries/SmartStore.Core/Extensions/IOExtensions.cs
@@ -1,9 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
namespace SmartStore
{
diff --git a/src/Libraries/SmartStore.Core/Extensions/RouteExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/RouteExtensions.cs
index e57d2ca275..845d7a9a79 100644
--- a/src/Libraries/SmartStore.Core/Extensions/RouteExtensions.cs
+++ b/src/Libraries/SmartStore.Core/Extensions/RouteExtensions.cs
@@ -34,10 +34,10 @@ public static string GetAreaName(this RouteBase route)
return null;
}
- ///
- /// Generates an identifier for the given route in the form "[{area}.]{controller}.{action}"
- ///
- public static string GenerateRouteIdentifier(this RouteData routeData)
+ ///
+ /// Generates an identifier for the given route in the form "[{area}.]{controller}.{action}"
+ ///
+ public static string GenerateRouteIdentifier(this RouteData routeData)
{
string area = routeData.GetAreaName();
string controller = routeData.GetRequiredString("controller");
diff --git a/src/Libraries/SmartStore.Core/Extensions/StringExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/StringExtensions.cs
index f822ef9ec6..211fb926af 100644
--- a/src/Libraries/SmartStore.Core/Extensions/StringExtensions.cs
+++ b/src/Libraries/SmartStore.Core/Extensions/StringExtensions.cs
@@ -11,6 +11,7 @@
using System.Linq;
using AngleSharp.Dom;
using AngleSharp.Parser.Html;
+using SmartStore.Core.Html;
namespace SmartStore
{
@@ -22,7 +23,7 @@ public static class StringExtensions
public const char LineFeed = '\n';
public const char Tab = '\t';
- private delegate void ActionLine(TextWriter textWriter, string line);
+ private delegate void ActionLine(TextWriter textWriter, string line);
#region Char extensions
@@ -564,34 +565,7 @@ public static string RemoveHtml(this string source, ICollection removeTa
public static string RemoveHtml(this string source)
{
- if (source.IsEmpty())
- return string.Empty;
-
- var ignoreTags = new HashSet(StringComparer.OrdinalIgnoreCase) { "script", "style", "svg", "img" };
-
- var parser = new HtmlParser();
- var doc = parser.Parse(source);
-
- var treeWalker = doc.CreateTreeWalker(doc.Body, FilterSettings.Text);
-
- var sb = new StringBuilder();
-
- var node = treeWalker.ToNext();
- while (node != null)
- {
- if (!ignoreTags.Contains(node.Parent.NodeName))
- {
- var text = node.TextContent;
- if (text.HasValue())
- {
- sb.AppendLine(text);
- }
- }
-
- node = treeWalker.ToNext();
- }
-
- return sb.ToString().HtmlDecode();
+ return HtmlUtils.StripTags(source).Trim().HtmlDecode();
}
///
@@ -637,30 +611,29 @@ public static string[] SplitSafe(this string value, string separator)
if (string.IsNullOrEmpty(value))
return new string[0];
- // do not use separator.IsEmpty() here because whitespace like " " is a valid separator.
+ // Do not use separator.IsEmpty() here because whitespace like " " is a valid separator.
// an empty separator "" returns array with value.
if (separator == null)
{
- separator = "|";
-
- if (value.IndexOf(separator) < 0)
+ for (var i = 0; i < value.Length; i++)
{
- if (value.IndexOf(';') > -1)
- {
- separator = ";";
- }
- else if (value.IndexOf(',') > -1)
+ var c = value[i];
+ if (c == ';' || c == ',' || c == '|')
{
- separator = ",";
+ return value.Split(new char[] { c }, StringSplitOptions.RemoveEmptyEntries);
}
- else if (value.IndexOf(Environment.NewLine) > -1)
+ if (c == '\r' && (i + 1) < value.Length & value[i + 1] == '\n')
{
- separator = Environment.NewLine;
+ return value.GetLines(false, true).ToArray();
}
}
- }
- return value.Split(new string[] { separator }, StringSplitOptions.RemoveEmptyEntries);
+ return new string[] { value };
+ }
+ else
+ {
+ return value.Split(new string[] { separator }, StringSplitOptions.RemoveEmptyEntries);
+ }
}
/// Splits a string into two strings
@@ -1113,11 +1086,8 @@ public static string ReplaceCsvChars(this string s)
[DebuggerStepThrough]
public static string HighlightKeywords(this string input, string keywords, string preMatch = "", string postMatch = " ")
{
- if (preMatch == null)
- throw new ArgumentNullException(nameof(preMatch));
-
- if (postMatch == null)
- throw new ArgumentNullException(nameof(postMatch));
+ Guard.NotNull(preMatch, nameof(preMatch));
+ Guard.NotNull(postMatch, nameof(postMatch));
if (string.IsNullOrWhiteSpace(input) || string.IsNullOrWhiteSpace(keywords))
{
@@ -1133,7 +1103,7 @@ public static string HighlightKeywords(this string input, string keywords, strin
if (!string.IsNullOrWhiteSpace(pattern))
{
var rg = new Regex(pattern, RegexOptions.IgnoreCase);
- input = rg.Replace(input, m => preMatch + m.Value + postMatch);
+ input = rg.Replace(input, m => preMatch + m.Value.EmptyNull().HtmlEncode() + postMatch);
}
return input;
diff --git a/src/Libraries/SmartStore.Core/Fakes/FakeHttpContext.cs b/src/Libraries/SmartStore.Core/Fakes/FakeHttpContext.cs
index 4a024630cc..68302e9a61 100644
--- a/src/Libraries/SmartStore.Core/Fakes/FakeHttpContext.cs
+++ b/src/Libraries/SmartStore.Core/Fakes/FakeHttpContext.cs
@@ -66,6 +66,8 @@ public FakeHttpContext(string relativeUrl,
_serverVariables = serverVariables;
_items = new Dictionary();
+
+ Handler = new FakeHttpHandler();
}
public override IHttpHandler Handler { get; set; }
diff --git a/src/Libraries/SmartStore.Core/Fakes/FakeHttpHandler.cs b/src/Libraries/SmartStore.Core/Fakes/FakeHttpHandler.cs
new file mode 100644
index 0000000000..ea0839801a
--- /dev/null
+++ b/src/Libraries/SmartStore.Core/Fakes/FakeHttpHandler.cs
@@ -0,0 +1,15 @@
+using System;
+using System.Web;
+
+namespace SmartStore.Core.Fakes
+{
+ public class FakeHttpHandler : IHttpHandler
+ {
+ public bool IsReusable => true;
+
+ public void ProcessRequest(HttpContext context)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/src/Libraries/SmartStore.Core/Fakes/FakeHttpRequest.cs b/src/Libraries/SmartStore.Core/Fakes/FakeHttpRequest.cs
index 5238183265..ee5ecc7928 100644
--- a/src/Libraries/SmartStore.Core/Fakes/FakeHttpRequest.cs
+++ b/src/Libraries/SmartStore.Core/Fakes/FakeHttpRequest.cs
@@ -115,8 +115,8 @@ public override string ApplicationPath
{
get
{
- //we know that relative paths always start with ~/
- //ApplicationPath should start with /
+ // We know that relative paths always start with ~/
+ // ApplicationPath should start with /
if (_relativeUrl != null && _relativeUrl.StartsWith("~/"))
return _relativeUrl.Remove(0, 1);
return null;
@@ -137,10 +137,11 @@ public override string UserHostAddress
}
public override string RawUrl => this.ApplicationPath;
- public override bool IsSecureConnection => false;
+ public override bool IsSecureConnection => _url?.Scheme?.EmptyNull().StartsWith("https", StringComparison.OrdinalIgnoreCase) == true;
public override bool IsAuthenticated => false;
public override string[] UserLanguages => new string[] { };
public override string UserAgent => "SmartStore.NET";
+ public override bool IsLocal => false;
public override RequestContext RequestContext
{
diff --git a/src/Libraries/SmartStore.Core/Html/BBCodeHelper.cs b/src/Libraries/SmartStore.Core/Html/BBCodeHelper.cs
index dea33ad5f2..b4e9c92eb0 100644
--- a/src/Libraries/SmartStore.Core/Html/BBCodeHelper.cs
+++ b/src/Libraries/SmartStore.Core/Html/BBCodeHelper.cs
@@ -4,21 +4,15 @@
namespace SmartStore.Core.Html
{
- ///
- /// Represents a BBCode helper
- ///
public partial class BBCodeHelper
{
- #region Fields
private static readonly Regex regexBold = new Regex(@"\[b\](.+?)\[/b\]", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex regexItalic = new Regex(@"\[i\](.+?)\[/i\]", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex regexUnderLine = new Regex(@"\[u\](.+?)\[/u\]", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex regexUrl1 = new Regex(@"\[url\=([^\]]+)\]([^\]]+)\[/url\]", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex regexUrl2 = new Regex(@"\[url\](.+?)\[/url\]", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex regexQuote = new Regex(@"\[quote(=.+?)?\](.+?)\[/quote\]", RegexOptions.Compiled | RegexOptions.IgnoreCase);
- #endregion
- #region Methods
///
/// Formats the text
///
@@ -30,8 +24,14 @@ public partial class BBCodeHelper
/// A value indicating whether to replace Code
/// A value indicating whether to replace Quote
/// Formatted text
- public static string FormatText(string text, bool replaceBold, bool replaceItalic,
- bool replaceUnderline, bool replaceUrl, bool replaceCode, bool replaceQuote)
+ public static string ToHtml(
+ string text,
+ bool replaceBold = true,
+ bool replaceItalic = true,
+ bool replaceUnderline = true,
+ bool replaceUrl = true,
+ bool replaceCode = true,
+ bool replaceQuote = true)
{
if (String.IsNullOrEmpty(text))
return string.Empty;
@@ -115,6 +115,5 @@ public static string RemoveQuotes(string str)
str = Regex.Replace(str, @"\[/quote\]", String.Empty, RegexOptions.Compiled | RegexOptions.IgnoreCase);
return str;
}
- #endregion
}
}
diff --git a/src/Libraries/SmartStore.Core/Html/HtmlUtils.cs b/src/Libraries/SmartStore.Core/Html/HtmlUtils.cs
index 3e645dcc1f..f3239a1b15 100644
--- a/src/Libraries/SmartStore.Core/Html/HtmlUtils.cs
+++ b/src/Libraries/SmartStore.Core/Html/HtmlUtils.cs
@@ -6,6 +6,10 @@
using AngleSharp;
using AngleSharp.Parser.Html;
using AngleSharp.Parser.Css;
+using System.Collections.Generic;
+using AngleSharp.Dom;
+using AngleSharp.Dom.Html;
+using Ganss.XSS;
namespace SmartStore.Core.Html
{
@@ -14,133 +18,127 @@ namespace SmartStore.Core.Html
///
public partial class HtmlUtils
{
- private readonly static Regex _paragraphStartRegex = new Regex("", RegexOptions.IgnoreCase);
+ private readonly static Regex _paragraphStartRegex = new Regex("
", RegexOptions.IgnoreCase);
private readonly static Regex _paragraphEndRegex = new Regex("
", RegexOptions.IgnoreCase);
- //private static Regex ampRegex = new Regex("&(?!(?:#[0-9]{2,4};|[a-z0-9]+;))", RegexOptions.Compiled | RegexOptions.IgnoreCase);
+ //private static Regex ampRegex = new Regex("&(?!(?:#[0-9]{2,4};|[a-z0-9]+;))", RegexOptions.Compiled | RegexOptions.IgnoreCase);
- private static string EnsureOnlyAllowedHtml(string text)
- {
- if (String.IsNullOrEmpty(text))
- return string.Empty;
+ ///
+ /// Formats the text
+ ///
+ /// Text
+ /// A value indicating whether to strip tags
+ /// A value indicating whether HTML is allowed
+ /// A value indicating whether HTML is allowed
+ /// A value indicating whether BBCode is allowed
+ /// A value indicating whether to resolve links
+ /// A value indicating whether to add "noFollow" tag
+ /// Formatted text
+ [Obsolete("Use specific formatter methods instead, e.g. StripTags(), SanitizeHtml() etc.")]
+ public static string FormatText(
+ string text,
+ bool stripTags,
+ bool convertPlainTextToHtml,
+ bool allowHtml,
+ bool allowBBCode,
+ bool resolveLinks,
+ bool addNoFollowTag)
+ {
- string allowedTags = "br,hr,b,i,u,a,div,ol,ul,li,dl,dt,dd,blockquote,img,span,p,em,strong,font,pre,h1,h2,h3,h4,h5,h6,address,cite";
+ if (String.IsNullOrEmpty(text))
+ return string.Empty;
- var m = Regex.Matches(text, "<.*?>", RegexOptions.IgnoreCase);
- for (int i = m.Count - 1; i >= 0; i--)
- {
- string tag = text.Substring(m[i].Index + 1, m[i].Length - 1).Trim().ToLower();
+ try
+ {
+ if (stripTags)
+ {
+ text = StripTags(text);
+ }
- if (!IsValidTag(tag, allowedTags))
- {
- text = text.Remove(m[i].Index, m[i].Length);
- }
- }
+ if (allowHtml)
+ {
+ text = SanitizeHtml(text);
+ }
+ else
+ {
+ text = HttpUtility.HtmlEncode(text);
+ }
- return text;
- }
+ if (convertPlainTextToHtml)
+ {
+ text = ConvertPlainTextToHtml(text);
+ }
- private static bool IsValidTag(string tag, string tags)
- {
- string[] allowedTags = tags.Split(',');
- if (tag.IndexOf("javascript", StringComparison.OrdinalIgnoreCase) >= 0) return false;
- if (tag.IndexOf("vbscript", StringComparison.OrdinalIgnoreCase) >= 0) return false;
- if (tag.IndexOf("onclick", StringComparison.OrdinalIgnoreCase) >= 0) return false;
+ if (allowBBCode)
+ {
+ text = BBCodeHelper.ToHtml(text, true, true, true, true, true, true);
+ }
- var endchars = new char[] { ' ', '>', '/', '\t' };
+ if (resolveLinks)
+ {
+ text = ResolveLinksHelper.FormatText(text);
+ }
- int pos = tag.IndexOfAny(endchars, 1);
- if (pos > 0) tag = tag.Substring(0, pos);
- if (tag[0] == '/') tag = tag.Substring(1);
+ if (addNoFollowTag)
+ {
+ //add noFollow tag. not implemented
+ }
+ }
+ catch (Exception exc)
+ {
+ text = string.Format("Text cannot be formatted. Error: {0}", exc.Message);
+ }
- return allowedTags.Any(aTag => tag == aTag);
- }
+ return text;
+ }
- ///
- /// Formats the text
- ///
- /// Text
- /// A value indicating whether to strip tags
- /// A value indicating whether HTML is allowed
- /// A value indicating whether HTML is allowed
- /// A value indicating whether BBCode is allowed
- /// A value indicating whether to resolve links
- /// A value indicating whether to add "noFollow" tag
- /// Formatted text
- public static string FormatText(
- string text,
- bool stripTags,
- bool convertPlainTextToHtml,
- bool allowHtml,
- bool allowBBCode,
- bool resolveLinks,
- bool addNoFollowTag)
+ public static string SanitizeHtml(string html, bool isFragment = true)
{
-
- if (String.IsNullOrEmpty(text))
+ if (string.IsNullOrEmpty(html))
return string.Empty;
- try
- {
- if (stripTags)
- {
- text = StripTags(text);
- }
-
- if (allowHtml)
- {
- text = EnsureOnlyAllowedHtml(text);
- }
- else
- {
- text = HttpUtility.HtmlEncode(text);
- }
-
- if (convertPlainTextToHtml)
- {
- text = ConvertPlainTextToHtml(text);
- }
-
- if (allowBBCode)
- {
- text = BBCodeHelper.FormatText(text, true, true, true, true, true, true);
- }
+ var sanitizer = new HtmlSanitizer();
- if (resolveLinks)
- {
- text = ResolveLinksHelper.FormatText(text);
- }
-
- if (addNoFollowTag)
- {
- //add noFollow tag. not implemented
- }
- }
- catch (Exception exc)
- {
- text = string.Format("Text cannot be formatted. Error: {0}", exc.Message);
- }
- return text;
+ if (isFragment)
+ {
+ return sanitizer.Sanitize(html);
+ }
+ else
+ {
+ return sanitizer.SanitizeDocument(html);
+ }
}
///
/// Strips tags
///
- /// Text
+ /// Text
/// Formatted text
- public static string StripTags(string text)
+ public static string StripTags(string html)
{
- if (String.IsNullOrEmpty(text))
- return string.Empty;
+ if (html.IsEmpty())
+ return string.Empty;
- text = Regex.Replace(text, @"(>)(\r|\n)*(<)", "><");
- text = Regex.Replace(text, "(<[^>]*>)([^<]*)", "$2");
- text = Regex.Replace(text, "(?[0-9]{2,4};|"|&| |<|>|€|©|®|‰|‡|†|‹|›|„|”|“|‚|’|‘|—|–||||| | | |˜|ˆ|Ÿ|š|Š)", "@");
+ var ignoreTags = new HashSet(StringComparer.OrdinalIgnoreCase) { "script", "style", "svg", "img" };
+ var parser = new HtmlParser();
+ var doc = parser.Parse(html);
- return text;
- }
+ List removeElements = new List();
+
+ foreach (var ignoreTag in ignoreTags)
+ {
+ removeElements.AddRange(doc.QuerySelectorAll(ignoreTag));
+ }
+
+ foreach (var el in removeElements)
+ {
+ el.Remove();
+ }
+
+ return doc.Body.TextContent;
+ }
///
- /// replace anchor text (remove a tag from the following url Name and output only the string "Name")
+ /// Replace anchor text (remove a tag from the following url Name and output only the string "Name")
///
/// Text
/// Text
@@ -160,7 +158,7 @@ public static string ReplaceAnchorTags(string text)
/// Formatted text
public static string ConvertPlainTextToHtml(string text)
{
- if (String.IsNullOrEmpty(text))
+ if (string.IsNullOrEmpty(text))
return string.Empty;
text = text.Replace("\r\n", " ");
diff --git a/src/Libraries/SmartStore.Core/Html/ResolveLinksHelper.cs b/src/Libraries/SmartStore.Core/Html/ResolveLinksHelper.cs
index c58f9540ba..afa070448f 100644
--- a/src/Libraries/SmartStore.Core/Html/ResolveLinksHelper.cs
+++ b/src/Libraries/SmartStore.Core/Html/ResolveLinksHelper.cs
@@ -10,16 +10,12 @@ namespace SmartStore.Core.Html
///
public partial class ResolveLinksHelper
{
- #region Fields
///
/// The regular expression used to parse links.
///
private static readonly Regex regex = new Regex("((http://|https://|www\\.)([A-Z0-9.\\-]{1,})\\.[0-9A-Z?;~&\\(\\)#,=\\-_\\./\\+]{2,})", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private const string link = "{2} ";
private const int MAX_LENGTH = 50;
- #endregion
-
- #region Utilities
///
/// Shortens any absolute URL to a specified maximum length
@@ -80,9 +76,7 @@ private static string ShortenUrl(string url, int max)
return url;
}
- #endregion
- #region Methods
///
/// Formats the text
///
@@ -93,21 +87,20 @@ public static string FormatText(string text)
if (String.IsNullOrEmpty(text))
return string.Empty;
- var info = CultureInfo.InvariantCulture;
+ var ci = CultureInfo.InvariantCulture;
foreach (Match match in regex.Matches(text))
{
if (!match.Value.Contains("://"))
{
- text = text.Replace(match.Value, string.Format(info, link, "http://", match.Value, ShortenUrl(match.Value, MAX_LENGTH)));
+ text = text.Replace(match.Value, string.Format(ci, link, "http://", match.Value, ShortenUrl(match.Value, MAX_LENGTH)));
}
else
{
- text = text.Replace(match.Value, string.Format(info, link, string.Empty, match.Value, ShortenUrl(match.Value, MAX_LENGTH)));
+ text = text.Replace(match.Value, string.Format(ci, link, string.Empty, match.Value, ShortenUrl(match.Value, MAX_LENGTH)));
}
}
return text;
}
- #endregion
}
}
diff --git a/src/Libraries/SmartStore.Core/IO/LocalFileSystem.cs b/src/Libraries/SmartStore.Core/IO/LocalFileSystem.cs
index 3187971ec6..9591f17d81 100644
--- a/src/Libraries/SmartStore.Core/IO/LocalFileSystem.cs
+++ b/src/Libraries/SmartStore.Core/IO/LocalFileSystem.cs
@@ -31,7 +31,7 @@ protected internal LocalFileSystem(string basePath, string publicPath)
{
basePath = basePath.EmptyNull();
- var pathIsAbsolute = FileSystemHelper.IsFullPath(basePath);
+ var pathIsAbsolute = PathHelper.IsAbsolutePhysicalPath(basePath);
NormalizeStoragePath(ref basePath, pathIsAbsolute);
@@ -517,7 +517,7 @@ public long Size
public DateTime LastUpdated
{
- get { return _fileInfo.LastWriteTime; }
+ get { return _fileInfo.LastWriteTimeUtc; }
}
public string Extension
@@ -595,7 +595,7 @@ public string Name
public DateTime LastUpdated
{
- get { return _directoryInfo.LastWriteTime; }
+ get { return _directoryInfo.LastWriteTimeUtc; }
}
public long Size
diff --git a/src/Libraries/SmartStore.Core/IO/VirtualPath/IVirtualFolder.cs b/src/Libraries/SmartStore.Core/IO/VirtualPath/IVirtualFolder.cs
index 6f1bb5f3a2..e4177560ad 100644
--- a/src/Libraries/SmartStore.Core/IO/VirtualPath/IVirtualFolder.cs
+++ b/src/Libraries/SmartStore.Core/IO/VirtualPath/IVirtualFolder.cs
@@ -33,6 +33,7 @@ public interface IVirtualFolder
Stream OpenFile(string relativePath);
void CreateTextFile(string relativePath, string content);
+ Task CreateTextFileAsync(string relativePath, string content);
Stream CreateFile(string relativePath);
void CreateDirectory(string relativePath);
@@ -40,6 +41,7 @@ public interface IVirtualFolder
void DeleteDirectory(string relativePath);
string ReadFile(string relativePath);
+ Task ReadFileAsync(string relativePath);
void CopyFile(string relativePath, Stream destination);
DateTime GetFileLastWriteTimeUtc(string relativePath);
diff --git a/src/Libraries/SmartStore.Core/IO/VirtualPath/VirtualFolder.cs b/src/Libraries/SmartStore.Core/IO/VirtualPath/VirtualFolder.cs
index 8b892dedea..6355143bc6 100644
--- a/src/Libraries/SmartStore.Core/IO/VirtualPath/VirtualFolder.cs
+++ b/src/Libraries/SmartStore.Core/IO/VirtualPath/VirtualFolder.cs
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using System.Threading.Tasks;
using SmartStore.Core.Logging;
namespace SmartStore.Core.IO
@@ -117,6 +118,19 @@ public void CreateTextFile(string relativePath, string content)
}
}
+ public async Task CreateTextFileAsync(string relativePath, string content)
+ {
+ Guard.NotEmpty(relativePath, nameof(relativePath));
+
+ using (var stream = CreateFile(relativePath))
+ {
+ using (var tw = new StreamWriter(stream))
+ {
+ await tw.WriteAsync(content);
+ }
+ }
+ }
+
public virtual Stream CreateFile(string relativePath)
{
var filePath = MapPath(relativePath);
@@ -170,6 +184,26 @@ public virtual string ReadFile(string relativePath)
}
}
+ public virtual async Task ReadFileAsync(string relativePath)
+ {
+ Guard.NotEmpty(relativePath, nameof(relativePath));
+
+ var path = GetVirtualPath(relativePath);
+
+ if (!_vpp.FileExists(path))
+ {
+ return null;
+ }
+
+ using (var stream = _vpp.OpenFile(path))
+ {
+ using (var reader = new StreamReader(stream))
+ {
+ return await reader.ReadToEndAsync();
+ }
+ }
+ }
+
public virtual void CopyFile(string relativePath, Stream destination)
{
Guard.NotEmpty(relativePath, nameof(relativePath));
diff --git a/src/Libraries/SmartStore.Core/IPageable.cs b/src/Libraries/SmartStore.Core/IPageable.cs
index 6689becf09..ce270c8e4e 100644
--- a/src/Libraries/SmartStore.Core/IPageable.cs
+++ b/src/Libraries/SmartStore.Core/IPageable.cs
@@ -3,6 +3,7 @@
using System.Collections;
using System.Collections.Generic;
using System.Linq;
+using System.Threading.Tasks;
namespace SmartStore.Core
{
@@ -100,7 +101,19 @@ public interface IPagedList : IPageable, IList
/// A query with applied paging args
IQueryable ApplyPaging(IQueryable query);
+ ///
+ /// Loads the data synchronously.
+ ///
+ /// When true , always reloads data. When false , first checks to see whether data has been loaded already and skips if so.
+ /// Returns itself for chaining.
IPagedList Load(bool force = false);
+
+ ///
+ /// Loads the data asynchronously.
+ ///
+ /// When true , always reloads data. When false , first checks to see whether data has been loaded already and skips if so.
+ /// Returns itself for chaining.
+ Task> LoadAsync(bool force = false);
}
}
diff --git a/src/Libraries/SmartStore.Core/Infrastructure/AppDomainTypeFinder.cs b/src/Libraries/SmartStore.Core/Infrastructure/AppDomainTypeFinder.cs
index b7ffec8f69..4ed72318a1 100644
--- a/src/Libraries/SmartStore.Core/Infrastructure/AppDomainTypeFinder.cs
+++ b/src/Libraries/SmartStore.Core/Infrastructure/AppDomainTypeFinder.cs
@@ -71,7 +71,7 @@ public IEnumerable FindClassesOfType(Type assignTypeFrom, IEnumerable(object key = null, ILifetimeScope scope = null) where T : class
+ [DebuggerStepThrough]
+ public T Resolve(object key = null, ILifetimeScope scope = null) where T : class
{
if (key == null)
{
@@ -36,22 +38,26 @@ public T Resolve(object key = null, ILifetimeScope scope = null) where T : cl
return (scope ?? Scope()).ResolveKeyed(key);
}
- public T ResolveNamed(string name, ILifetimeScope scope = null) where T : class
+ [DebuggerStepThrough]
+ public T ResolveNamed(string name, ILifetimeScope scope = null) where T : class
{
return (scope ?? Scope()).ResolveNamed(name);
}
- public object Resolve(Type type, ILifetimeScope scope = null)
+ [DebuggerStepThrough]
+ public object Resolve(Type type, ILifetimeScope scope = null)
{
return (scope ?? Scope()).Resolve(type);
}
- public object ResolveNamed(string name, Type type, ILifetimeScope scope = null)
+ [DebuggerStepThrough]
+ public object ResolveNamed(string name, Type type, ILifetimeScope scope = null)
{
return (scope ?? Scope()).ResolveNamed(name, type);
}
- public T[] ResolveAll(object key = null, ILifetimeScope scope = null)
+ [DebuggerStepThrough]
+ public T[] ResolveAll(object key = null, ILifetimeScope scope = null)
{
if (key == null)
{
@@ -130,7 +136,8 @@ private bool TryResolveAll(Type[] types, out object[] instances, ILifetimeScope
}
}
- public bool TryResolve(Type serviceType, ILifetimeScope scope, out object instance)
+ [DebuggerStepThrough]
+ public bool TryResolve(Type serviceType, ILifetimeScope scope, out object instance)
{
instance = null;
@@ -144,7 +151,8 @@ public bool TryResolve(Type serviceType, ILifetimeScope scope, out object instan
}
}
- public bool TryResolve(ILifetimeScope scope, out T instance)
+ [DebuggerStepThrough]
+ public bool TryResolve(ILifetimeScope scope, out T instance)
{
instance = default(T);
@@ -178,6 +186,7 @@ public T InjectUnsetProperties(T instance, ILifetimeScope scope = null)
return (scope ?? Scope()).InjectUnsetProperties(instance);
}
+ [DebuggerStepThrough]
public ILifetimeScope Scope()
{
var scope = _container.Resolve().GetLifetimeScope(null);
diff --git a/src/Libraries/SmartStore.Core/Infrastructure/DependencyManagement/DefaultLifetimeScopeAccessor.cs b/src/Libraries/SmartStore.Core/Infrastructure/DependencyManagement/DefaultLifetimeScopeAccessor.cs
index bf84c53263..1c7fed864c 100644
--- a/src/Libraries/SmartStore.Core/Infrastructure/DependencyManagement/DefaultLifetimeScopeAccessor.cs
+++ b/src/Libraries/SmartStore.Core/Infrastructure/DependencyManagement/DefaultLifetimeScopeAccessor.cs
@@ -61,6 +61,7 @@ public ILifetimeScope GetLifetimeScope(Action configurationAct
_state.SetState((scope = BeginLifetimeScope(configurationAction)));
//scope.CurrentScopeEnding += OnScopeEnding;
}
+
return scope;
}
@@ -76,7 +77,7 @@ public ILifetimeScope GetLifetimeScope(Action configurationAct
// Debug.WriteLine("Scope END, Web: " + isWeb);
//}
- private ILifetimeScope BeginLifetimeScope(Action configurationAction)
+ public ILifetimeScope BeginLifetimeScope(Action configurationAction)
{
return (configurationAction == null)
? _rootContainer.BeginLifetimeScope(ScopeTag)
diff --git a/src/Libraries/SmartStore.Core/Infrastructure/DependencyManagement/ILifetimeScopeAccessor.cs b/src/Libraries/SmartStore.Core/Infrastructure/DependencyManagement/ILifetimeScopeAccessor.cs
index 03beda80b8..ee09073226 100644
--- a/src/Libraries/SmartStore.Core/Infrastructure/DependencyManagement/ILifetimeScopeAccessor.cs
+++ b/src/Libraries/SmartStore.Core/Infrastructure/DependencyManagement/ILifetimeScopeAccessor.cs
@@ -39,5 +39,14 @@ public interface ILifetimeScopeAccessor
/// desirable to create a new scope, but use the existing, http context bound scope instead.
///
IDisposable BeginContextAwareScope();
+
+ ///
+ /// Creates a nested lifetime scope that services can be resolved from
+ ///
+ /// A configuration action that will execute during lifetime scope creation.
+ ///
+ /// A new nested lifetime scope.
+ ///
+ ILifetimeScope BeginLifetimeScope(Action configurationAction);
}
}
diff --git a/src/Libraries/SmartStore.Core/Infrastructure/SmartStoreEngine.cs b/src/Libraries/SmartStore.Core/Infrastructure/SmartStoreEngine.cs
index 18dcfc4222..0029454b28 100644
--- a/src/Libraries/SmartStore.Core/Infrastructure/SmartStoreEngine.cs
+++ b/src/Libraries/SmartStore.Core/Infrastructure/SmartStoreEngine.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using System.Web.Mvc;
@@ -148,6 +149,7 @@ public bool IsFullyInitialized
set;
}
+ [DebuggerStepThrough]
public T Resolve(string name = null) where T : class
{
if (name.HasValue())
@@ -157,6 +159,7 @@ public T Resolve(string name = null) where T : class
return ContainerManager.Resolve();
}
+ [DebuggerStepThrough]
public object Resolve(Type type, string name = null)
{
if (name.HasValue())
@@ -166,7 +169,7 @@ public object Resolve(Type type, string name = null)
return ContainerManager.Resolve(type);
}
-
+ [DebuggerStepThrough]
public T[] ResolveAll()
{
return ContainerManager.ResolveAll();
diff --git a/src/Libraries/SmartStore.Core/Infrastructure/WebAppTypeFinder.cs b/src/Libraries/SmartStore.Core/Infrastructure/WebAppTypeFinder.cs
index f06b87c086..a437eebd7d 100644
--- a/src/Libraries/SmartStore.Core/Infrastructure/WebAppTypeFinder.cs
+++ b/src/Libraries/SmartStore.Core/Infrastructure/WebAppTypeFinder.cs
@@ -95,7 +95,7 @@ protected virtual void LoadPluginAssemblies()
AppDomain.CurrentDomain.GetAssemblies().Select(x => x.FullName),
StringComparer.OrdinalIgnoreCase);
- var pluginAssemblies = new HashSet(PluginManager.ReferencedPlugins.Select(x => x.ReferencedAssembly).Where(x => x != null));
+ var pluginAssemblies = new HashSet(PluginManager.ReferencedPlugins.Select(x => x.Assembly.Assembly));
foreach (var assembly in pluginAssemblies)
{
diff --git a/src/Libraries/SmartStore.Core/Logging/log4net/Log4netLogger.cs b/src/Libraries/SmartStore.Core/Logging/log4net/Log4netLogger.cs
index 1b043a2108..6bd1f49e02 100644
--- a/src/Libraries/SmartStore.Core/Logging/log4net/Log4netLogger.cs
+++ b/src/Libraries/SmartStore.Core/Logging/log4net/Log4netLogger.cs
@@ -74,13 +74,12 @@ public void Log(LogLevel level, Exception exception, string message, object[] ar
protected internal void TryAddExtendedThreadInfo(LoggingEvent loggingEvent)
{
- HttpRequest httpRequest = null;
+ var isAppInitialized = EngineContext.Current.IsFullyInitialized;
- try
- {
- httpRequest = HttpContext.Current.Request;
- }
- catch
+ // Don't knowingly run into exception
+ var httpRequest = isAppInitialized ? HttpContext.Current.SafeGetHttpRequest() : null;
+
+ if (httpRequest == null)
{
loggingEvent.Properties["CustomerId"] = DBNull.Value;
loggingEvent.Properties["Url"] = DBNull.Value;
@@ -100,14 +99,12 @@ protected internal void TryAddExtendedThreadInfo(LoggingEvent loggingEvent)
{
using (new ActionDisposable(() => props["sm:ThreadInfoAdded"] = true))
{
- if (DataSettings.DatabaseIsInstalled() && EngineContext.Current.IsFullyInitialized)
+ if (DataSettings.DatabaseIsInstalled() && isAppInitialized)
{
var container = EngineContext.Current.ContainerManager;
- IWorkContext workContext;
-
// CustomerId
- if (container.TryResolve(null, out workContext))
+ if (container.TryResolve(null, out IWorkContext workContext))
{
try
{
@@ -119,10 +116,9 @@ protected internal void TryAddExtendedThreadInfo(LoggingEvent loggingEvent)
}
}
- IWebHelper webHelper;
// Url & stuff
- if (container.TryResolve(null, out webHelper))
+ if (container.TryResolve(null, out IWebHelper webHelper))
{
try
{
diff --git a/src/Libraries/SmartStore.Core/Money.cs b/src/Libraries/SmartStore.Core/Money.cs
index e4bdf4a24f..33244b92ef 100644
--- a/src/Libraries/SmartStore.Core/Money.cs
+++ b/src/Libraries/SmartStore.Core/Money.cs
@@ -135,8 +135,14 @@ int IComparable.CompareTo(object obj)
public override bool Equals(object obj)
{
- return Equals(obj as Money);
- }
+ // Prevent stack overflow.
+ if (obj != null)
+ {
+ return Equals(obj as Money);
+ }
+
+ return false;
+ }
bool IEquatable.Equals(Money other)
{
diff --git a/src/Libraries/SmartStore.Core/PagedList`T.cs b/src/Libraries/SmartStore.Core/PagedList`T.cs
index f7e951d047..0fa22d90e1 100644
--- a/src/Libraries/SmartStore.Core/PagedList`T.cs
+++ b/src/Libraries/SmartStore.Core/PagedList`T.cs
@@ -5,6 +5,7 @@
using System.Data.Entity.Infrastructure;
using System.Collections;
using System.Collections.ObjectModel;
+using System.Threading.Tasks;
namespace SmartStore.Core
{
@@ -66,6 +67,26 @@ private void EnsureIsLoaded()
}
}
+ private async Task EnsureIsLoadedAsync()
+ {
+ if (_list == null)
+ {
+ if (_totalCount == null)
+ {
+ _totalCount = SourceQuery.Count();
+ }
+
+ if (_queryIsPagedAlready)
+ {
+ _list = await SourceQuery.ToListAsync();
+ }
+ else
+ {
+ _list = await ApplyPaging(SourceQuery).ToListAsync();
+ }
+ }
+ }
+
#region IPageable Members
public IQueryable SourceQuery { get; private set; }
@@ -101,6 +122,7 @@ public IQueryable ApplyPaging(IQueryable query)
public IPagedList Load(bool force = false)
{
+ // Returns instance for chaining.
if (force && _list != null)
{
_list.Clear();
@@ -112,6 +134,20 @@ public IPagedList Load(bool force = false)
return this;
}
+ public async Task> LoadAsync(bool force = false)
+ {
+ // Returns instance for chaining.
+ if (force && _list != null)
+ {
+ _list.Clear();
+ _list = null;
+ }
+
+ await EnsureIsLoadedAsync();
+
+ return this;
+ }
+
public int PageIndex { get; set; }
public int PageSize { get; set; }
diff --git a/src/Libraries/SmartStore.Core/Plugins/PluginDescriptor.cs b/src/Libraries/SmartStore.Core/Plugins/PluginDescriptor.cs
index 35b2b3e8c9..ab6a32754e 100644
--- a/src/Libraries/SmartStore.Core/Plugins/PluginDescriptor.cs
+++ b/src/Libraries/SmartStore.Core/Plugins/PluginDescriptor.cs
@@ -11,6 +11,30 @@
namespace SmartStore.Core.Plugins
{
+ ///
+ /// Information about an assembly referenced by a plugin.
+ ///
+ public sealed class AssemblyReference
+ {
+ ///
+ /// File in ASP.NET temp folder
+ ///
+ public FileInfo File { get; internal set; }
+ ///
+ /// The original assembly file that a shadow copy was made from it
+ ///
+ public FileInfo OriginalFile { get; internal set; }
+ ///
+ /// The assembly that has been shadow copied and was loaded into the AppDomain
+ ///
+ public Assembly Assembly { get; internal set; }
+
+ ///
+ /// Exception thrown during activation/probing
+ ///
+ internal Exception ActivationException { get; set; }
+ }
+
[DataContract]
public class PluginDescriptor : IComparable
{
@@ -19,17 +43,16 @@ public class PluginDescriptor : IComparable
public PluginDescriptor()
{
- this.Version = new Version("1.0");
- this.MinAppVersion = SmartStoreVersion.Version;
+ Version = new Version("1.0");
+ MinAppVersion = SmartStoreVersion.Version;
}
// Unit tests
- public PluginDescriptor(Assembly referencedAssembly, FileInfo originalAssemblyFile, Type pluginType)
+ public PluginDescriptor(Assembly referencedAssembly, FileInfo originalAssemblyFile, Type pluginClrType)
: this()
{
- this.ReferencedAssembly = referencedAssembly;
- this.OriginalAssemblyFile = originalAssemblyFile;
- this.PluginType = pluginType;
+ Assembly = new AssemblyReference { Assembly = referencedAssembly, OriginalFile = originalAssemblyFile, File = originalAssemblyFile };
+ PluginClrType = pluginClrType;
}
///
@@ -42,6 +65,12 @@ public PluginDescriptor(Assembly referencedAssembly, FileInfo originalAssemblyFi
///
public string PhysicalPath { get; set; }
+ ///
+ /// Gets the folder name
+ ///
+ [DataMember]
+ public string FolderName { get; internal set; }
+
///
/// The virtual path of the runtime plugin
///
@@ -79,33 +108,23 @@ public string BrandImageFileName
}
///
- /// Plugin type
+ /// Plugin runtime type
///
- public Type PluginType { get; set; }
-
- ///
- /// The assembly that has been shadow copied and was loaded into the AppDomain
- ///
- public Assembly ReferencedAssembly { get; internal set; }
-
- ///
- /// The original assembly file that a shadow copy was made from it
- ///
- public FileInfo OriginalAssemblyFile { get; internal set; }
+ public Type PluginClrType { get; set; }
///
- /// The list of assembly files found in the plugin folder, except the main plugin assembly.
+ /// The plugin assembly.
///
- public IEnumerable ReferencedLocalAssemblyFiles { get; internal set; }
+ public AssemblyReference Assembly { get; internal set; } = new AssemblyReference();
///
- /// Gets any exception thrown during plugin activation & initializing
+ /// List of assemblies files found in the plugin folder, except the main plugin assembly.
///
- public Exception ActivationException { get; internal set; }
+ internal AssemblyReference[] ReferencedLocalAssemblies { get; set; } = new AssemblyReference[0];
- ///
- /// Gets or sets the plugin group
- ///
+ ///
+ /// Gets or sets the plugin group
+ ///
[DataMember]
public string Group { get; internal set; }
@@ -123,12 +142,6 @@ public bool IsInKnownGroup
[DataMember]
public string FriendlyName { get; set; }
- ///
- /// Gets the folder name
- ///
- [DataMember]
- public string FolderName { get; internal set; }
-
///
/// Gets or sets the system name
///
@@ -243,10 +256,10 @@ public string ResourceRootKey
public T Instance() where T : class, IPlugin
{
object instance;
- if (!EngineContext.Current.ContainerManager.TryResolve(PluginType, null, out instance))
+ if (!EngineContext.Current.ContainerManager.TryResolve(PluginClrType, null, out instance))
{
// Not registered
- instance = EngineContext.Current.ContainerManager.ResolveUnregistered(PluginType);
+ instance = EngineContext.Current.ContainerManager.ResolveUnregistered(PluginClrType);
}
var typedInstance = instance as T;
diff --git a/src/Libraries/SmartStore.Core/Plugins/PluginFileParser.cs b/src/Libraries/SmartStore.Core/Plugins/PluginFileParser.cs
index 07fcd1de9e..10e0ce284f 100644
--- a/src/Libraries/SmartStore.Core/Plugins/PluginFileParser.cs
+++ b/src/Libraries/SmartStore.Core/Plugins/PluginFileParser.cs
@@ -42,7 +42,9 @@ public override int Compare(string x, string y)
"Sales",
"Design",
"Performance",
- "Misc"
+ "B2B",
+ "Storefront",
+ "Law"
};
public readonly static IComparer KnownGroupComparer = new GroupComparer();
diff --git a/src/Libraries/SmartStore.Core/Plugins/PluginFinder.cs b/src/Libraries/SmartStore.Core/Plugins/PluginFinder.cs
index 8e61d47f37..4eddd033dd 100644
--- a/src/Libraries/SmartStore.Core/Plugins/PluginFinder.cs
+++ b/src/Libraries/SmartStore.Core/Plugins/PluginFinder.cs
@@ -1,32 +1,33 @@
using System;
-using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
namespace SmartStore.Core.Plugins
{
- ///
- /// Plugin finder
- ///
public class PluginFinder : IPluginFinder
{
private IList _plugins;
private readonly IDictionary _nameMap = new Dictionary(StringComparer.InvariantCultureIgnoreCase);
private readonly IDictionary _assemblyMap = new Dictionary();
- private static readonly object s_lock = new object();
+ private static readonly object _lock = new object();
- public PluginFinder()
+ private PluginFinder()
{
- lock (s_lock)
+ lock (_lock)
{
LoadPlugins();
}
}
+ public static IPluginFinder Current { get; } = new PluginFinder();
+
private void LoadPlugins()
{
+ if (_plugins != null)
+ return;
+
var plugins = PluginManager.ReferencedPlugins.ToList();
plugins.Sort(); //sort
_plugins = plugins;
@@ -34,7 +35,10 @@ private void LoadPlugins()
foreach (var plugin in plugins)
{
_nameMap[plugin.SystemName] = plugin;
- _assemblyMap[plugin.ReferencedAssembly] = plugin;
+ if (plugin.Assembly.Assembly != null)
+ {
+ _assemblyMap[plugin.Assembly.Assembly] = plugin;
+ }
}
}
@@ -47,7 +51,7 @@ private void LoadPlugins()
public virtual IEnumerable GetPlugins(bool installedOnly = true) where T : class, IPlugin
{
foreach (var plugin in _plugins)
- if (typeof(T).IsAssignableFrom(plugin.PluginType))
+ if (typeof(T).IsAssignableFrom(plugin.PluginClrType))
if (!installedOnly || plugin.Installed)
yield return plugin.Instance();
}
@@ -74,15 +78,14 @@ public virtual IEnumerable GetPluginDescriptors(bool instal
where T : class, IPlugin
{
foreach (var plugin in _plugins)
- if (typeof(T).IsAssignableFrom(plugin.PluginType))
+ if (typeof(T).IsAssignableFrom(plugin.PluginClrType))
if (!installedOnly || plugin.Installed)
yield return plugin;
}
public virtual PluginDescriptor GetPluginDescriptorByAssembly(Assembly assembly, bool installedOnly = true)
{
- PluginDescriptor descriptor;
- if (assembly != null && _assemblyMap.TryGetValue(assembly, out descriptor))
+ if (assembly != null && _assemblyMap.TryGetValue(assembly, out var descriptor))
{
if (!installedOnly || descriptor.Installed)
return descriptor;
@@ -99,8 +102,7 @@ public virtual PluginDescriptor GetPluginDescriptorByAssembly(Assembly assembly,
/// >Plugin descriptor
public virtual PluginDescriptor GetPluginDescriptorBySystemName(string systemName, bool installedOnly = true)
{
- PluginDescriptor descriptor;
- if (systemName.HasValue() && _nameMap.TryGetValue(systemName, out descriptor))
+ if (systemName.HasValue() && _nameMap.TryGetValue(systemName, out var descriptor))
{
if (!installedOnly || descriptor.Installed)
return descriptor;
@@ -118,12 +120,11 @@ public virtual PluginDescriptor GetPluginDescriptorBySystemName(string systemNam
/// >Plugin descriptor
public virtual PluginDescriptor GetPluginDescriptorBySystemName(string systemName, bool installedOnly = true) where T : class, IPlugin
{
- PluginDescriptor descriptor;
- if (systemName.HasValue() && _nameMap.TryGetValue(systemName, out descriptor))
+ if (systemName.HasValue() && _nameMap.TryGetValue(systemName, out var descriptor))
{
if (!installedOnly || descriptor.Installed)
{
- if (typeof(T).IsAssignableFrom(descriptor.PluginType))
+ if (typeof(T).IsAssignableFrom(descriptor.PluginClrType))
return descriptor;
}
}
diff --git a/src/Libraries/SmartStore.Core/Plugins/PluginManager.Cache.cs b/src/Libraries/SmartStore.Core/Plugins/PluginManager.Cache.cs
index 2d4e6ae541..dff9c225b5 100644
--- a/src/Libraries/SmartStore.Core/Plugins/PluginManager.Cache.cs
+++ b/src/Libraries/SmartStore.Core/Plugins/PluginManager.Cache.cs
@@ -38,10 +38,14 @@ private static bool DetectAndCleanStalePlugins(IEnumerable plu
.Select(x => new FileInfo(Path.Combine(_shadowCopyDir.FullName, x)))
.ToArray();
- foreach (var f in staleFiles)
+ // If that fails, then we will try to remove it the hard way by renaming it to .delete if it doesn't delete nicely
+ if (staleFiles.Length > 5)
{
- // If that fails, then we will try to remove it the hard way by renaming it to .delete if it doesn't delete nicely
- TryDeleteStaleFile(f);
+ staleFiles.AsParallel().ForAll(f => TryDeleteStaleFile(f));
+ }
+ else
+ {
+ staleFiles.Each(f => TryDeleteStaleFile(f));
}
return true;
@@ -132,14 +136,11 @@ private static void SavePluginsAssemblies(IEnumerable plugins)
foreach (var p in plugins)
{
- if (p.ReferencedLocalAssemblyFiles != null)
- {
- p.ReferencedLocalAssemblyFiles.Each(x => list.Add(x.Name));
- }
+ p.ReferencedLocalAssemblies.Each(x => list.Add(x.OriginalFile.Name));
- if (p.OriginalAssemblyFile != null)
+ if (p.Assembly.OriginalFile != null)
{
- list.Add(p.OriginalAssemblyFile.Name);
+ list.Add(p.Assembly.OriginalFile.Name);
}
}
@@ -166,15 +167,12 @@ private static int ComputePluginsHash(IEnumerable plugins)
// Add each *.dll, Description.txt, web.config to the hash
foreach (var p in plugins)
{
- if (p.OriginalAssemblyFile != null)
+ if (p.Assembly.OriginalFile != null)
{
- hashCombiner.Add(p.OriginalAssemblyFile);
+ hashCombiner.Add(p.Assembly.OriginalFile);
}
- if (p.ReferencedLocalAssemblyFiles != null)
- {
- p.ReferencedLocalAssemblyFiles.Each(x => hashCombiner.Add(x));
- }
+ p.ReferencedLocalAssemblies.Each(x => hashCombiner.Add(x.OriginalFile));
hashCombiner.Add(Path.Combine(p.PhysicalPath, "Description.txt"));
hashCombiner.Add(Path.Combine(p.PhysicalPath, "web.config"));
diff --git a/src/Libraries/SmartStore.Core/Plugins/PluginManager.cs b/src/Libraries/SmartStore.Core/Plugins/PluginManager.cs
index 6a95a30aae..c7c4d6fe4e 100644
--- a/src/Libraries/SmartStore.Core/Plugins/PluginManager.cs
+++ b/src/Libraries/SmartStore.Core/Plugins/PluginManager.cs
@@ -16,6 +16,7 @@
using SmartStore.Utilities;
using SmartStore.Utilities.Threading;
using SmartStore.Core.Data;
+using System.Threading.Tasks;
// Contributor: Umbraco (http://www.umbraco.com). Thanks a lot!
// SEE THIS POST for full details of what this does
@@ -33,7 +34,7 @@ public partial class PluginManager
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool SetDllDirectory(string lpPathName);
- private static readonly ReaderWriterLockSlim Locker = new ReaderWriterLockSlim();
+ private static readonly object _lock = new object();
private static readonly string _pluginsPath = "~/Plugins";
private static DirectoryInfo _shadowCopyDir;
private static readonly ConcurrentDictionary _referencedPlugins = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase);
@@ -105,101 +106,122 @@ public static void Initialize()
// adding a process-specific environment path (either bin/x86 or bin/x64)
// ensures that unmanaged native dependencies can be resolved successfully.
SetNativeDllPath();
- //SetPrivateEnvPath();
DynamicModuleUtility.RegisterModule(typeof(AutofacRequestLifetimeHttpModule));
- #region Plugins
+ var incompatiblePlugins = (new HashSet(StringComparer.OrdinalIgnoreCase)).AsSynchronized();
+ var inactiveAssemblies = _inactiveAssemblies.AsSynchronized();
+ var dirty = false;
- var incompatiblePlugins = new HashSet(StringComparer.OrdinalIgnoreCase);
+ var watch = Stopwatch.StartNew();
- using (Locker.GetWriteLock())
- {
- _shadowCopyDir = new DirectoryInfo(AppDomain.CurrentDomain.DynamicDirectory);
+ _shadowCopyDir = new DirectoryInfo(AppDomain.CurrentDomain.DynamicDirectory);
+
+ var plugins = LoadPluginDescriptors().ToArray();
+ var compatiblePlugins = plugins.Where(x => !x.Incompatible).ToArray();
+
+ var ms = watch.ElapsedMilliseconds;
+ Debug.WriteLine("INIT PLUGINS (LoadPluginDescriptors). Time elapsed: {0} ms.".FormatCurrentUI(ms));
+
+ // If plugins state is dirty, we copy files over to the dynamic folder,
+ // otherwise we just reference the previously copied file.
+ dirty = DetectAndCleanStalePlugins(compatiblePlugins);
+
+ // Perf: Initialize/probe all plugins in parallel
+ plugins.AsParallel().ForAll(x =>
+ {
+ // Deploy to ASP.NET dynamic folder.
+ DeployPlugin(x, dirty);
+
+ // Finalize
+ FinalizePlugin(x);
+ });
+
+ //// Retry when failed, because during parallel execution assembly loading MAY fail.
+ //// Therefore we retry initialization for failed plugins, but sequentially this time.
+ //foreach (var p in plugins)
+ //{
+ // // INFO: this seems redundant, but it's ok:
+ // // DeployPlugin() only probes assemblies that are not loaded yet.
+ // DeployPlugin(p, dirty);
+
+ // // Finalize
+ // FinalizePlugin(p);
+ //}
+
+ if (dirty && DataSettings.DatabaseIsInstalled())
+ {
+ // Save current hash of all deployed plugins to disk
+ var hash = ComputePluginsHash(_referencedPlugins.Values.OrderBy(x => x.FolderName).ToArray());
+ SavePluginsHash(hash);
- var plugins = LoadPluginDescriptors().ToArray();
- var compatiblePlugins = plugins.Where(x => !x.Incompatible).ToArray();
+ // Save names of all deployed assemblies to disk (so we can nuke them later)
+ SavePluginsAssemblies(_referencedPlugins.Values);
+ }
- // If plugins state is dirty, we copy files over to the dynamic folder,
- // otherwise we just reference the previously copied file.
- var dirty = DetectAndCleanStalePlugins(compatiblePlugins);
+ IncompatiblePlugins = incompatiblePlugins.AsReadOnly();
- foreach (var plugin in plugins)
+ ms = watch.ElapsedMilliseconds;
+ Debug.WriteLine("INIT PLUGINS (Deployment complete). Time elapsed: {0} ms.".FormatCurrentUI(ms));
+
+ void DeployPlugin(PluginDescriptor p, bool shadowCopy)
+ {
+ if (p.Incompatible)
{
- if (plugin.Incompatible)
- {
- incompatiblePlugins.Add(plugin.SystemName);
- continue;
- }
- else
+ // Do nothing if plugin is incompatible
+ return;
+ }
+
+ // First copy referenced local assemblies (if any)
+ for (int i = 0; i < p.ReferencedLocalAssemblies.Length; ++i)
+ {
+ var refAr = p.ReferencedLocalAssemblies[i];
+ if (refAr.Assembly == null)
{
- _referencedPlugins[plugin.SystemName] = plugin;
+ Probe(refAr, p, shadowCopy);
}
+ }
- try
- {
- // Shadow copy main plugin assembly
- plugin.ReferencedAssembly = Probe(plugin.OriginalAssemblyFile, dirty);
-
- // Shadow copy other referenced plugin local assemblies
- if (plugin.ReferencedLocalAssemblyFiles != null)
- {
- foreach (var assemblyFile in plugin.ReferencedLocalAssemblyFiles)
- {
- Probe(assemblyFile, dirty);
- }
- }
-
- if (!plugin.Installed)
- {
- _inactiveAssemblies.Add(plugin.ReferencedAssembly);
- }
-
- // Initialize: Find IPlugin, IPreApplicationStart, IConfigurable etc.
- ActivatePlugin(plugin);
- }
- catch (UnauthorizedAccessException)
+ // Then copy main plugin assembly
+ var ar = p.Assembly;
+ if (ar.Assembly == null)
+ {
+ Probe(ar, p, shadowCopy);
+ if (ar.Assembly != null)
{
- // Throw the exception if its UnauthorizedAccessException as this will
- // be because we most likely cannot copy to the dynamic folder.
- throw;
+ // Activate (even if uninstalled): Find IPlugin, IPreApplicationStart, IConfigurable etc.
+ ActivatePlugin(p);
}
- catch (ReflectionTypeLoadException ex)
- {
- var msg = string.Empty;
- foreach (var e in ex.LoaderExceptions)
- {
- msg += e.Message + Environment.NewLine;
- }
+ }
+ }
- HandlePluginActivationException(ex, plugin, msg, incompatiblePlugins);
- }
- catch (Exception ex)
- {
- var msg = string.Empty;
- for (var e = ex; e != null; e = e.InnerException)
- {
- msg += e.Message + Environment.NewLine;
- }
+ void FinalizePlugin(PluginDescriptor p)
+ {
+ _referencedPlugins[p.SystemName] = p;
- HandlePluginActivationException(ex, plugin, msg, incompatiblePlugins);
- }
+ if (p.Incompatible)
+ {
+ incompatiblePlugins.Add(p.SystemName);
+ return;
}
- if (dirty && DataSettings.DatabaseIsInstalled())
+ var firstFailedAssembly = p.ReferencedLocalAssemblies.FirstOrDefault(x => x.ActivationException != null);
+ if (firstFailedAssembly == null && p.Assembly.ActivationException != null)
{
- // Save current hash of all deployed plugins to disk
- var hash = ComputePluginsHash(_referencedPlugins.Values.OrderBy(x => x.FolderName).ToArray());
- SavePluginsHash(hash);
-
- // Save names of all deployed assemblies to disk (so we can nuke them later)
- SavePluginsAssemblies(_referencedPlugins.Values);
+ firstFailedAssembly = p.Assembly;
+ }
+ if (firstFailedAssembly != null)
+ {
+ Debug.WriteLine(firstFailedAssembly.ActivationException.Message);
+ p.Incompatible = true;
+ incompatiblePlugins.Add(p.SystemName);
}
- IncompatiblePlugins = incompatiblePlugins.AsReadOnly();
+ if ((!p.Installed || firstFailedAssembly != null) && p.Assembly.Assembly != null)
+ {
+ inactiveAssemblies.Add(p.Assembly.Assembly);
+ }
}
-
- #endregion
}
///
@@ -216,7 +238,7 @@ private static IEnumerable LoadPluginDescriptors()
if (!pluginsDir.Exists)
{
pluginsDir.Create();
- yield break;
+ return Enumerable.Empty();
}
// Determine all plugin folders: ~/Plugins/{SystemName}
@@ -225,19 +247,56 @@ private static IEnumerable LoadPluginDescriptors()
.OrderBy(x => x.Name)
.ToArray();
- var installedPluginSystemNames = PluginFileParser.ParseInstalledPluginsFile();
+ var installedPluginSystemNames = PluginFileParser.ParseInstalledPluginsFile().AsSynchronized();
// Load/activate all plugins
- foreach (var d in allPluginDirs)
- {
- var descriptor = LoadPluginDescriptor(d, installedPluginSystemNames);
- if (descriptor != null)
- {
- yield return descriptor;
- }
- }
+ return allPluginDirs
+ .AsParallel()
+ .AsOrdered()
+ .Select(d => LoadPluginDescriptor(d, installedPluginSystemNames))
+ .Where(x => x != null);
}
+ /////
+ ///// Loads and parses the descriptors of all installed plugins
+ /////
+ ///// All descriptors
+ //private static Task ReadPluginDescriptors(BlockingCollection bag)
+ //{
+ // // TODO: Add verbose exception handling / raising here since this is happening on app startup and could
+ // // prevent app from starting altogether
+
+ // return Task.Factory.StartNew(() =>
+ // {
+ // var pluginsDir = new DirectoryInfo(CommonHelper.MapPath(_pluginsPath));
+
+ // if (!pluginsDir.Exists)
+ // {
+ // pluginsDir.Create();
+ // }
+ // else
+ // {
+ // // Determine all plugin folders: ~/Plugins/{SystemName}
+ // var allPluginDirs = pluginsDir.EnumerateDirectories().ToArray()
+ // .Where(x => !x.Name.IsMatch("bin") && !x.Name.IsMatch("_Backup"))
+ // .OrderBy(x => x.Name)
+ // .ToArray();
+
+ // var installedPluginSystemNames = PluginFileParser.ParseInstalledPluginsFile().AsSynchronized();
+
+ // // Load/activate all plugins
+ // allPluginDirs
+ // .AsParallel()
+ // .AsOrdered()
+ // .Select(d => LoadPluginDescriptor(d, installedPluginSystemNames))
+ // .Where(x => x != null)
+ // .ForAll(x => bag.Add(x));
+ // }
+
+ // bag.CompleteAdding();
+ // });
+ //}
+
private static PluginDescriptor LoadPluginDescriptor(DirectoryInfo d, ICollection installedPluginSystemNames)
{
var descriptionFile = new FileInfo(Path.Combine(d.FullName, "Description.txt"));
@@ -282,9 +341,9 @@ private static PluginDescriptor LoadPluginDescriptor(DirectoryInfo d, ICollectio
.ToDictionarySafe(x => x.Name, StringComparer.OrdinalIgnoreCase);
// Set 'OriginalAssemblyFile' property
- descriptor.OriginalAssemblyFile = pluginBinaries.Get(descriptor.PluginFileName);
+ descriptor.Assembly.OriginalFile = pluginBinaries.Get(descriptor.PluginFileName);
- if (descriptor.OriginalAssemblyFile == null)
+ if (descriptor.Assembly.OriginalFile == null)
{
throw new SmartException("The main assembly '{0}' for plugin '{1}' could not be found.".FormatInvariant(descriptor.PluginFileName, descriptor.SystemName));
}
@@ -294,7 +353,7 @@ private static PluginDescriptor LoadPluginDescriptor(DirectoryInfo d, ICollectio
.Where(x => !x.Key.IsCaseInsensitiveEqual(descriptor.PluginFileName))
.Select(x => x.Value);
- descriptor.ReferencedLocalAssemblyFiles = otherAssemblyFiles.ToArray();
+ descriptor.ReferencedLocalAssemblies = otherAssemblyFiles.Select(x => new AssemblyReference { OriginalFile = x }).ToArray();
return descriptor;
}
@@ -302,15 +361,15 @@ private static PluginDescriptor LoadPluginDescriptor(DirectoryInfo d, ICollectio
private static void ActivatePlugin(PluginDescriptor plugin)
{
// Init plugin type (only one plugin per assembly is allowed)
- var exportedTypes = plugin.ReferencedAssembly.ExportedTypes;
bool pluginFound = false;
bool preStarterFound = !plugin.Installed;
+ var exportedTypes = plugin.Assembly.Assembly.GetExportedTypes();
foreach (var t in exportedTypes)
{
if (typeof(IPlugin).IsAssignableFrom(t) && !t.IsInterface && t.IsClass && !t.IsAbstract)
{
- plugin.PluginType = t;
+ plugin.PluginClrType = t;
plugin.IsConfigurable = typeof(IConfigurable).IsAssignableFrom(t);
pluginFound = true;
}
@@ -332,22 +391,6 @@ private static void ActivatePlugin(PluginDescriptor plugin)
}
}
- private static void HandlePluginActivationException(Exception ex, PluginDescriptor plugin, string msg, ICollection incompatiblePlugins)
- {
- msg = "Error loading plugin '{0}'".FormatInvariant(plugin.SystemName) + Environment.NewLine + msg;
-
- var fail = new SmartException(msg, ex);
- Debug.WriteLine(fail.Message);
-
- if (plugin.ReferencedAssembly != null)
- {
- _inactiveAssemblies.Add(plugin.ReferencedAssembly);
- }
-
- plugin.ActivationException = fail;
- incompatiblePlugins.Add(plugin.SystemName);
- }
-
///
/// Mark plugin as installed
///
@@ -527,51 +570,101 @@ private static void SetNativeDllPath()
///
/// Perform file deploy
///
- /// Plugin main dll file info
+ /// Assembly reference to probe
/// Reference to the shadow copied Assembly
- private static Assembly Probe(FileInfo plugin, bool performShadowCopy)
+ private static Assembly Probe(AssemblyReference ar, PluginDescriptor d, bool shadowCopy)
{
- if (plugin.Directory == null || plugin.Directory.Parent == null)
+ var file = ar.OriginalFile;
+
+ try
{
- throw new InvalidOperationException("The plugin directory for the " + plugin.Name +
- " file exists in a folder outside of the allowed SmartStore folder hierarchy");
+ if (file.Directory == null || file.Directory.Parent == null)
+ {
+ throw new InvalidOperationException("The plugin directory for the " + file.Name +
+ " file exists in a folder outside of the allowed SmartStore folder hierarchy");
+ }
+
+ ar.File = InitializeFullTrust(file, shadowCopy);
+
+ // Load assembly locked, because concurrent load calls - even with different assemblies -
+ // will result in strange app init behaviour.
+ lock (_lock)
+ {
+ // We can now register the plugin definition
+ ar.Assembly = Assembly.Load(AssemblyName.GetAssemblyName(ar.File.FullName));
+
+ // Add the reference to the build manager
+ if (ar.Assembly != null)
+ {
+ // Loading assembly can fail in parallel loops.
+ // In this case, we'll probe again later in a sequential loop.
+ BuildManager.AddReferencedAssembly(ar.Assembly);
+ }
+ }
+
+ ar.ActivationException = null;
}
+ catch (UnauthorizedAccessException)
+ {
+ // Throw the exception if its UnauthorizedAccessException as this will
+ // be because we most likely cannot copy to the dynamic folder.
+ throw;
+ }
+ catch (ReflectionTypeLoadException ex)
+ {
+ var msg = string.Empty;
+ foreach (var e in ex.LoaderExceptions)
+ {
+ msg += e.Message + Environment.NewLine;
+ }
- var probedPlugin = InitializeFullTrust(plugin, performShadowCopy);
+ ar.ActivationException = CreateException(msg, ex);
+ }
+ catch (Exception ex)
+ {
+ var msg = string.Empty;
+ for (var e = ex; e != null; e = e.InnerException)
+ {
+ msg += e.Message + Environment.NewLine;
+ }
- // We can now register the plugin definition
- var probedAssembly = Assembly.Load(AssemblyName.GetAssemblyName(probedPlugin.FullName));
+ ar.ActivationException = CreateException(msg, ex);
+ }
- // Add the reference to the build manager
- BuildManager.AddReferencedAssembly(probedAssembly);
+ return ar.Assembly;
- return probedAssembly;
- }
+ Exception CreateException(string message, Exception innerException)
+ {
+ return new SmartException(
+ "Error loading plugin '{0}'".FormatInvariant(d.SystemName) + Environment.NewLine + message,
+ innerException);
+ }
+ }
///
/// Used to initialize plugins when running in Full Trust
///
- /// Plugin main dll file
+ /// Plugin dll file
/// Shadow copied file
- private static FileInfo InitializeFullTrust(FileInfo plugin, bool performShadowCopy)
+ private static FileInfo InitializeFullTrust(FileInfo dll, bool shadowCopy)
{
- var probedPlugin = new FileInfo(Path.Combine(_shadowCopyDir.FullName, plugin.Name));
+ var probedDll = new FileInfo(Path.Combine(_shadowCopyDir.FullName, dll.Name));
// If instructed to not perform the copy, just return the path to where it is supposed to be
- if (!performShadowCopy && probedPlugin.Exists)
- return probedPlugin;
+ if (!shadowCopy && probedDll.Exists)
+ return probedDll;
try
{
- File.Copy(plugin.FullName, probedPlugin.FullName, true);
+ File.Copy(dll.FullName, probedDll.FullName, true);
}
catch (UnauthorizedAccessException)
{
- throw new UnauthorizedAccessException(string.Format("Access to the path '{0}' is denied, ensure that read, write and modify permissions are allowed.", probedPlugin.Directory.FullName));
+ throw new UnauthorizedAccessException(string.Format("Access to the path '{0}' is denied, ensure that read, write and modify permissions are allowed.", probedDll.Directory.FullName));
}
catch (IOException)
{
- Debug.WriteLine(probedPlugin.FullName + " is locked, attempting to rename");
+ Debug.WriteLine(probedDll.FullName + " is locked, attempting to rename");
// This occurs when the files are locked,
// For some reason devenv locks plugin files some times and for another crazy reason you are allowed to rename them
@@ -579,23 +672,23 @@ private static FileInfo InitializeFullTrust(FileInfo plugin, bool performShadowC
try
{
// If all else fails during the cleanup and we cannot copy over so we need to rename with a GUID
- var deleteName = GetNewDeleteName(probedPlugin);
- File.Move(probedPlugin.FullName, deleteName);
+ var deleteName = GetNewDeleteName(probedDll);
+ File.Move(probedDll.FullName, deleteName);
}
catch (UnauthorizedAccessException)
{
- throw new UnauthorizedAccessException(string.Format("Access to the path '{0}' is denied, ensure that read, write and modify permissions are allowed.", probedPlugin.Directory.FullName));
+ throw new UnauthorizedAccessException(string.Format("Access to the path '{0}' is denied, ensure that read, write and modify permissions are allowed.", probedDll.Directory.FullName));
}
catch (IOException exc)
{
- throw new IOException(probedPlugin.FullName + " rename failed, cannot initialize plugin", exc);
+ throw new IOException(probedDll.FullName + " rename failed, cannot initialize plugin", exc);
}
// OK, we've made it this so far, now retry the shadow copy
- File.Copy(plugin.FullName, probedPlugin.FullName, true);
+ File.Copy(dll.FullName, probedDll.FullName, true);
}
- return probedPlugin;
+ return probedDll;
}
///
diff --git a/src/Libraries/SmartStore.Core/Plugins/Providers/ProviderMetadata.cs b/src/Libraries/SmartStore.Core/Plugins/Providers/ProviderMetadata.cs
index d07a628170..8b806c43fc 100644
--- a/src/Libraries/SmartStore.Core/Plugins/Providers/ProviderMetadata.cs
+++ b/src/Libraries/SmartStore.Core/Plugins/Providers/ProviderMetadata.cs
@@ -1,13 +1,41 @@
using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
using SmartStore.Core.Domain.DataExchange;
+using SmartStore.Utilities;
namespace SmartStore.Core.Plugins
{
- public class ProviderMetadata
+ public interface IProviderMetadata
+ {
+ ///
+ /// Gets the provider system name
+ ///
+ string SystemName { get; }
+
+ ///
+ /// Gets the resource key pattern for user data (e.g. FriendlyName)
+ ///
+ ///
+ /// Plugins.{1}.{0} > 0 = provider system name, 1 = propertyname
+ ///
+ string ResourceKeyPattern { get; }
+
+ ///
+ /// Gets the provider friendly name
+ ///
+ string FriendlyName { get; }
+
+ ///
+ /// Gets the provider description
+ ///
+ string Description { get; }
+
+ ///
+ /// Gets the provider display order
+ ///
+ int DisplayOrder { get; }
+ }
+
+ public class ProviderMetadata : IProviderMetadata
{
///
/// Gets or sets the provider type
@@ -112,7 +140,7 @@ public override bool Equals(object obj)
public override int GetHashCode()
{
- return SystemName.GetHashCode();
+ return HashCodeCombiner.Start().Add(typeof(ProviderMetadata)).Add(SystemName).CombinedHash;
}
}
}
diff --git a/src/Libraries/SmartStore.Core/Search/AcquireWriterContext.cs b/src/Libraries/SmartStore.Core/Search/AcquireWriterContext.cs
index 23cada0323..e0df140361 100644
--- a/src/Libraries/SmartStore.Core/Search/AcquireWriterContext.cs
+++ b/src/Libraries/SmartStore.Core/Search/AcquireWriterContext.cs
@@ -11,7 +11,6 @@ namespace SmartStore.Core.Search
public enum AcquirementReason
{
Indexing,
- Optimizing,
Deleting
}
@@ -98,9 +97,10 @@ public void Clear()
Currencies.Clear();
StoreMappings.Clear();
CustomerRoleMappings.Clear();
- DeliveryTimes.Clear();
Manufacturers.Clear();
Categories.Clear();
+ DeliveryTimes.Clear();
+ Translations.Clear();
CustomProperties.Clear();
}
}
diff --git a/src/Libraries/SmartStore.Core/SmartStore.Core.csproj b/src/Libraries/SmartStore.Core/SmartStore.Core.csproj
index df978dc456..e89e76c15d 100644
--- a/src/Libraries/SmartStore.Core/SmartStore.Core.csproj
+++ b/src/Libraries/SmartStore.Core/SmartStore.Core.csproj
@@ -1,5 +1,6 @@
-
+
+
Debug
AnyCPU
@@ -63,11 +64,10 @@
- ..\..\packages\AngleSharp.0.9.9\lib\net45\AngleSharp.dll
+ ..\..\packages\AngleSharp.0.9.11\lib\net45\AngleSharp.dll
-
- ..\..\packages\Autofac.4.5.0\lib\net45\Autofac.dll
- True
+
+ ..\..\packages\Autofac.4.9.0\lib\net45\Autofac.dll
..\..\packages\Autofac.Mvc5.4.0.2\lib\net45\Autofac.Integration.Mvc.dll
@@ -81,6 +81,9 @@
..\..\packages\EntityFramework.6.2.0\lib\net45\EntityFramework.SqlServer.dll
True
+
+ ..\..\packages\HtmlSanitizer.4.0.205\lib\net45\HtmlSanitizer.dll
+
..\..\packages\log4net.2.0.8\lib\net45-full\log4net.dll
True
@@ -89,13 +92,11 @@
True
..\..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll
-
- ..\..\packages\Microsoft.Web.Xdt.2.1.1\lib\net40\Microsoft.Web.XmlTransform.dll
- True
+
+ ..\..\packages\Microsoft.Web.Xdt.3.0.0\lib\net40\Microsoft.Web.XmlTransform.dll
-
- ..\..\packages\Newtonsoft.Json.10.0.2\lib\net45\Newtonsoft.Json.dll
- True
+
+ ..\..\packages\Newtonsoft.Json.12.0.1\lib\net45\Newtonsoft.Json.dll
..\..\packages\NuGet.Core.2.14.0\lib\net40-Client\NuGet.Core.dll
@@ -119,33 +120,27 @@
-
- ..\..\packages\System.ValueTuple.4.4.0\lib\net461\System.ValueTuple.dll
+
+ ..\..\packages\System.ValueTuple.4.5.0\lib\net461\System.ValueTuple.dll
- False
- ..\..\packages\Microsoft.AspNet.WebPages.3.2.3\lib\net45\System.Web.Helpers.dll
+ ..\..\packages\Microsoft.AspNet.WebPages.3.2.7\lib\net45\System.Web.Helpers.dll
-
- False
- ..\..\packages\Microsoft.AspNet.Mvc.5.2.3\lib\net45\System.Web.Mvc.dll
+
+ ..\..\packages\Microsoft.AspNet.Mvc.5.2.7\lib\net45\System.Web.Mvc.dll
- False
- ..\..\packages\Microsoft.AspNet.Razor.3.2.3\lib\net45\System.Web.Razor.dll
+ ..\..\packages\Microsoft.AspNet.Razor.3.2.7\lib\net45\System.Web.Razor.dll
- False
- ..\..\packages\Microsoft.AspNet.WebPages.3.2.3\lib\net45\System.Web.WebPages.dll
+ ..\..\packages\Microsoft.AspNet.WebPages.3.2.7\lib\net45\System.Web.WebPages.dll
- False
- ..\..\packages\Microsoft.AspNet.WebPages.3.2.3\lib\net45\System.Web.WebPages.Deployment.dll
+ ..\..\packages\Microsoft.AspNet.WebPages.3.2.7\lib\net45\System.Web.WebPages.Deployment.dll
- False
- ..\..\packages\Microsoft.AspNet.WebPages.3.2.3\lib\net45\System.Web.WebPages.Razor.dll
+ ..\..\packages\Microsoft.AspNet.WebPages.3.2.7\lib\net45\System.Web.WebPages.Razor.dll
@@ -160,10 +155,12 @@
Properties\AssemblyVersionInfo.cs
-
+
+
+
@@ -179,6 +176,7 @@
+
@@ -216,7 +214,11 @@
+
+
+
+
@@ -250,7 +252,17 @@
+
+
+
+
+
+
+
+
+
+
@@ -259,6 +271,7 @@
+
@@ -281,7 +294,7 @@
-
+
@@ -336,11 +349,7 @@
-
-
-
-
@@ -392,7 +401,6 @@
-
@@ -684,6 +692,7 @@
+
@@ -692,6 +701,7 @@
+
diff --git a/src/Libraries/SmartStore.Core/SmartStoreVersion.cs b/src/Libraries/SmartStore.Core/SmartStoreVersion.cs
index 75f292f08f..e5bc5e5e48 100644
--- a/src/Libraries/SmartStore.Core/SmartStoreVersion.cs
+++ b/src/Libraries/SmartStore.Core/SmartStoreVersion.cs
@@ -43,8 +43,11 @@ public static class SmartStoreVersion
new Version("2.5"),
new Version("3.0"),
new Version("3.1"),
- new Version("3.1.5")
- };
+ new Version("3.1.5"),
+ new Version("3.2"),
+ new Version("3.2.1"),
+ new Version("3.2.2")
+ };
private const string HELP_BASEURL = "https://docs.smartstore.com/display/";
@@ -113,8 +116,8 @@ public static string GenerateHelpUrl(string languageCode, string path)
public static string GetUserGuideSpaceKey(string languageCode)
{
return languageCode.IsCaseInsensitiveEqual("de")
- ? "SDDE31"
- : "SMNET31";
+ ? "SDDE32"
+ : "SMNET32";
}
///
diff --git a/src/Libraries/SmartStore.Core/Themes/DefaultThemeRegistry.cs b/src/Libraries/SmartStore.Core/Themes/DefaultThemeRegistry.cs
index 5a4a2253b2..0c90853524 100644
--- a/src/Libraries/SmartStore.Core/Themes/DefaultThemeRegistry.cs
+++ b/src/Libraries/SmartStore.Core/Themes/DefaultThemeRegistry.cs
@@ -8,6 +8,7 @@
using System.Threading;
using SmartStore.Collections;
using SmartStore.Core.Events;
+using SmartStore.Core.Infrastructure.DependencyManagement;
using SmartStore.Core.Logging;
using SmartStore.Utilities;
@@ -29,10 +30,10 @@ public partial class DefaultThemeRegistry : DisposableObject, IThemeRegistry
public DefaultThemeRegistry(IEventPublisher eventPublisher, IApplicationEnvironment env, bool? enableMonitoring, string themesBasePath, bool autoLoadThemes)
{
- this._enableMonitoring = enableMonitoring ?? CommonHelper.GetAppSetting("sm:MonitorThemesFolder", true);
- this._eventPublisher = eventPublisher;
- this._env = env;
- this._themesBasePath = themesBasePath.NullEmpty() ?? _env.ThemesFolder.RootPath;
+ _enableMonitoring = enableMonitoring ?? CommonHelper.GetAppSetting("sm:MonitorThemesFolder", true);
+ _eventPublisher = eventPublisher;
+ _env = env;
+ _themesBasePath = themesBasePath.NullEmpty() ?? _env.ThemesFolder.RootPath;
Logger = NullLogger.Instance;
diff --git a/src/Libraries/SmartStore.Core/Themes/InheritedThemeFileResult.cs b/src/Libraries/SmartStore.Core/Themes/InheritedThemeFileResult.cs
index 09c40503e8..77d708c5c8 100644
--- a/src/Libraries/SmartStore.Core/Themes/InheritedThemeFileResult.cs
+++ b/src/Libraries/SmartStore.Core/Themes/InheritedThemeFileResult.cs
@@ -34,10 +34,10 @@ public class InheritedThemeFileResult
///
public string ResultThemeName { get; set; }
- public bool IsExplicit { get; set; }
+ public bool IsBased { get; set; }
///
- /// The query string, e.g. '?explicit'
+ /// The query string, e.g. '?base'
///
public string Query { get; set; }
}
diff --git a/src/Libraries/SmartStore.Core/Utilities/CommonHelper.cs b/src/Libraries/SmartStore.Core/Utilities/CommonHelper.cs
index 2f337b8c39..1fb1f2240d 100644
--- a/src/Libraries/SmartStore.Core/Utilities/CommonHelper.cs
+++ b/src/Libraries/SmartStore.Core/Utilities/CommonHelper.cs
@@ -19,7 +19,20 @@ namespace SmartStore.Utilities
public static partial class CommonHelper
{
private static bool? _isDevEnvironment;
+
+ [ThreadStatic]
+ private static Random _random;
+ private static Random GetRandomizer()
+ {
+ if (_random == null)
+ {
+ _random = new Random();
+ }
+
+ return _random;
+ }
+
///
/// Generate random digit code
///
@@ -27,19 +40,21 @@ public static partial class CommonHelper
/// Result string
public static string GenerateRandomDigitCode(int length)
{
- var random = new Random();
- string str = string.Empty;
- for (int i = 0; i < length; i++)
- str = String.Concat(str, random.Next(10).ToString());
- return str;
- }
+ var buffer = new int[length];
+ for (int i = 0; i < length; ++i)
+ {
+ buffer[i] = GetRandomizer().Next(10);
+ }
+
+ return string.Join("", buffer);
+ }
///
- /// Returns an random interger number within a specified rage
+ /// Returns a random number within the range to - 1.
///
/// Minimum number
- /// Maximum number
- /// Result
+ /// Maximum number (exclusive!).
+ /// Random integer number.
public static int GenerateRandomInteger(int min = 0, int max = 2147483647)
{
var randomNumberBuffer = new byte[10];
diff --git a/src/Libraries/SmartStore.Core/Utilities/FileDownloadManager.cs b/src/Libraries/SmartStore.Core/Utilities/FileDownloadManager.cs
index a7f0a60e39..40572c1b8b 100644
--- a/src/Libraries/SmartStore.Core/Utilities/FileDownloadManager.cs
+++ b/src/Libraries/SmartStore.Core/Utilities/FileDownloadManager.cs
@@ -104,10 +104,10 @@ public FileDownloadResponse DownloadFile(string url, bool sendAuthCookie = false
/// Items to be downloaded
public async Task DownloadAsync(FileDownloadManagerContext context, IEnumerable items)
{
- await DownloadFiles(context, items);
+ await DownloadFilesAsync(context, items);
}
- private async Task DownloadFiles(FileDownloadManagerContext context, IEnumerable items)
+ private async Task DownloadFilesAsync(FileDownloadManagerContext context, IEnumerable